@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.
@@ -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
- return [
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
- ].join("\n");
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
  });
@@ -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":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;ACCO,SAASA,mBAAmBC,OAAc;AAC/C,SAAOC,KAAKC,UAAUC,aAAaH,KAAAA,CAAAA;AACrC;AAFgBD;AAKhB,SAASI,aAAaH,OAAc;AAClC,MAAII,MAAMC,QAAQL,KAAAA,EAAQ,QAAOA,MAAMM,IAAIH,YAAAA;AAC3C,MAAIH,UAAU,QAAQ,OAAOA,UAAU,UAAU;AAC/C,UAAMO,SAASP;AACf,UAAMQ,SAAkC,CAAC;AACzC,eAAWC,OAAOC,OAAOC,KAAKJ,MAAAA,EAAQK,KAAI,EAAIJ,QAAOC,GAAAA,IAAON,aAAaI,OAAOE,GAAAA,CAAI;AACpF,WAAOD;EACT;AACA,SAAOR;AACT;AATSG;;;ACNT,yBAAqF;AAS9E,SAASU,yBAAAA;AACd,QAAM,EAAEC,YAAYC,UAAS,QAAKC,wCAAoB,SAAA;AACtD,SAAO;IACLF,YAAYA,WAAWG,OAAO;MAAEC,MAAM;MAASC,QAAQ;IAAM,CAAA,EAAGC,SAAS,QAAA;IACzEL,WAAWA,UAAUE,OAAO;MAAEC,MAAM;MAAQC,QAAQ;IAAM,CAAA,EAAGC,SAAS,QAAA;EACxE;AACF;AANgBP;AAST,SAASQ,aAAgBC,SAAYC,kBAAwB;AAClE,QAAMC,UAAMC,qCAAiB;IAAED,KAAKE,OAAOC,KAAKJ,kBAAkB,QAAA;IAAWJ,QAAQ;IAAOD,MAAM;EAAQ,CAAA;AAC1G,QAAMU,gBAAYC,yBAAK,MAAMH,OAAOC,KAAKG,mBAAmBR,OAAAA,GAAU,MAAA,GAASE,GAAAA,EAAKJ,SAAS,QAAA;AAC7F,SAAO;IAAEE;IAASM;EAAU;AAC9B;AAJgBP;AAOT,SAASU,eAAkBC,KAAwBC,iBAAuB;AAC/E,MAAI;AACF,UAAMT,UAAMU,oCAAgB;MAAEV,KAAKE,OAAOC,KAAKM,iBAAiB,QAAA;MAAWd,QAAQ;MAAOD,MAAM;IAAO,CAAA;AACvG,eAAOiB,2BACL,MACAT,OAAOC,KAAKG,mBAAmBE,IAAIV,OAAO,GAAG,MAAA,GAC7CE,KACAE,OAAOC,KAAKK,IAAIJ,WAAW,QAAA,CAAA;EAE/B,QAAQ;AACN,WAAO;EACT;AACF;AAZgBG;;;ACzBhB,IAAAK,sBAA4E;AA6BrE,SAASC,sBAAsBC,OAA4B;AAChE,QAAMC,eAAWC,gCAAW,QAAA,EACzBC,OAAOH,MAAMI,QAAQ,EAAA,EACrBC,OAAO,KAAA;AACV,SAAO;IAACL,MAAMM,OAAOC,YAAW;IAAIP,MAAMQ;IAAMR,MAAMS,SAAS;IAAIR;IAAUS,OAAOV,MAAMW,SAAS;IAAGC,KAAK,IAAA;AAC7G;AALgBb;AAQT,SAASc,mBACdb,OACAc,kBAAwB;AAExB,QAAMH,YAAYI,KAAKC,MAAMC,KAAKC,IAAG,IAAK,GAAA;AAC1C,QAAMC,YAAYpB,sBAAsB;IAAE,GAAGC;IAAOW;EAAU,CAAA;AAC9D,QAAMS,UAAMC,sCAAiB;IAAED,KAAKE,OAAOC,KAAKT,kBAAkB,QAAA;IAAWU,QAAQ;IAAOC,MAAM;EAAQ,CAAA;AAC1G,QAAMC,gBAAYC,0BAAK,MAAML,OAAOC,KAAKJ,WAAW,MAAA,GAASC,GAAAA,EAAKQ,SAAS,QAAA;AAC3E,SAAO;IAAE,eAAelB,OAAOC,SAAAA;IAAY,eAAee;EAAU;AACtE;AATgBb;AAYT,SAASgB,oBAAoB7B,OAA+B;AACjE,MAAI;AACF,UAAMW,YAAYmB,OAAO9B,MAAMW,SAAS;AACxC,QAAI,CAACmB,OAAOC,SAASpB,SAAAA,EAAY,QAAO;AACxC,UAAMqB,UAAUhC,MAAMiC,kBAAkB;AACxC,QAAIlB,KAAKmB,IAAInB,KAAKC,MAAMC,KAAKC,IAAG,IAAK,GAAA,IAAQP,SAAAA,IAAaqB,QAAS,QAAO;AAC1E,UAAMb,YAAYpB,sBAAsB;MACtCO,QAAQN,MAAMM;MACdE,MAAMR,MAAMQ;MACZC,OAAOT,MAAMS;MACbL,MAAMJ,MAAMmC;MACZxB;IACF,CAAA;AACA,UAAMS,UAAMgB,qCAAgB;MAAEhB,KAAKE,OAAOC,KAAKvB,MAAMqC,WAAW,QAAA;MAAWb,QAAQ;MAAOC,MAAM;IAAO,CAAA;AACvG,eAAOa,4BAAO,MAAMhB,OAAOC,KAAKJ,WAAW,MAAA,GAASC,KAAKE,OAAOC,KAAKvB,MAAM0B,WAAW,QAAA,CAAA;EACxF,QAAQ;AACN,WAAO;EACT;AACF;AAlBgBG;","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"]}
@@ -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
- interface RequestCanonicalInput {
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
- interface RequestCanonicalInput {
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
- return [
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
- ].join("\n");
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
  });
@@ -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"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vritti/api-sdk",
3
3
  "type": "module",
4
- "version": "0.3.11",
4
+ "version": "0.3.12",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",