diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 6a9459fc0..1673b2149 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -41,8 +41,8 @@ --vp-c-indigo-darker: hsl(var(--indigo-h-s) 10%); --vp-c-indigo-soft: hsl(var(--indigo-h-s) 10% / 0.75); - --vp-c-divider-light-1: hsl(var(--neutral-h-s) 50% / 0.25); - --vp-c-divider-light-2: hsl(var(--neutral-h-s) 50% / 0.125); + --vp-c-divider-light-1: hsl(var(--neutral-h-s) 50% / 0.2); + --vp-c-divider-light-2: hsl(var(--neutral-h-s) 50% / 0.1); --vp-c-divider-dark-1: hsl(var(--neutral-h-s) 50% / 0.5); --vp-c-divider-dark-2: hsl(var(--neutral-h-s) 50% / 0.33); diff --git a/docs/introduction.md b/docs/introduction.md index a6cb504de..b7d57d3ec 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -8,9 +8,9 @@ Any application that supports [the Micropub protocol](https://micropub.spec.indi ```sh POST /micropub HTTP/1.1 -Host: indiekit.mywebsite.com +Host: indiekit.website.example Content-Type: application/x-www-form-urlencoded -Authorization: Bearer XXXXXXX +Authorization: Bearer [ACCESS_TOKEN] h=entry &content=Hello+world @@ -39,22 +39,27 @@ Note the `mp-syndicate-to` property in the above example. If you’ve configured You can then send a second `POST` request, this time to `https://indiekit.website.example/syndicate` along with your access token which you can find on your server’s status page: ```sh -POST /syndicate HTTP/1.1 -Host: indiekit.mywebsite.com -Content-Type: application/x-www-form-urlencoded - -token=XXXXXXX +POST /syndicate?token=[ACCESS_TOKEN] HTTP/1.1 +Host: indiekit.website.example +Accept: application/json ``` -This will tell Indiekit to syndicate the most recent un-syndicated post to the third-party websites listed in the front matter. +This will tell Indiekit to syndicate the most recent un-syndicated post to the third-party websites listed in a post’s front matter. ::: tip ### Use an outgoing webhook on Netlify -Netlify allows [posting to an outgoing webhook](https://docs.netlify.com/site-deploys/notifications/#outgoing-webhooks) once a deploy has succeeded. +If you are using [Netlify](https://www.netlify.com) to host your website, you can send a notification to the syndication endpoint once a deployment has been completed. + +First, create an environment variable for your Indiekit server called `WEBHOOK_SECRET` and give it a secret, hard-to-guess value. + +Then on Netlify, in your site’s ‘Build & Deploy’ settings, add an [outgoing webhook](https://docs.netlify.com/site-deploys/notifications/#outgoing-webhooks) with the following values: + +* **Event to listen for:** ‘Deploy succeeded’ +* **URL to notify:** `[YOUR_INDIEKIT_URL]/syndicate` +* **JWS secret token:** The same value you used for `WEBHOOK_SECRET` -In ‘URL to notify’, enter your server’s syndication endpoint with your access token as the `token` parameter, for example: `https://indiekit.website.example/syndicate?token=XXXXXXX`. ::: Once this has been completed, Indiekit will update the post, replacing `mp-syndicate-to` with a `syndication` property listing the location of each syndicated copy: diff --git a/package-lock.json b/package-lock.json index 1b7e5d6ac..effaec516 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,30 +62,39 @@ } }, "helpers/access-token": { + "name": "@indiekit-test/token", "license": "MIT" }, "helpers/config": { + "name": "@indiekit-test/config", "license": "MIT" }, "helpers/fixtures": { + "name": "@indiekit-test/fixtures", "license": "MIT" }, "helpers/frontend": { + "name": "@indiekit-test/frontend", "license": "MIT" }, "helpers/media-data": { + "name": "@indiekit-test/media-data", "license": "MIT" }, "helpers/mock-agent": { + "name": "@indiekit-test/mock-agent", "license": "MIT" }, "helpers/post-data": { + "name": "@indiekit-test/post-data", "license": "MIT" }, "helpers/publication": { + "name": "@indiekit-test/publication", "license": "MIT" }, "helpers/server": { + "name": "@indiekit-test/server", "license": "MIT", "dependencies": { "get-port": "^6.1.2" @@ -103,9 +112,11 @@ } }, "helpers/session": { + "name": "@indiekit-test/session", "license": "MIT" }, "helpers/store": { + "name": "@indiekit-test/store", "license": "MIT" }, "node_modules/@algolia/autocomplete-core": { @@ -21790,6 +21801,7 @@ } }, "packages/endpoint-auth": { + "name": "@indiekit/endpoint-auth", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21815,6 +21827,7 @@ } }, "packages/endpoint-files": { + "name": "@indiekit/endpoint-files", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21828,6 +21841,7 @@ } }, "packages/endpoint-image": { + "name": "@indiekit/endpoint-image", "version": "1.0.0-beta.0", "license": "MIT", "dependencies": { @@ -21840,6 +21854,7 @@ } }, "packages/endpoint-json-feed": { + "name": "@indiekit/endpoint-json-feed", "version": "1.0.0-beta.0", "license": "MIT", "dependencies": { @@ -21851,6 +21866,7 @@ } }, "packages/endpoint-media": { + "name": "@indiekit/endpoint-media", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21876,6 +21892,7 @@ } }, "packages/endpoint-micropub": { + "name": "@indiekit/endpoint-micropub", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21905,6 +21922,7 @@ } }, "packages/endpoint-posts": { + "name": "@indiekit/endpoint-posts", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21921,6 +21939,7 @@ } }, "packages/endpoint-share": { + "name": "@indiekit/endpoint-share", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21934,11 +21953,13 @@ } }, "packages/endpoint-syndicate": { + "name": "@indiekit/endpoint-syndicate", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.1", "express": "^4.17.1", + "jsonwebtoken": "^9.0.0", "undici": "^5.2.0" }, "engines": { @@ -21946,6 +21967,7 @@ } }, "packages/error": { + "name": "@indiekit/error", "version": "1.0.0-beta.1", "license": "MIT", "engines": { @@ -21953,6 +21975,7 @@ } }, "packages/frontend": { + "name": "@indiekit/frontend", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -21979,6 +22002,7 @@ } }, "packages/indiekit": { + "name": "@indiekit/indiekit", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22002,6 +22026,7 @@ "express-fileupload": "^1.4.0", "express-rate-limit": "^6.2.0", "i18n": "^0.15.0", + "jsonwebtoken": "^9.0.0", "keyv": "^4.2.0", "keyv-mongodb": "^3.0.0", "lodash": "^4.17.21", @@ -22030,6 +22055,7 @@ } }, "packages/preset-hugo": { + "name": "@indiekit/preset-hugo", "version": "1.0.0-alpha.18", "license": "MIT", "dependencies": { @@ -22050,6 +22076,7 @@ } }, "packages/preset-jekyll": { + "name": "@indiekit/preset-jekyll", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22068,6 +22095,7 @@ } }, "packages/store-bitbucket": { + "name": "@indiekit/store-bitbucket", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22079,6 +22107,7 @@ } }, "packages/store-file-system": { + "name": "@indiekit/store-file-system", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22089,6 +22118,7 @@ } }, "packages/store-ftp": { + "name": "@indiekit/store-ftp", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22100,6 +22130,7 @@ } }, "packages/store-gitea": { + "name": "@indiekit/store-gitea", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22111,6 +22142,7 @@ } }, "packages/store-github": { + "name": "@indiekit/store-github", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22122,6 +22154,7 @@ } }, "packages/store-gitlab": { + "name": "@indiekit/store-gitlab", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22133,6 +22166,7 @@ } }, "packages/syndicator-internet-archive": { + "name": "@indiekit/syndicator-internet-archive", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22144,6 +22178,7 @@ } }, "packages/syndicator-mastodon": { + "name": "@indiekit/syndicator-mastodon", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -22158,6 +22193,7 @@ } }, "packages/syndicator-twitter": { + "name": "@indiekit/syndicator-twitter", "version": "1.0.0-beta.1", "license": "MIT", "dependencies": { @@ -23985,6 +24021,7 @@ "requires": { "@indiekit/error": "^1.0.0-beta.1", "express": "^4.17.1", + "jsonwebtoken": "^9.0.0", "undici": "^5.2.0" } }, @@ -24036,6 +24073,7 @@ "express-fileupload": "^1.4.0", "express-rate-limit": "^6.2.0", "i18n": "^0.15.0", + "jsonwebtoken": "^9.0.0", "keyv": "^4.2.0", "keyv-mongodb": "^3.0.0", "lodash": "^4.17.21", diff --git a/packages/endpoint-posts/includes/endpoint-posts-syndicate.njk b/packages/endpoint-posts/includes/endpoint-posts-syndicate.njk index 46e190213..407431e92 100644 --- a/packages/endpoint-posts/includes/endpoint-posts-syndicate.njk +++ b/packages/endpoint-posts/includes/endpoint-posts-syndicate.njk @@ -1,6 +1,6 @@
{{ input({ - name: "token", + name: "access_token", type: "hidden", value: token }) | indent(2) }} diff --git a/packages/endpoint-syndicate/README.md b/packages/endpoint-syndicate/README.md index b81210baa..c11b8d393 100644 --- a/packages/endpoint-syndicate/README.md +++ b/packages/endpoint-syndicate/README.md @@ -27,5 +27,47 @@ Add `@indiekit/endpoint-syndicate` to your list of plug-ins, specifying options ## Supported endpoint queries -- Access token (required): `/syndicate?token=XXXXXXX` -- URL to syndicate: `/syndicate?token=XXXXXXX&source_url=https%3A%2F%2Fwebsite.example%2Fposts%2F1` +- URL to syndicate: `/syndicate?source_url=https%3A%2F%2Fwebsite.example%2Fposts%2F1` + +## Authorization + +Authorization is needed to update posts with any syndicated URLs. This can be done in a few different ways: + +### Query string + +Include your server’s access token as the `token` query: + +```http +POST /syndicate?token=[ACCESS_TOKEN] HTTP/1.1 +Host: indiekit.website.example +Accept: application/json +``` + +You can find an access token on your server’s status page. + +### Form body + +Include a value for `access_token` in your form submission: + +```http +POST /syndicate HTTP/1.1 +Host: indiekit.website.example +Content-type: application/x-www-form-urlencoded +Accept: application/json + +access_token=[ACCESS_TOKEN] +``` + +You can find an access token on your server’s status page. + +### Using a webhook secret (Netlify only) + +If you are using [Netlify](https://www.netlify.com) to host your website, you can send a notification to the syndication endpoint once a deployment has been completed. + +First, create an environment variable for your Indiekit server called `WEBHOOK_SECRET` and give it a secret, hard-to-guess value. + +Then on Netlify, in your site’s ‘Build & Deploy’ settings, add an [outgoing webhook](https://docs.netlify.com/site-deploys/notifications/#outgoing-webhooks) with the following values: + +- **Event to listen for:** ‘Deploy succeeded’ +- **URL to notify:** `[YOUR_INDIEKIT_URL]/syndicate` +- **JWS secret token:** The same value you used for `WEBHOOK_SECRET` diff --git a/packages/endpoint-syndicate/index.js b/packages/endpoint-syndicate/index.js index e209fbc0a..81f23c3fa 100644 --- a/packages/endpoint-syndicate/index.js +++ b/packages/endpoint-syndicate/index.js @@ -13,7 +13,7 @@ export default class SyndicateEndpoint { this.mountPath = this.options.mountPath; } - get routes() { + get routesPublic() { router.post("/", syndicateController.post); return router; diff --git a/packages/endpoint-syndicate/lib/controllers/syndicate.js b/packages/endpoint-syndicate/lib/controllers/syndicate.js index 8a8215926..8cfed0c17 100644 --- a/packages/endpoint-syndicate/lib/controllers/syndicate.js +++ b/packages/endpoint-syndicate/lib/controllers/syndicate.js @@ -1,12 +1,14 @@ import { IndiekitError } from "@indiekit/error"; import { fetch } from "undici"; +import { findBearerToken } from "../token.js"; import { getPostData } from "../utils.js"; export const syndicateController = { async post(request, response, next) { try { const { application, publication } = request.app.locals; - const token = request.query.token || request.body.token; + const bearerToken = findBearerToken(request); + const sourceUrl = request.query.source_url || request.body.syndication?.source_url; const redirectUri = @@ -76,7 +78,7 @@ export const syndicateController = { method: "POST", headers: { accept: "application/json", - authorization: `Bearer ${token}`, + authorization: `Bearer ${bearerToken}`, "content-type": "application/json", }, body: JSON.stringify({ diff --git a/packages/endpoint-syndicate/lib/token.js b/packages/endpoint-syndicate/lib/token.js new file mode 100644 index 000000000..5063df820 --- /dev/null +++ b/packages/endpoint-syndicate/lib/token.js @@ -0,0 +1,57 @@ +import process from "node:process"; +import { IndiekitError } from "@indiekit/error"; +import jwt from "jsonwebtoken"; + +export const findBearerToken = (request) => { + if (request.headers?.["x-webhook-signature"]) { + const signature = request.headers["x-webhook-signature"]; + const verifiedToken = verifyToken(signature); + const bearerToken = signToken(verifiedToken, request.body.url); + return bearerToken; + } + + if (request.body?.access_token) { + const bearerToken = request.body.access_token; + delete request.body.access_token; + return bearerToken; + } + + if (request.query?.token) { + const bearerToken = request.query.token; + return bearerToken; + } + + throw IndiekitError.invalidRequest("No bearer token provided by request"); +}; + +/** + * Generate short-lived bearer token with update scope + * + * @param {object} verifiedToken - JSON Web Token + * @param {string} url - Publication URL + * @returns {string} Signed JSON Web Token + */ +export const signToken = (verifiedToken, url) => + jwt.sign( + { + me: url, + scope: "update", + }, + process.env.SECRET, + { + expiresIn: "10m", + } + ); + +/** + * Verify that token provided by signature was issued by Netlify + * + * @param {string} signature - JSON Web Signature + * @returns {object} JSON Web Token + * @see {@link https://docs.netlify.com/site-deploys/notifications/#payload-signature} + */ +export const verifyToken = (signature) => + jwt.verify(signature, process.env.WEBHOOK_SECRET, { + algorithms: ["HS256"], + issuer: ["netlify"], + }); diff --git a/packages/endpoint-syndicate/package.json b/packages/endpoint-syndicate/package.json index 2f9ce37e0..e3f4fa08c 100644 --- a/packages/endpoint-syndicate/package.json +++ b/packages/endpoint-syndicate/package.json @@ -35,6 +35,7 @@ "dependencies": { "@indiekit/error": "^1.0.0-beta.1", "express": "^4.17.1", + "jsonwebtoken": "^9.0.0", "undici": "^5.2.0" }, "publishConfig": { diff --git a/packages/endpoint-syndicate/tests/integration/200-no-post-record-for-url.js b/packages/endpoint-syndicate/tests/integration/200-no-post-record-for-url.js index ee2acc768..f54bbd7a9 100644 --- a/packages/endpoint-syndicate/tests/integration/200-no-post-record-for-url.js +++ b/packages/endpoint-syndicate/tests/integration/200-no-post-record-for-url.js @@ -10,7 +10,7 @@ test("Returns no post record for URL", async (t) => { const request = supertest.agent(server); const result = await request .post("/syndicate") - .auth(testToken(), { type: "bearer" }) + .query({ token: testToken() }) .set("accept", "application/json") .query({ source_url: "https://website.example/notes/foobar/" }); diff --git a/packages/endpoint-syndicate/tests/integration/200-no-post-records.js b/packages/endpoint-syndicate/tests/integration/200-no-post-records.js index 5cafadba5..92b0f44cc 100644 --- a/packages/endpoint-syndicate/tests/integration/200-no-post-records.js +++ b/packages/endpoint-syndicate/tests/integration/200-no-post-records.js @@ -10,7 +10,7 @@ test("Returns no post records", async (t) => { const request = supertest.agent(server); const result = await request .post("/syndicate") - .auth(testToken(), { type: "bearer" }) + .query({ token: testToken() }) .set("accept", "application/json"); t.is(result.status, 200); diff --git a/packages/endpoint-syndicate/tests/integration/200-no-posts-awaiting-syndication.js b/packages/endpoint-syndicate/tests/integration/200-no-posts-awaiting-syndication.js index ddcdb0eed..79a6c82e4 100644 --- a/packages/endpoint-syndicate/tests/integration/200-no-posts-awaiting-syndication.js +++ b/packages/endpoint-syndicate/tests/integration/200-no-posts-awaiting-syndication.js @@ -19,7 +19,7 @@ test("Returns no post records awaiting syndication", async (t) => { .send("name=foobar"); const result = await request .post("/syndicate") - .auth(testToken(), { type: "bearer" }) + .query({ token: testToken() }) .set("accept", "application/json"); t.is(result.status, 200); diff --git a/packages/endpoint-syndicate/tests/integration/200-no-syndication-targets.js b/packages/endpoint-syndicate/tests/integration/200-no-syndication-targets.js index 7dfa5ae62..5d2cedc09 100644 --- a/packages/endpoint-syndicate/tests/integration/200-no-syndication-targets.js +++ b/packages/endpoint-syndicate/tests/integration/200-no-syndication-targets.js @@ -11,14 +11,14 @@ test("Returns no syndication targets configured", async (t) => { const request = supertest.agent(server); await request .post("/micropub") - .auth(testToken(), { type: "bearer" }) + .query(testToken(), { type: "bearer" }) .set("accept", "application/json") .send("h=entry") .send("name=foobar") .send("mp-syndicate-to=https://twitter.com/username"); const result = await request .post("/syndicate") - .auth(testToken(), { type: "bearer" }) + .query({ token: testToken() }) .set("accept", "application/json"); t.is(result.status, 200); diff --git a/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js b/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js new file mode 100644 index 000000000..af46171a7 --- /dev/null +++ b/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js @@ -0,0 +1,56 @@ +import { createHash } from "node:crypto"; +import process from "node:process"; +import test from "ava"; +import nock from "nock"; +import supertest from "supertest"; +import jwt from "jsonwebtoken"; +import { mockAgent } from "@indiekit-test/mock-agent"; +import { testServer } from "@indiekit-test/server"; +import { testToken } from "@indiekit-test/token"; + +await mockAgent("store"); + +test.beforeEach(() => { + process.env.SECRET = "secret"; + process.env.WEBHOOK_SECRET = "webhook-secret"; +}); + +test("Syndicates recent post (via Netlify webhook)", async (t) => { + nock("https://api.twitter.com") + .post("/1.1/statuses/update.json") + .reply(200, { + id_str: "1234567890987654321", // eslint-disable-line camelcase + user: { screen_name: "username" }, // eslint-disable-line camelcase + }); + + const sha256 = createHash("sha256").update("foo").digest("hex"); + const webhookSignature = jwt.sign( + { iss: "netlify", sha256 }, + process.env.WEBHOOK_SECRET + ); + + const server = await testServer({ + plugins: ["@indiekit/syndicator-twitter"], + }); + const request = supertest.agent(server); + await request + .post("/micropub") + .auth(testToken(), { type: "bearer" }) + .set("accept", "application/json") + .send("h=entry") + .send("name=foobar") + .send("mp-syndicate-to=https://twitter.com/username"); + const result = await request + .post("/syndicate") + .set("accept", "application/json") + .set("x-webhook-signature", webhookSignature) + .send({ url: "https://website.example" }); + + t.is(result.status, 200); + t.is( + result.body.success_description, + "Post updated at https://website.example/notes/foobar/" + ); + + server.close(t); +}); diff --git a/packages/endpoint-syndicate/tests/integration/302-syndicates-url-for-target-redirect.js b/packages/endpoint-syndicate/tests/integration/302-syndicates-url-for-target-redirect.js index 6e68575db..97a22f4e0 100644 --- a/packages/endpoint-syndicate/tests/integration/302-syndicates-url-for-target-redirect.js +++ b/packages/endpoint-syndicate/tests/integration/302-syndicates-url-for-target-redirect.js @@ -34,8 +34,8 @@ test("Syndicates a URL", async (t) => { url: "https://website.example/notes/foobar/", redirect_uri: "/posts/12345", }, - }) - .send({ token: testToken() }); + access_token: testToken(), + }); t.is(result.status, 302); t.is( diff --git a/packages/endpoint-syndicate/tests/integration/401-indieauth-error.js b/packages/endpoint-syndicate/tests/integration/401-indieauth-error.js deleted file mode 100644 index 4198119d4..000000000 --- a/packages/endpoint-syndicate/tests/integration/401-indieauth-error.js +++ /dev/null @@ -1,31 +0,0 @@ -import test from "ava"; -import supertest from "supertest"; -import { testServer } from "@indiekit-test/server"; -import { cookie } from "@indiekit-test/session"; -import { testToken } from "@indiekit-test/token"; - -test("Returns 401 error from Micropub endpoint", async (t) => { - const server = await testServer(); - const request = supertest.agent(server); - await request - .post("/micropub") - .auth(testToken(), { type: "bearer" }) - .set("accept", "application/json") - .set("cookie", [cookie()]) - .send("h=entry") - .send("name=foobar") - .send("mp-syndicate-to=https://twitter.com/username"); - const result = await request - .post("/syndicate") - .set("accept", "application/json") - .query({ url: "https://website.example/notes/foobar/" }) - .query({ token: "foo.bar.baz" }); - - t.is(result.status, 401); - t.is( - result.body.error_description, - "The access token provided is expired, revoked, malformed, or invalid for other reasons" - ); - - server.close(t); -}); diff --git a/packages/endpoint-syndicate/tests/unit/token.js b/packages/endpoint-syndicate/tests/unit/token.js new file mode 100644 index 000000000..43d142f34 --- /dev/null +++ b/packages/endpoint-syndicate/tests/unit/token.js @@ -0,0 +1,70 @@ +import { createHash } from "node:crypto"; +import process from "node:process"; +import test from "ava"; +import jwt from "jsonwebtoken"; +import { findBearerToken, signToken, verifyToken } from "../../lib/token.js"; + +const sha256 = createHash("sha256").update("foo").digest("hex"); +const jwtHeader = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + +test.beforeEach((t) => { + process.env.SECRET = "secret"; + process.env.WEBHOOK_SECRET = "webhook-secret"; + t.context = { + bearerToken: jwt.sign( + { me: "https://website.example" }, + process.env.SECRET + ), + signature: jwt.sign({ iss: "netlify", sha256 }, process.env.WEBHOOK_SECRET), + }; +}); + +test("Returns bearer token from `X-Webhook-Signature` header", (t) => { + const request = { + body: { url: "https://website.example" }, + headers: { "x-webhook-signature": t.context.signature }, + }; + const result = findBearerToken(request); + + t.true(result.startsWith(jwtHeader)); +}); + +test("Returns bearer token from `body.access_token`", (t) => { + const request = { body: { access_token: t.context.bearerToken } }; + const result = findBearerToken(request); + + t.true(result.startsWith(jwtHeader)); +}); + +test("Returns bearer token from query", (t) => { + const request = { query: { token: t.context.bearerToken } }; + const result = findBearerToken(request); + + t.true(result.startsWith(jwtHeader)); +}); + +test("Throws error if no bearer token provided by request", (t) => { + t.throws( + () => { + findBearerToken({}); + }, + { + name: "InvalidRequestError", + message: "No bearer token provided by request", + } + ); +}); + +test("Signs token", (t) => { + const result = signToken({ foo: "bar" }); + + t.true(result.startsWith(jwtHeader)); +}); + +test("Verifies signed token", (t) => { + const result = verifyToken(t.context.signature); + + t.truthy(result.iat); + t.is(result.iss, "netlify"); + t.is(result.sha256, sha256); +});