@vritti/api-sdk 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.cjs +193 -78
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +84 -2
- package/dist/auth.d.ts +84 -2
- package/dist/auth.js +114 -6
- package/dist/auth.js.map +1 -1
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/request-CDllp7xb.d.cts +111 -0
- package/dist/request-CDllp7xb.d.ts +111 -0
- package/dist/signing.cjs +18 -2
- package/dist/signing.cjs.map +1 -1
- package/dist/signing.d.cts +2 -31
- package/dist/signing.d.ts +2 -31
- package/dist/signing.js +18 -2
- package/dist/signing.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace-scope headers, in the order the canonical appends them.
|
|
3
|
+
*
|
|
4
|
+
* Exactly one is expected — the kind of scope *is* which header is sent, the same
|
|
5
|
+
* convention core-web and core-app already use. Signing the header **name** rather
|
|
6
|
+
* than a separate `kind` field means a verifier needs no scope vocabulary: it scans
|
|
7
|
+
* this same list in this same order and rebuilds an identical string.
|
|
8
|
+
*
|
|
9
|
+
* Fixed and shared so a signer and a verifier can never disagree about it, and so the
|
|
10
|
+
* discouraged multi-header case is deterministic rather than ambiguous.
|
|
11
|
+
*/
|
|
12
|
+
declare const WORKSPACE_HEADER_ORDER: readonly ["x-site-id", "x-sg-id", "x-le-id", "x-org-id"];
|
|
13
|
+
interface RequestCanonicalInput {
|
|
14
|
+
method: string;
|
|
15
|
+
path: string;
|
|
16
|
+
orgId?: string;
|
|
17
|
+
body?: string | Buffer;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
/**
|
|
20
|
+
* The raw query string, without the leading `?`.
|
|
21
|
+
*
|
|
22
|
+
* Signed byte for byte, with no sorting or re-encoding, so the signer and the
|
|
23
|
+
* verifier cannot disagree about normalization. The consequence is that a proxy
|
|
24
|
+
* which reorders or re-encodes parameters invalidates the signature.
|
|
25
|
+
*/
|
|
26
|
+
query?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The party (person) the request acts for, when there is one.
|
|
29
|
+
*
|
|
30
|
+
* Signed so a caller cannot be swapped for another shopper in transit.
|
|
31
|
+
*/
|
|
32
|
+
partyId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The workspace-scope headers actually present on the request, by header name.
|
|
35
|
+
*
|
|
36
|
+
* Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in
|
|
37
|
+
* that order — so signer and verifier agree without either needing to know what a
|
|
38
|
+
* "site" or a "legal entity" is.
|
|
39
|
+
*/
|
|
40
|
+
workspaceHeaders?: Record<string, string | undefined>;
|
|
41
|
+
}
|
|
42
|
+
interface SignRequestInput {
|
|
43
|
+
method: string;
|
|
44
|
+
path: string;
|
|
45
|
+
orgId?: string;
|
|
46
|
+
body?: string | Buffer;
|
|
47
|
+
/** Raw query string, without the leading `?`. See `RequestCanonicalInput`. */
|
|
48
|
+
query?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The party (person) the request acts for, when there is one.
|
|
51
|
+
*
|
|
52
|
+
* Signed so a caller cannot be swapped for another shopper in transit.
|
|
53
|
+
*/
|
|
54
|
+
partyId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The workspace-scope headers actually present on the request, by header name.
|
|
57
|
+
*
|
|
58
|
+
* Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in
|
|
59
|
+
* that order — so signer and verifier agree without either needing to know what a
|
|
60
|
+
* "site" or a "legal entity" is.
|
|
61
|
+
*/
|
|
62
|
+
workspaceHeaders?: Record<string, string | undefined>;
|
|
63
|
+
}
|
|
64
|
+
interface VerifySignedRequestInput {
|
|
65
|
+
method: string;
|
|
66
|
+
path: string;
|
|
67
|
+
orgId?: string;
|
|
68
|
+
/** Raw query string, without the leading `?`. See `RequestCanonicalInput`. */
|
|
69
|
+
query?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The party (person) the request acts for, when there is one.
|
|
72
|
+
*
|
|
73
|
+
* Signed so a caller cannot be swapped for another shopper in transit.
|
|
74
|
+
*/
|
|
75
|
+
partyId?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The workspace-scope headers actually present on the request, by header name.
|
|
78
|
+
*
|
|
79
|
+
* Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in
|
|
80
|
+
* that order — so signer and verifier agree without either needing to know what a
|
|
81
|
+
* "site" or a "legal entity" is.
|
|
82
|
+
*/
|
|
83
|
+
workspaceHeaders?: Record<string, string | undefined>;
|
|
84
|
+
rawBody?: string | Buffer;
|
|
85
|
+
timestamp: string | number;
|
|
86
|
+
signature: string;
|
|
87
|
+
publicKey: string;
|
|
88
|
+
maxSkewSeconds?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Builds the canonical string a request's signature is made over:
|
|
92
|
+
* `METHOD\npath\norgId\nsha256hex(body)\ntimestamp`, then a labelled line per
|
|
93
|
+
* optional field that is present.
|
|
94
|
+
*
|
|
95
|
+
* Optional fields are appended **only when set**, so a caller that supplies none
|
|
96
|
+
* produces exactly the string this function has always produced — which is what
|
|
97
|
+
* lets new fields be added without invalidating existing signers. They are
|
|
98
|
+
* **labelled** so that "query present, party absent" can never be confused with
|
|
99
|
+
* "party present, query absent".
|
|
100
|
+
*
|
|
101
|
+
* Absence is still tamper-evident: strip a signed field in transit and the
|
|
102
|
+
* verifier rebuilds the canonical without it, which no longer matches.
|
|
103
|
+
*/
|
|
104
|
+
declare function buildRequestCanonical(input: RequestCanonicalInput): string;
|
|
105
|
+
declare function signRequestHeaders(input: SignRequestInput, privateKeyBase64: string): {
|
|
106
|
+
'x-timestamp': string;
|
|
107
|
+
'x-signature': string;
|
|
108
|
+
};
|
|
109
|
+
declare function verifySignedRequest(input: VerifySignedRequestInput): boolean;
|
|
110
|
+
|
|
111
|
+
export { type RequestCanonicalInput as R, type SignRequestInput as S, type VerifySignedRequestInput as V, WORKSPACE_HEADER_ORDER as W, buildRequestCanonical as b, signRequestHeaders as s, verifySignedRequest as v };
|
package/dist/signing.cjs
CHANGED
|
@@ -93,15 +93,28 @@ __name(verifyDocument, "verifyDocument");
|
|
|
93
93
|
|
|
94
94
|
// src/signing/request.ts
|
|
95
95
|
var import_node_crypto2 = require("crypto");
|
|
96
|
+
var WORKSPACE_HEADER_ORDER = [
|
|
97
|
+
"x-site-id",
|
|
98
|
+
"x-sg-id",
|
|
99
|
+
"x-le-id",
|
|
100
|
+
"x-org-id"
|
|
101
|
+
];
|
|
96
102
|
function buildRequestCanonical(input) {
|
|
97
103
|
const bodyHash = (0, import_node_crypto2.createHash)("sha256").update(input.body ?? "").digest("hex");
|
|
98
|
-
|
|
104
|
+
const lines = [
|
|
99
105
|
input.method.toUpperCase(),
|
|
100
106
|
input.path,
|
|
101
107
|
input.orgId ?? "",
|
|
102
108
|
bodyHash,
|
|
103
109
|
String(input.timestamp)
|
|
104
|
-
]
|
|
110
|
+
];
|
|
111
|
+
if (input.query) lines.push(`query:${input.query}`);
|
|
112
|
+
if (input.partyId) lines.push(`party:${input.partyId}`);
|
|
113
|
+
for (const header of WORKSPACE_HEADER_ORDER) {
|
|
114
|
+
const value = input.workspaceHeaders?.[header];
|
|
115
|
+
if (value) lines.push(`${header}:${value}`);
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
105
118
|
}
|
|
106
119
|
__name(buildRequestCanonical, "buildRequestCanonical");
|
|
107
120
|
function signRequestHeaders(input, privateKeyBase64) {
|
|
@@ -132,6 +145,9 @@ function verifySignedRequest(input) {
|
|
|
132
145
|
method: input.method,
|
|
133
146
|
path: input.path,
|
|
134
147
|
orgId: input.orgId,
|
|
148
|
+
query: input.query,
|
|
149
|
+
partyId: input.partyId,
|
|
150
|
+
workspaceHeaders: input.workspaceHeaders,
|
|
135
151
|
body: input.rawBody,
|
|
136
152
|
timestamp
|
|
137
153
|
});
|
package/dist/signing.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/signing/index.ts","../src/signing/canonical.ts","../src/signing/document.ts","../src/signing/request.ts"],"sourcesContent":["// Signing module — Ed25519 primitives: keypairs, canonical-JSON document signing, and signed control-plane requests\nexport { canonicalStringify } from './canonical';\nexport { generateSigningKeyPair, type SignedDocument, signDocument, verifyDocument } from './document';\nexport {\n buildRequestCanonical,\n type RequestCanonicalInput,\n type SignRequestInput,\n signRequestHeaders,\n type VerifySignedRequestInput,\n verifySignedRequest,\n} from './request';\n","// Serializes a value as deterministic JSON: object keys recursively sorted, arrays keep their order\nexport function canonicalStringify(value: unknown): string {\n return JSON.stringify(sortKeysDeep(value));\n}\n\n// Recursively rebuilds objects with sorted keys so JSON.stringify output is order-independent\nfunction sortKeysDeep(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeysDeep);\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(record).sort()) sorted[key] = sortKeysDeep(record[key]);\n return sorted;\n }\n return value;\n}\n","import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';\nimport { canonicalStringify } from './canonical';\n\nexport interface SignedDocument<T> {\n payload: T;\n signature: string;\n}\n\n// Generates an Ed25519 key pair as base64 DER strings (pkcs8 private / spki public)\nexport function generateSigningKeyPair(): { privateKey: string; publicKey: string } {\n const { privateKey, publicKey } = generateKeyPairSync('ed25519');\n return {\n privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),\n publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),\n };\n}\n\n// Signs a payload's canonical JSON with an Ed25519 private key (base64 pkcs8 DER)\nexport function signDocument<T>(payload: T, privateKeyBase64: string): SignedDocument<T> {\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonicalStringify(payload), 'utf8'), key).toString('base64');\n return { payload, signature };\n}\n\n// Verifies a signed document against an Ed25519 public key (base64 spki DER); malformed input ⇒ false\nexport function verifyDocument<T>(doc: SignedDocument<T>, publicKeyBase64: string): boolean {\n try {\n const key = createPublicKey({ key: Buffer.from(publicKeyBase64, 'base64'), format: 'der', type: 'spki' });\n return verify(\n null,\n Buffer.from(canonicalStringify(doc.payload), 'utf8'),\n key,\n Buffer.from(doc.signature, 'base64'),\n );\n } catch {\n return false;\n }\n}\n","import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';\n\nexport interface RequestCanonicalInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n timestamp: number;\n}\n\nexport interface SignRequestInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n}\n\nexport interface VerifySignedRequestInput {\n method: string;\n path: string;\n orgId?: string;\n rawBody?: string | Buffer;\n timestamp: string | number;\n signature: string;\n publicKey: string;\n maxSkewSeconds?: number;\n}\n\n// Builds the canonical string signed for a control-plane request: METHOD\\npath\\norgId\\nsha256hex(body)\\ntimestamp\nexport function buildRequestCanonical(input: RequestCanonicalInput): string {\n const bodyHash = createHash('sha256')\n .update(input.body ?? '')\n .digest('hex');\n return [input.method.toUpperCase(), input.path, input.orgId ?? '', bodyHash, String(input.timestamp)].join('\\n');\n}\n\n// Signs a request with an Ed25519 private key (base64 pkcs8 DER), stamping the current unix time\nexport function signRequestHeaders(\n input: SignRequestInput,\n privateKeyBase64: string,\n): { 'x-timestamp': string; 'x-signature': string } {\n const timestamp = Math.floor(Date.now() / 1000);\n const canonical = buildRequestCanonical({ ...input, timestamp });\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonical, 'utf8'), key).toString('base64');\n return { 'x-timestamp': String(timestamp), 'x-signature': signature };\n}\n\n// Verifies a signed request (signature + timestamp skew) against an Ed25519 public key; malformed input ⇒ false\nexport function verifySignedRequest(input: VerifySignedRequestInput): boolean {\n try {\n const timestamp = Number(input.timestamp);\n if (!Number.isFinite(timestamp)) return false;\n const maxSkew = input.maxSkewSeconds ?? 300;\n if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > maxSkew) return false;\n const canonical = buildRequestCanonical({\n method: input.method,\n path: input.path,\n orgId: input.orgId,\n body: input.rawBody,\n timestamp,\n });\n const key = createPublicKey({ key: Buffer.from(input.publicKey, 'base64'), format: 'der', type: 'spki' });\n return verify(null, Buffer.from(canonical, 'utf8'), key, Buffer.from(input.signature, 'base64'));\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["canonicalStringify","value","JSON","stringify","sortKeysDeep","Array","isArray","map","record","sorted","key","Object","keys","sort","generateSigningKeyPair","privateKey","publicKey","generateKeyPairSync","export","type","format","toString","signDocument","payload","privateKeyBase64","key","createPrivateKey","Buffer","from","signature","sign","canonicalStringify","verifyDocument","doc","publicKeyBase64","createPublicKey","verify","import_node_crypto","buildRequestCanonical","input","bodyHash","createHash","update","body","digest","method","toUpperCase","path","orgId","String","timestamp","join","signRequestHeaders","privateKeyBase64","Math","floor","Date","now","canonical","key","createPrivateKey","Buffer","from","format","type","signature","sign","toString","verifySignedRequest","Number","isFinite","maxSkew","maxSkewSeconds","abs","rawBody","createPublicKey","publicKey","verify"]}
|
|
1
|
+
{"version":3,"sources":["../src/signing/index.ts","../src/signing/canonical.ts","../src/signing/document.ts","../src/signing/request.ts"],"sourcesContent":["// Signing module — Ed25519 primitives: keypairs, canonical-JSON document signing, and signed control-plane requests\nexport { canonicalStringify } from './canonical';\nexport { generateSigningKeyPair, type SignedDocument, signDocument, verifyDocument } from './document';\nexport {\n buildRequestCanonical,\n type RequestCanonicalInput,\n type SignRequestInput,\n signRequestHeaders,\n type VerifySignedRequestInput,\n verifySignedRequest,\n} from './request';\n","// Serializes a value as deterministic JSON: object keys recursively sorted, arrays keep their order\nexport function canonicalStringify(value: unknown): string {\n return JSON.stringify(sortKeysDeep(value));\n}\n\n// Recursively rebuilds objects with sorted keys so JSON.stringify output is order-independent\nfunction sortKeysDeep(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeysDeep);\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(record).sort()) sorted[key] = sortKeysDeep(record[key]);\n return sorted;\n }\n return value;\n}\n","import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';\nimport { canonicalStringify } from './canonical';\n\nexport interface SignedDocument<T> {\n payload: T;\n signature: string;\n}\n\n// Generates an Ed25519 key pair as base64 DER strings (pkcs8 private / spki public)\nexport function generateSigningKeyPair(): { privateKey: string; publicKey: string } {\n const { privateKey, publicKey } = generateKeyPairSync('ed25519');\n return {\n privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),\n publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),\n };\n}\n\n// Signs a payload's canonical JSON with an Ed25519 private key (base64 pkcs8 DER)\nexport function signDocument<T>(payload: T, privateKeyBase64: string): SignedDocument<T> {\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonicalStringify(payload), 'utf8'), key).toString('base64');\n return { payload, signature };\n}\n\n// Verifies a signed document against an Ed25519 public key (base64 spki DER); malformed input ⇒ false\nexport function verifyDocument<T>(doc: SignedDocument<T>, publicKeyBase64: string): boolean {\n try {\n const key = createPublicKey({ key: Buffer.from(publicKeyBase64, 'base64'), format: 'der', type: 'spki' });\n return verify(\n null,\n Buffer.from(canonicalStringify(doc.payload), 'utf8'),\n key,\n Buffer.from(doc.signature, 'base64'),\n );\n } catch {\n return false;\n }\n}\n","import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';\n\n/**\n * The workspace-scope headers, in the order the canonical appends them.\n *\n * Exactly one is expected — the kind of scope *is* which header is sent, the same\n * convention core-web and core-app already use. Signing the header **name** rather\n * than a separate `kind` field means a verifier needs no scope vocabulary: it scans\n * this same list in this same order and rebuilds an identical string.\n *\n * Fixed and shared so a signer and a verifier can never disagree about it, and so the\n * discouraged multi-header case is deterministic rather than ambiguous.\n */\nexport const WORKSPACE_HEADER_ORDER = ['x-site-id', 'x-sg-id', 'x-le-id', 'x-org-id'] as const;\n\nexport interface RequestCanonicalInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n timestamp: number;\n /**\n * The raw query string, without the leading `?`.\n *\n * Signed byte for byte, with no sorting or re-encoding, so the signer and the\n * verifier cannot disagree about normalization. The consequence is that a proxy\n * which reorders or re-encodes parameters invalidates the signature.\n */\n query?: string;\n /**\n * The party (person) the request acts for, when there is one.\n *\n * Signed so a caller cannot be swapped for another shopper in transit.\n */\n partyId?: string;\n /**\n * The workspace-scope headers actually present on the request, by header name.\n *\n * Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in\n * that order — so signer and verifier agree without either needing to know what a\n * \"site\" or a \"legal entity\" is.\n */\n workspaceHeaders?: Record<string, string | undefined>;\n}\n\nexport interface SignRequestInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n /** Raw query string, without the leading `?`. See `RequestCanonicalInput`. */\n query?: string;\n /**\n * The party (person) the request acts for, when there is one.\n *\n * Signed so a caller cannot be swapped for another shopper in transit.\n */\n partyId?: string;\n /**\n * The workspace-scope headers actually present on the request, by header name.\n *\n * Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in\n * that order — so signer and verifier agree without either needing to know what a\n * \"site\" or a \"legal entity\" is.\n */\n workspaceHeaders?: Record<string, string | undefined>;\n}\n\nexport interface VerifySignedRequestInput {\n method: string;\n path: string;\n orgId?: string;\n /** Raw query string, without the leading `?`. See `RequestCanonicalInput`. */\n query?: string;\n /**\n * The party (person) the request acts for, when there is one.\n *\n * Signed so a caller cannot be swapped for another shopper in transit.\n */\n partyId?: string;\n /**\n * The workspace-scope headers actually present on the request, by header name.\n *\n * Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in\n * that order — so signer and verifier agree without either needing to know what a\n * \"site\" or a \"legal entity\" is.\n */\n workspaceHeaders?: Record<string, string | undefined>;\n rawBody?: string | Buffer;\n timestamp: string | number;\n signature: string;\n publicKey: string;\n maxSkewSeconds?: number;\n}\n\n/**\n * Builds the canonical string a request's signature is made over:\n * `METHOD\\npath\\norgId\\nsha256hex(body)\\ntimestamp`, then a labelled line per\n * optional field that is present.\n *\n * Optional fields are appended **only when set**, so a caller that supplies none\n * produces exactly the string this function has always produced — which is what\n * lets new fields be added without invalidating existing signers. They are\n * **labelled** so that \"query present, party absent\" can never be confused with\n * \"party present, query absent\".\n *\n * Absence is still tamper-evident: strip a signed field in transit and the\n * verifier rebuilds the canonical without it, which no longer matches.\n */\nexport function buildRequestCanonical(input: RequestCanonicalInput): string {\n const bodyHash = createHash('sha256')\n .update(input.body ?? '')\n .digest('hex');\n\n const lines = [input.method.toUpperCase(), input.path, input.orgId ?? '', bodyHash, String(input.timestamp)];\n if (input.query) lines.push(`query:${input.query}`);\n if (input.partyId) lines.push(`party:${input.partyId}`);\n\n // Header name and value both go in, so swapping x-le-id for x-site-id on the same\n // value breaks the signature. Fixed order, so multiple headers stay deterministic.\n for (const header of WORKSPACE_HEADER_ORDER) {\n const value = input.workspaceHeaders?.[header];\n if (value) lines.push(`${header}:${value}`);\n }\n\n return lines.join('\\n');\n}\n\n// Signs a request with an Ed25519 private key (base64 pkcs8 DER), stamping the current unix time\nexport function signRequestHeaders(\n input: SignRequestInput,\n privateKeyBase64: string,\n): { 'x-timestamp': string; 'x-signature': string } {\n const timestamp = Math.floor(Date.now() / 1000);\n const canonical = buildRequestCanonical({ ...input, timestamp });\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonical, 'utf8'), key).toString('base64');\n return { 'x-timestamp': String(timestamp), 'x-signature': signature };\n}\n\n// Verifies a signed request (signature + timestamp skew) against an Ed25519 public key; malformed input ⇒ false\nexport function verifySignedRequest(input: VerifySignedRequestInput): boolean {\n try {\n const timestamp = Number(input.timestamp);\n if (!Number.isFinite(timestamp)) return false;\n const maxSkew = input.maxSkewSeconds ?? 300;\n if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > maxSkew) return false;\n const canonical = buildRequestCanonical({\n method: input.method,\n path: input.path,\n orgId: input.orgId,\n query: input.query,\n partyId: input.partyId,\n workspaceHeaders: input.workspaceHeaders,\n body: input.rawBody,\n timestamp,\n });\n const key = createPublicKey({ key: Buffer.from(input.publicKey, 'base64'), format: 'der', type: 'spki' });\n return verify(null, Buffer.from(canonical, 'utf8'), key, Buffer.from(input.signature, 'base64'));\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["canonicalStringify","value","JSON","stringify","sortKeysDeep","Array","isArray","map","record","sorted","key","Object","keys","sort","generateSigningKeyPair","privateKey","publicKey","generateKeyPairSync","export","type","format","toString","signDocument","payload","privateKeyBase64","key","createPrivateKey","Buffer","from","signature","sign","canonicalStringify","verifyDocument","doc","publicKeyBase64","createPublicKey","verify","import_node_crypto","WORKSPACE_HEADER_ORDER","buildRequestCanonical","input","bodyHash","createHash","update","body","digest","lines","method","toUpperCase","path","orgId","String","timestamp","query","push","partyId","header","value","workspaceHeaders","join","signRequestHeaders","privateKeyBase64","Math","floor","Date","now","canonical","key","createPrivateKey","Buffer","from","format","type","signature","sign","toString","verifySignedRequest","Number","isFinite","maxSkew","maxSkewSeconds","abs","rawBody","createPublicKey","publicKey","verify"]}
|
package/dist/signing.d.cts
CHANGED
|
@@ -1,35 +1,6 @@
|
|
|
1
1
|
export { S as SignedDocument, g as generateSigningKeyPair, s as signDocument, v as verifyDocument } from './document-BoS0NIbf.cjs';
|
|
2
|
+
export { R as RequestCanonicalInput, S as SignRequestInput, V as VerifySignedRequestInput, b as buildRequestCanonical, s as signRequestHeaders, v as verifySignedRequest } from './request-CDllp7xb.cjs';
|
|
2
3
|
|
|
3
4
|
declare function canonicalStringify(value: unknown): string;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
method: string;
|
|
7
|
-
path: string;
|
|
8
|
-
orgId?: string;
|
|
9
|
-
body?: string | Buffer;
|
|
10
|
-
timestamp: number;
|
|
11
|
-
}
|
|
12
|
-
interface SignRequestInput {
|
|
13
|
-
method: string;
|
|
14
|
-
path: string;
|
|
15
|
-
orgId?: string;
|
|
16
|
-
body?: string | Buffer;
|
|
17
|
-
}
|
|
18
|
-
interface VerifySignedRequestInput {
|
|
19
|
-
method: string;
|
|
20
|
-
path: string;
|
|
21
|
-
orgId?: string;
|
|
22
|
-
rawBody?: string | Buffer;
|
|
23
|
-
timestamp: string | number;
|
|
24
|
-
signature: string;
|
|
25
|
-
publicKey: string;
|
|
26
|
-
maxSkewSeconds?: number;
|
|
27
|
-
}
|
|
28
|
-
declare function buildRequestCanonical(input: RequestCanonicalInput): string;
|
|
29
|
-
declare function signRequestHeaders(input: SignRequestInput, privateKeyBase64: string): {
|
|
30
|
-
'x-timestamp': string;
|
|
31
|
-
'x-signature': string;
|
|
32
|
-
};
|
|
33
|
-
declare function verifySignedRequest(input: VerifySignedRequestInput): boolean;
|
|
34
|
-
|
|
35
|
-
export { type RequestCanonicalInput, type SignRequestInput, type VerifySignedRequestInput, buildRequestCanonical, canonicalStringify, signRequestHeaders, verifySignedRequest };
|
|
6
|
+
export { canonicalStringify };
|
package/dist/signing.d.ts
CHANGED
|
@@ -1,35 +1,6 @@
|
|
|
1
1
|
export { S as SignedDocument, g as generateSigningKeyPair, s as signDocument, v as verifyDocument } from './document-BoS0NIbf.js';
|
|
2
|
+
export { R as RequestCanonicalInput, S as SignRequestInput, V as VerifySignedRequestInput, b as buildRequestCanonical, s as signRequestHeaders, v as verifySignedRequest } from './request-CDllp7xb.js';
|
|
2
3
|
|
|
3
4
|
declare function canonicalStringify(value: unknown): string;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
method: string;
|
|
7
|
-
path: string;
|
|
8
|
-
orgId?: string;
|
|
9
|
-
body?: string | Buffer;
|
|
10
|
-
timestamp: number;
|
|
11
|
-
}
|
|
12
|
-
interface SignRequestInput {
|
|
13
|
-
method: string;
|
|
14
|
-
path: string;
|
|
15
|
-
orgId?: string;
|
|
16
|
-
body?: string | Buffer;
|
|
17
|
-
}
|
|
18
|
-
interface VerifySignedRequestInput {
|
|
19
|
-
method: string;
|
|
20
|
-
path: string;
|
|
21
|
-
orgId?: string;
|
|
22
|
-
rawBody?: string | Buffer;
|
|
23
|
-
timestamp: string | number;
|
|
24
|
-
signature: string;
|
|
25
|
-
publicKey: string;
|
|
26
|
-
maxSkewSeconds?: number;
|
|
27
|
-
}
|
|
28
|
-
declare function buildRequestCanonical(input: RequestCanonicalInput): string;
|
|
29
|
-
declare function signRequestHeaders(input: SignRequestInput, privateKeyBase64: string): {
|
|
30
|
-
'x-timestamp': string;
|
|
31
|
-
'x-signature': string;
|
|
32
|
-
};
|
|
33
|
-
declare function verifySignedRequest(input: VerifySignedRequestInput): boolean;
|
|
34
|
-
|
|
35
|
-
export { type RequestCanonicalInput, type SignRequestInput, type VerifySignedRequestInput, buildRequestCanonical, canonicalStringify, signRequestHeaders, verifySignedRequest };
|
|
6
|
+
export { canonicalStringify };
|
package/dist/signing.js
CHANGED
|
@@ -63,15 +63,28 @@ __name(verifyDocument, "verifyDocument");
|
|
|
63
63
|
|
|
64
64
|
// src/signing/request.ts
|
|
65
65
|
import { createHash, createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, sign as sign2, verify as verify2 } from "crypto";
|
|
66
|
+
var WORKSPACE_HEADER_ORDER = [
|
|
67
|
+
"x-site-id",
|
|
68
|
+
"x-sg-id",
|
|
69
|
+
"x-le-id",
|
|
70
|
+
"x-org-id"
|
|
71
|
+
];
|
|
66
72
|
function buildRequestCanonical(input) {
|
|
67
73
|
const bodyHash = createHash("sha256").update(input.body ?? "").digest("hex");
|
|
68
|
-
|
|
74
|
+
const lines = [
|
|
69
75
|
input.method.toUpperCase(),
|
|
70
76
|
input.path,
|
|
71
77
|
input.orgId ?? "",
|
|
72
78
|
bodyHash,
|
|
73
79
|
String(input.timestamp)
|
|
74
|
-
]
|
|
80
|
+
];
|
|
81
|
+
if (input.query) lines.push(`query:${input.query}`);
|
|
82
|
+
if (input.partyId) lines.push(`party:${input.partyId}`);
|
|
83
|
+
for (const header of WORKSPACE_HEADER_ORDER) {
|
|
84
|
+
const value = input.workspaceHeaders?.[header];
|
|
85
|
+
if (value) lines.push(`${header}:${value}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
75
88
|
}
|
|
76
89
|
__name(buildRequestCanonical, "buildRequestCanonical");
|
|
77
90
|
function signRequestHeaders(input, privateKeyBase64) {
|
|
@@ -102,6 +115,9 @@ function verifySignedRequest(input) {
|
|
|
102
115
|
method: input.method,
|
|
103
116
|
path: input.path,
|
|
104
117
|
orgId: input.orgId,
|
|
118
|
+
query: input.query,
|
|
119
|
+
partyId: input.partyId,
|
|
120
|
+
workspaceHeaders: input.workspaceHeaders,
|
|
105
121
|
body: input.rawBody,
|
|
106
122
|
timestamp
|
|
107
123
|
});
|
package/dist/signing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/signing/canonical.ts","../src/signing/document.ts","../src/signing/request.ts"],"sourcesContent":["// Serializes a value as deterministic JSON: object keys recursively sorted, arrays keep their order\nexport function canonicalStringify(value: unknown): string {\n return JSON.stringify(sortKeysDeep(value));\n}\n\n// Recursively rebuilds objects with sorted keys so JSON.stringify output is order-independent\nfunction sortKeysDeep(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeysDeep);\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(record).sort()) sorted[key] = sortKeysDeep(record[key]);\n return sorted;\n }\n return value;\n}\n","import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';\nimport { canonicalStringify } from './canonical';\n\nexport interface SignedDocument<T> {\n payload: T;\n signature: string;\n}\n\n// Generates an Ed25519 key pair as base64 DER strings (pkcs8 private / spki public)\nexport function generateSigningKeyPair(): { privateKey: string; publicKey: string } {\n const { privateKey, publicKey } = generateKeyPairSync('ed25519');\n return {\n privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),\n publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),\n };\n}\n\n// Signs a payload's canonical JSON with an Ed25519 private key (base64 pkcs8 DER)\nexport function signDocument<T>(payload: T, privateKeyBase64: string): SignedDocument<T> {\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonicalStringify(payload), 'utf8'), key).toString('base64');\n return { payload, signature };\n}\n\n// Verifies a signed document against an Ed25519 public key (base64 spki DER); malformed input ⇒ false\nexport function verifyDocument<T>(doc: SignedDocument<T>, publicKeyBase64: string): boolean {\n try {\n const key = createPublicKey({ key: Buffer.from(publicKeyBase64, 'base64'), format: 'der', type: 'spki' });\n return verify(\n null,\n Buffer.from(canonicalStringify(doc.payload), 'utf8'),\n key,\n Buffer.from(doc.signature, 'base64'),\n );\n } catch {\n return false;\n }\n}\n","import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';\n\nexport interface RequestCanonicalInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n timestamp: number;\n}\n\nexport interface SignRequestInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n}\n\nexport interface VerifySignedRequestInput {\n method: string;\n path: string;\n orgId?: string;\n rawBody?: string | Buffer;\n timestamp: string | number;\n signature: string;\n publicKey: string;\n maxSkewSeconds?: number;\n}\n\n// Builds the canonical string signed for a control-plane request: METHOD\\npath\\norgId\\nsha256hex(body)\\ntimestamp\nexport function buildRequestCanonical(input: RequestCanonicalInput): string {\n const bodyHash = createHash('sha256')\n .update(input.body ?? '')\n .digest('hex');\n return [input.method.toUpperCase(), input.path, input.orgId ?? '', bodyHash, String(input.timestamp)].join('\\n');\n}\n\n// Signs a request with an Ed25519 private key (base64 pkcs8 DER), stamping the current unix time\nexport function signRequestHeaders(\n input: SignRequestInput,\n privateKeyBase64: string,\n): { 'x-timestamp': string; 'x-signature': string } {\n const timestamp = Math.floor(Date.now() / 1000);\n const canonical = buildRequestCanonical({ ...input, timestamp });\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonical, 'utf8'), key).toString('base64');\n return { 'x-timestamp': String(timestamp), 'x-signature': signature };\n}\n\n// Verifies a signed request (signature + timestamp skew) against an Ed25519 public key; malformed input ⇒ false\nexport function verifySignedRequest(input: VerifySignedRequestInput): boolean {\n try {\n const timestamp = Number(input.timestamp);\n if (!Number.isFinite(timestamp)) return false;\n const maxSkew = input.maxSkewSeconds ?? 300;\n if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > maxSkew) return false;\n const canonical = buildRequestCanonical({\n method: input.method,\n path: input.path,\n orgId: input.orgId,\n body: input.rawBody,\n timestamp,\n });\n const key = createPublicKey({ key: Buffer.from(input.publicKey, 'base64'), format: 'der', type: 'spki' });\n return verify(null, Buffer.from(canonical, 'utf8'), key, Buffer.from(input.signature, 'base64'));\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["canonicalStringify","value","JSON","stringify","sortKeysDeep","Array","isArray","map","record","sorted","key","Object","keys","sort","createPrivateKey","createPublicKey","generateKeyPairSync","sign","verify","generateSigningKeyPair","privateKey","publicKey","generateKeyPairSync","export","type","format","toString","signDocument","payload","privateKeyBase64","key","createPrivateKey","Buffer","from","signature","sign","canonicalStringify","verifyDocument","doc","publicKeyBase64","createPublicKey","verify","createHash","createPrivateKey","createPublicKey","sign","verify","buildRequestCanonical","input","bodyHash","createHash","update","body","digest","method","toUpperCase","path","orgId","String","timestamp","join","signRequestHeaders","privateKeyBase64","Math","floor","Date","now","canonical","key","createPrivateKey","Buffer","from","format","type","signature","sign","toString","verifySignedRequest","Number","isFinite","maxSkew","maxSkewSeconds","abs","rawBody","createPublicKey","publicKey","verify"]}
|
|
1
|
+
{"version":3,"sources":["../src/signing/canonical.ts","../src/signing/document.ts","../src/signing/request.ts"],"sourcesContent":["// Serializes a value as deterministic JSON: object keys recursively sorted, arrays keep their order\nexport function canonicalStringify(value: unknown): string {\n return JSON.stringify(sortKeysDeep(value));\n}\n\n// Recursively rebuilds objects with sorted keys so JSON.stringify output is order-independent\nfunction sortKeysDeep(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeysDeep);\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(record).sort()) sorted[key] = sortKeysDeep(record[key]);\n return sorted;\n }\n return value;\n}\n","import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';\nimport { canonicalStringify } from './canonical';\n\nexport interface SignedDocument<T> {\n payload: T;\n signature: string;\n}\n\n// Generates an Ed25519 key pair as base64 DER strings (pkcs8 private / spki public)\nexport function generateSigningKeyPair(): { privateKey: string; publicKey: string } {\n const { privateKey, publicKey } = generateKeyPairSync('ed25519');\n return {\n privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),\n publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),\n };\n}\n\n// Signs a payload's canonical JSON with an Ed25519 private key (base64 pkcs8 DER)\nexport function signDocument<T>(payload: T, privateKeyBase64: string): SignedDocument<T> {\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonicalStringify(payload), 'utf8'), key).toString('base64');\n return { payload, signature };\n}\n\n// Verifies a signed document against an Ed25519 public key (base64 spki DER); malformed input ⇒ false\nexport function verifyDocument<T>(doc: SignedDocument<T>, publicKeyBase64: string): boolean {\n try {\n const key = createPublicKey({ key: Buffer.from(publicKeyBase64, 'base64'), format: 'der', type: 'spki' });\n return verify(\n null,\n Buffer.from(canonicalStringify(doc.payload), 'utf8'),\n key,\n Buffer.from(doc.signature, 'base64'),\n );\n } catch {\n return false;\n }\n}\n","import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';\n\n/**\n * The workspace-scope headers, in the order the canonical appends them.\n *\n * Exactly one is expected — the kind of scope *is* which header is sent, the same\n * convention core-web and core-app already use. Signing the header **name** rather\n * than a separate `kind` field means a verifier needs no scope vocabulary: it scans\n * this same list in this same order and rebuilds an identical string.\n *\n * Fixed and shared so a signer and a verifier can never disagree about it, and so the\n * discouraged multi-header case is deterministic rather than ambiguous.\n */\nexport const WORKSPACE_HEADER_ORDER = ['x-site-id', 'x-sg-id', 'x-le-id', 'x-org-id'] as const;\n\nexport interface RequestCanonicalInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n timestamp: number;\n /**\n * The raw query string, without the leading `?`.\n *\n * Signed byte for byte, with no sorting or re-encoding, so the signer and the\n * verifier cannot disagree about normalization. The consequence is that a proxy\n * which reorders or re-encodes parameters invalidates the signature.\n */\n query?: string;\n /**\n * The party (person) the request acts for, when there is one.\n *\n * Signed so a caller cannot be swapped for another shopper in transit.\n */\n partyId?: string;\n /**\n * The workspace-scope headers actually present on the request, by header name.\n *\n * Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in\n * that order — so signer and verifier agree without either needing to know what a\n * \"site\" or a \"legal entity\" is.\n */\n workspaceHeaders?: Record<string, string | undefined>;\n}\n\nexport interface SignRequestInput {\n method: string;\n path: string;\n orgId?: string;\n body?: string | Buffer;\n /** Raw query string, without the leading `?`. See `RequestCanonicalInput`. */\n query?: string;\n /**\n * The party (person) the request acts for, when there is one.\n *\n * Signed so a caller cannot be swapped for another shopper in transit.\n */\n partyId?: string;\n /**\n * The workspace-scope headers actually present on the request, by header name.\n *\n * Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in\n * that order — so signer and verifier agree without either needing to know what a\n * \"site\" or a \"legal entity\" is.\n */\n workspaceHeaders?: Record<string, string | undefined>;\n}\n\nexport interface VerifySignedRequestInput {\n method: string;\n path: string;\n orgId?: string;\n /** Raw query string, without the leading `?`. See `RequestCanonicalInput`. */\n query?: string;\n /**\n * The party (person) the request acts for, when there is one.\n *\n * Signed so a caller cannot be swapped for another shopper in transit.\n */\n partyId?: string;\n /**\n * The workspace-scope headers actually present on the request, by header name.\n *\n * Only names in `WORKSPACE_HEADER_ORDER` are considered, and they are appended in\n * that order — so signer and verifier agree without either needing to know what a\n * \"site\" or a \"legal entity\" is.\n */\n workspaceHeaders?: Record<string, string | undefined>;\n rawBody?: string | Buffer;\n timestamp: string | number;\n signature: string;\n publicKey: string;\n maxSkewSeconds?: number;\n}\n\n/**\n * Builds the canonical string a request's signature is made over:\n * `METHOD\\npath\\norgId\\nsha256hex(body)\\ntimestamp`, then a labelled line per\n * optional field that is present.\n *\n * Optional fields are appended **only when set**, so a caller that supplies none\n * produces exactly the string this function has always produced — which is what\n * lets new fields be added without invalidating existing signers. They are\n * **labelled** so that \"query present, party absent\" can never be confused with\n * \"party present, query absent\".\n *\n * Absence is still tamper-evident: strip a signed field in transit and the\n * verifier rebuilds the canonical without it, which no longer matches.\n */\nexport function buildRequestCanonical(input: RequestCanonicalInput): string {\n const bodyHash = createHash('sha256')\n .update(input.body ?? '')\n .digest('hex');\n\n const lines = [input.method.toUpperCase(), input.path, input.orgId ?? '', bodyHash, String(input.timestamp)];\n if (input.query) lines.push(`query:${input.query}`);\n if (input.partyId) lines.push(`party:${input.partyId}`);\n\n // Header name and value both go in, so swapping x-le-id for x-site-id on the same\n // value breaks the signature. Fixed order, so multiple headers stay deterministic.\n for (const header of WORKSPACE_HEADER_ORDER) {\n const value = input.workspaceHeaders?.[header];\n if (value) lines.push(`${header}:${value}`);\n }\n\n return lines.join('\\n');\n}\n\n// Signs a request with an Ed25519 private key (base64 pkcs8 DER), stamping the current unix time\nexport function signRequestHeaders(\n input: SignRequestInput,\n privateKeyBase64: string,\n): { 'x-timestamp': string; 'x-signature': string } {\n const timestamp = Math.floor(Date.now() / 1000);\n const canonical = buildRequestCanonical({ ...input, timestamp });\n const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });\n const signature = sign(null, Buffer.from(canonical, 'utf8'), key).toString('base64');\n return { 'x-timestamp': String(timestamp), 'x-signature': signature };\n}\n\n// Verifies a signed request (signature + timestamp skew) against an Ed25519 public key; malformed input ⇒ false\nexport function verifySignedRequest(input: VerifySignedRequestInput): boolean {\n try {\n const timestamp = Number(input.timestamp);\n if (!Number.isFinite(timestamp)) return false;\n const maxSkew = input.maxSkewSeconds ?? 300;\n if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > maxSkew) return false;\n const canonical = buildRequestCanonical({\n method: input.method,\n path: input.path,\n orgId: input.orgId,\n query: input.query,\n partyId: input.partyId,\n workspaceHeaders: input.workspaceHeaders,\n body: input.rawBody,\n timestamp,\n });\n const key = createPublicKey({ key: Buffer.from(input.publicKey, 'base64'), format: 'der', type: 'spki' });\n return verify(null, Buffer.from(canonical, 'utf8'), key, Buffer.from(input.signature, 'base64'));\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["canonicalStringify","value","JSON","stringify","sortKeysDeep","Array","isArray","map","record","sorted","key","Object","keys","sort","createPrivateKey","createPublicKey","generateKeyPairSync","sign","verify","generateSigningKeyPair","privateKey","publicKey","generateKeyPairSync","export","type","format","toString","signDocument","payload","privateKeyBase64","key","createPrivateKey","Buffer","from","signature","sign","canonicalStringify","verifyDocument","doc","publicKeyBase64","createPublicKey","verify","createHash","createPrivateKey","createPublicKey","sign","verify","WORKSPACE_HEADER_ORDER","buildRequestCanonical","input","bodyHash","createHash","update","body","digest","lines","method","toUpperCase","path","orgId","String","timestamp","query","push","partyId","header","value","workspaceHeaders","join","signRequestHeaders","privateKeyBase64","Math","floor","Date","now","canonical","key","createPrivateKey","Buffer","from","format","type","signature","sign","toString","verifySignedRequest","Number","isFinite","maxSkew","maxSkewSeconds","abs","rawBody","createPublicKey","publicKey","verify"]}
|