From 07cb65164ff3188297c71f7fd37660ebcd664b4b Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 30 Jun 2026 14:01:36 -0400 Subject: [PATCH 1/5] Document k6 request signing --- docs/cloud/request-signing.md | 121 ++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 43 deletions(-) diff --git a/docs/cloud/request-signing.md b/docs/cloud/request-signing.md index 18253f355..0ad1c5420 100644 --- a/docs/cloud/request-signing.md +++ b/docs/cloud/request-signing.md @@ -32,66 +32,55 @@ npm install http-message-sig Build and send a signed request like this: ```js -import crypto from 'node:crypto'; -import { signatureHeadersSync } from 'http-message-sig'; +import crypto from "node:crypto"; +import { signatureHeadersSync } from "http-message-sig"; -const method = 'POST'; - -// These variables/secrets can be store in (and fetched from) the environment, instead: -const url = 'https://my-env.some-domain.com/api'; -const schemaToken = 'WcVqivS64CCRQN9ohVcKk5FB6RIFTApd'; +const method = "POST"; +const url = "https://my-env.some-domain.com/api"; const body = JSON.stringify({ - query: ` - { - entries(section: "blog") { - title - url - } - } - `, + query: `{ entries(section: "blog") { title url } }`, }); const headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${schemaToken}`, + "Content-Type": "application/json", + Authorization: "Bearer my-secret-gql-schema-token", }; const created = new Date(); const signer = { - keyid: 'hmac', - alg: 'hmac-sha256', - signSync(data) { - return crypto - .createHmac('sha256', process.env.CRAFT_CLOUD_SIGNING_KEY) - .update(data) - .digest(); - }, + keyid: "hmac", + alg: "hmac-sha256", + signSync(data) { + return crypto + .createHmac("sha256", process.env.CRAFT_CLOUD_SIGNING_KEY) + .update(data) + .digest(); + }, }; const signatureHeaders = signatureHeadersSync( - { method, url, headers, body }, - { - key: 'sig', - signer, - components: ['@method', '@target-uri'], - created, - // This is optional (and cannot exceed five minutes, to validate at the edge): - expires: new Date(created.getTime() + 60_000), - }, + { method, url, headers, body }, + { + key: "sig", + signer, + components: ["@method", "@target-uri"], + created, + + // Optional 60-second expiry. The maximum is five minutes. + expires: new Date(created.getTime() + 60 * 1000), + } ); -const response = await fetch(url, { - method, - headers: { - ...headers, - ...signatureHeaders, - }, - body, +await fetch(url, { + method, + headers: { + ...headers, + ...signatureHeaders, + }, + body, }); - -const result = await response.json(); ``` ::: tip @@ -99,6 +88,52 @@ Requests signed using the `@target-uri` [component](https://www.rfc-editor.org/r The example above satisfies this by using the same `url` variable for the signed request and the `fetch()` call. ::: +### From Grafana Cloud k6 + +This example uses [Grafana Cloud k6](https://grafana.com/docs/k6/latest/examples/) with native dependencies: + +```js +import crypto from "k6/crypto"; +import http from "k6/http"; + +const method = "POST"; +const url = "https://my-env.some-domain.com/api"; + +const body = JSON.stringify({ + query: `{ entries(section: "blog") { title url } }`, +}); + +export default function () { + const created = Math.floor(Date.now() / 1000); + const expires = created + 60; + + const signatureParams = + `("@method" "@target-uri");created=${created};expires=${expires};keyid="hmac";alg="hmac-sha256"`; + + const signatureBase = [ + `"@method": ${method}`, + `"@target-uri": ${url}`, + `"@signature-params": ${signatureParams}`, + ].join("\n"); + + const signature = crypto.hmac( + "sha256", + __ENV.CRAFT_CLOUD_SIGNING_KEY, + signatureBase, + "base64" + ); + + http.post(url, body, { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer my-secret-gql-schema-token", + "Signature-Input": `sig=${signatureParams}`, + Signature: `sig=:${signature}:`, + }, + }); +} +``` + ### From Craft Any Craft project running on Cloud can sign requests. From 2320818e259aa4eb345d1ee8853cad2c365bb0ea Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 30 Jun 2026 14:05:40 -0400 Subject: [PATCH 2/5] Match JS docs style --- docs/cloud/request-signing.md | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/cloud/request-signing.md b/docs/cloud/request-signing.md index 0ad1c5420..6ec4f34a0 100644 --- a/docs/cloud/request-signing.md +++ b/docs/cloud/request-signing.md @@ -32,29 +32,29 @@ npm install http-message-sig Build and send a signed request like this: ```js -import crypto from "node:crypto"; -import { signatureHeadersSync } from "http-message-sig"; +import crypto from 'node:crypto'; +import { signatureHeadersSync } from 'http-message-sig'; -const method = "POST"; -const url = "https://my-env.some-domain.com/api"; +const method = 'POST'; +const url = 'https://my-env.some-domain.com/api'; const body = JSON.stringify({ query: `{ entries(section: "blog") { title url } }`, }); const headers = { - "Content-Type": "application/json", - Authorization: "Bearer my-secret-gql-schema-token", + 'Content-Type': 'application/json', + 'Authorization': 'Bearer my-secret-gql-schema-token', }; const created = new Date(); const signer = { - keyid: "hmac", - alg: "hmac-sha256", + keyid: 'hmac', + alg: 'hmac-sha256', signSync(data) { return crypto - .createHmac("sha256", process.env.CRAFT_CLOUD_SIGNING_KEY) + .createHmac('sha256', process.env.CRAFT_CLOUD_SIGNING_KEY) .update(data) .digest(); }, @@ -63,9 +63,9 @@ const signer = { const signatureHeaders = signatureHeadersSync( { method, url, headers, body }, { - key: "sig", + key: 'sig', signer, - components: ["@method", "@target-uri"], + components: ['@method', '@target-uri'], created, // Optional 60-second expiry. The maximum is five minutes. @@ -93,11 +93,11 @@ The example above satisfies this by using the same `url` variable for the signed This example uses [Grafana Cloud k6](https://grafana.com/docs/k6/latest/examples/) with native dependencies: ```js -import crypto from "k6/crypto"; -import http from "k6/http"; +import crypto from 'k6/crypto'; +import http from 'k6/http'; -const method = "POST"; -const url = "https://my-env.some-domain.com/api"; +const method = 'POST'; +const url = 'https://my-env.some-domain.com/api'; const body = JSON.stringify({ query: `{ entries(section: "blog") { title url } }`, @@ -114,21 +114,21 @@ export default function () { `"@method": ${method}`, `"@target-uri": ${url}`, `"@signature-params": ${signatureParams}`, - ].join("\n"); + ].join('\n'); const signature = crypto.hmac( - "sha256", + 'sha256', __ENV.CRAFT_CLOUD_SIGNING_KEY, signatureBase, - "base64" + 'base64' ); http.post(url, body, { headers: { - "Content-Type": "application/json", - Authorization: "Bearer my-secret-gql-schema-token", - "Signature-Input": `sig=${signatureParams}`, - Signature: `sig=:${signature}:`, + 'Content-Type': 'application/json', + 'Authorization': 'Bearer my-secret-gql-schema-token', + 'Signature-Input': `sig=${signatureParams}`, + 'Signature': `sig=:${signature}:`, }, }); } From 1a1bd9c0e957a08f18a9b4e9de417bdc22a6bdcd Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 30 Jun 2026 14:07:52 -0400 Subject: [PATCH 3/5] Clarify k6 signature parameters --- docs/cloud/request-signing.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/cloud/request-signing.md b/docs/cloud/request-signing.md index 6ec4f34a0..2a58e6d5f 100644 --- a/docs/cloud/request-signing.md +++ b/docs/cloud/request-signing.md @@ -107,8 +107,13 @@ export default function () { const created = Math.floor(Date.now() / 1000); const expires = created + 60; - const signatureParams = - `("@method" "@target-uri");created=${created};expires=${expires};keyid="hmac";alg="hmac-sha256"`; + const signatureParams = [ + '("@method" "@target-uri")', + `created=${created}`, + `expires=${expires}`, + 'keyid="hmac"', + 'alg="hmac-sha256"', + ].join(';'); const signatureBase = [ `"@method": ${method}`, From 9d4eca3c3c0c702a125a4a8f01a5a6a271200fc4 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 30 Jun 2026 14:15:43 -0400 Subject: [PATCH 4/5] Clarify k6 signature base --- docs/cloud/request-signing.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/cloud/request-signing.md b/docs/cloud/request-signing.md index 2a58e6d5f..fdec79c35 100644 --- a/docs/cloud/request-signing.md +++ b/docs/cloud/request-signing.md @@ -116,10 +116,12 @@ export default function () { ].join(';'); const signatureBase = [ - `"@method": ${method}`, - `"@target-uri": ${url}`, - `"@signature-params": ${signatureParams}`, - ].join('\n'); + ['@method', method], + ['@target-uri', url], + ['@signature-params', signatureParams], + ] + .map(([component, value]) => `"${component}": ${value}`) + .join('\n'); const signature = crypto.hmac( 'sha256', From 76b78abf2a9776897cfb1e82cdf0adeabd5ff2f5 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 30 Jun 2026 14:30:18 -0400 Subject: [PATCH 5/5] Simplify request signing example --- docs/cloud/request-signing.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/cloud/request-signing.md b/docs/cloud/request-signing.md index fdec79c35..81199ba6e 100644 --- a/docs/cloud/request-signing.md +++ b/docs/cloud/request-signing.md @@ -42,11 +42,6 @@ const body = JSON.stringify({ query: `{ entries(section: "blog") { title url } }`, }); -const headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer my-secret-gql-schema-token', -}; - const created = new Date(); const signer = { @@ -61,7 +56,7 @@ const signer = { }; const signatureHeaders = signatureHeadersSync( - { method, url, headers, body }, + { method, url }, { key: 'sig', signer, @@ -76,7 +71,8 @@ const signatureHeaders = signatureHeadersSync( await fetch(url, { method, headers: { - ...headers, + 'Content-Type': 'application/json', + 'Authorization': 'Bearer my-secret-gql-schema-token', ...signatureHeaders, }, body,