@suveren/gateway 0.2.3 → 0.2.4

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.
@@ -401,6 +401,41 @@ type GatekeeperResult = {
401
401
  errors: GatekeeperError[];
402
402
  };
403
403
 
404
+ /**
405
+ * JCS — JSON Canonicalization (RFC 8785) for signing.
406
+ *
407
+ * Both attestation and receipt signatures are computed over a canonical byte
408
+ * serialization of the payload, so that two independent implementations (an AS,
409
+ * a gateway, an external verifier — in any language) agree on exactly which
410
+ * bytes were signed regardless of how they happened to construct the object.
411
+ *
412
+ * `JSON.stringify` is NOT sufficient: it preserves key *insertion* order, so the
413
+ * same logical payload built with keys in a different order serializes to
414
+ * different bytes and the signature fails to verify. Canonicalization removes
415
+ * that dependency by sorting keys.
416
+ *
417
+ * This is RFC 8785-compatible and matches the v0.5 spec's normative signing
418
+ * canonicalization (core.md "Signing Canonicalization"):
419
+ * 1. UTF-8.
420
+ * 2. Object keys sorted (RFC 8785 sorts by UTF-16 code units — exactly what
421
+ * JavaScript's default String comparison does, so `Object.keys().sort()` is
422
+ * correct here).
423
+ * 3. No insignificant whitespace.
424
+ * 4. Numbers in the shortest round-trippable form — the ECMAScript
425
+ * Number-to-String algorithm, which `JSON.stringify(number)` produces and
426
+ * RFC 8785 adopts verbatim.
427
+ * 5. Strings escaped per RFC 8259; non-ASCII passed through as UTF-8 (this is
428
+ * `JSON.stringify`'s behaviour for strings).
429
+ * 6. Array order preserved.
430
+ *
431
+ * Because it relies only on `JSON.stringify` for leaves plus `Object.keys`,
432
+ * `Array`, and `String` sort — all of which are environment-independent in
433
+ * JavaScript — Node and the browser produce byte-identical output. The
434
+ * conformance test pins a (payload → canonical bytes) vector that any other
435
+ * implementation can check against.
436
+ */
437
+ declare function canonicalize(value: unknown): string;
438
+
404
439
  /**
405
440
  * Frame Canonicalization for Agent Profiles
406
441
  *
@@ -598,4 +633,4 @@ declare function listProfiles(): string[];
598
633
  declare function getAllProfiles(): AgentProfile[];
599
634
  declare function clearProfiles(): void;
600
635
 
601
- export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
636
+ export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
@@ -401,6 +401,41 @@ type GatekeeperResult = {
401
401
  errors: GatekeeperError[];
402
402
  };
403
403
 
404
+ /**
405
+ * JCS — JSON Canonicalization (RFC 8785) for signing.
406
+ *
407
+ * Both attestation and receipt signatures are computed over a canonical byte
408
+ * serialization of the payload, so that two independent implementations (an AS,
409
+ * a gateway, an external verifier — in any language) agree on exactly which
410
+ * bytes were signed regardless of how they happened to construct the object.
411
+ *
412
+ * `JSON.stringify` is NOT sufficient: it preserves key *insertion* order, so the
413
+ * same logical payload built with keys in a different order serializes to
414
+ * different bytes and the signature fails to verify. Canonicalization removes
415
+ * that dependency by sorting keys.
416
+ *
417
+ * This is RFC 8785-compatible and matches the v0.5 spec's normative signing
418
+ * canonicalization (core.md "Signing Canonicalization"):
419
+ * 1. UTF-8.
420
+ * 2. Object keys sorted (RFC 8785 sorts by UTF-16 code units — exactly what
421
+ * JavaScript's default String comparison does, so `Object.keys().sort()` is
422
+ * correct here).
423
+ * 3. No insignificant whitespace.
424
+ * 4. Numbers in the shortest round-trippable form — the ECMAScript
425
+ * Number-to-String algorithm, which `JSON.stringify(number)` produces and
426
+ * RFC 8785 adopts verbatim.
427
+ * 5. Strings escaped per RFC 8259; non-ASCII passed through as UTF-8 (this is
428
+ * `JSON.stringify`'s behaviour for strings).
429
+ * 6. Array order preserved.
430
+ *
431
+ * Because it relies only on `JSON.stringify` for leaves plus `Object.keys`,
432
+ * `Array`, and `String` sort — all of which are environment-independent in
433
+ * JavaScript — Node and the browser produce byte-identical output. The
434
+ * conformance test pins a (payload → canonical bytes) vector that any other
435
+ * implementation can check against.
436
+ */
437
+ declare function canonicalize(value: unknown): string;
438
+
404
439
  /**
405
440
  * Frame Canonicalization for Agent Profiles
406
441
  *
@@ -598,4 +633,4 @@ declare function listProfiles(): string[];
598
633
  declare function getAllProfiles(): AgentProfile[];
599
634
  declare function clearProfiles(): void;
600
635
 
601
- export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
636
+ export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContextHash, computeFrameHash, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  canonicalBounds: () => canonicalBounds,
35
35
  canonicalContext: () => canonicalContext,
36
36
  canonicalFrame: () => canonicalFrame,
37
+ canonicalize: () => canonicalize,
37
38
  checkAttestationExpiry: () => checkAttestationExpiry,
38
39
  clearProfiles: () => clearProfiles,
39
40
  computeBoundsHash: () => computeBoundsHash,
@@ -60,6 +61,31 @@ __export(index_exports, {
60
61
  });
61
62
  module.exports = __toCommonJS(index_exports);
62
63
 
64
+ // src/canonicalize.ts
65
+ function canonicalize(value) {
66
+ if (value === void 0) {
67
+ throw new Error("canonicalize: undefined is not serializable");
68
+ }
69
+ if (typeof value === "number" && !Number.isFinite(value)) {
70
+ throw new Error(`canonicalize: ${value} is not a valid JSON number`);
71
+ }
72
+ if (value === null || typeof value !== "object") {
73
+ return JSON.stringify(value);
74
+ }
75
+ if (Array.isArray(value)) {
76
+ const items = value.map((el) => el === void 0 ? "null" : canonicalize(el));
77
+ return "[" + items.join(",") + "]";
78
+ }
79
+ const obj = value;
80
+ const parts = [];
81
+ for (const key of Object.keys(obj).sort()) {
82
+ const v = obj[key];
83
+ if (v === void 0) continue;
84
+ parts.push(JSON.stringify(key) + ":" + canonicalize(v));
85
+ }
86
+ return "{" + parts.join(",") + "}";
87
+ }
88
+
63
89
  // src/frame.ts
64
90
  var import_crypto = require("crypto");
65
91
  function validateFrameParams(params, profile) {
@@ -215,7 +241,7 @@ function attestationId(blob) {
215
241
  }
216
242
  async function verifyAttestationSignature(attestation, publicKeyHex) {
217
243
  try {
218
- const payloadJson = JSON.stringify(attestation.payload);
244
+ const payloadJson = canonicalize(attestation.payload);
219
245
  const payloadBytes = new TextEncoder().encode(payloadJson);
220
246
  const signatureBytes = Buffer.from(attestation.signature, "base64");
221
247
  const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
@@ -719,6 +745,7 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
719
745
  canonicalBounds,
720
746
  canonicalContext,
721
747
  canonicalFrame,
748
+ canonicalize,
722
749
  checkAttestationExpiry,
723
750
  clearProfiles,
724
751
  computeBoundsHash,
@@ -1,3 +1,28 @@
1
+ // src/canonicalize.ts
2
+ function canonicalize(value) {
3
+ if (value === void 0) {
4
+ throw new Error("canonicalize: undefined is not serializable");
5
+ }
6
+ if (typeof value === "number" && !Number.isFinite(value)) {
7
+ throw new Error(`canonicalize: ${value} is not a valid JSON number`);
8
+ }
9
+ if (value === null || typeof value !== "object") {
10
+ return JSON.stringify(value);
11
+ }
12
+ if (Array.isArray(value)) {
13
+ const items = value.map((el) => el === void 0 ? "null" : canonicalize(el));
14
+ return "[" + items.join(",") + "]";
15
+ }
16
+ const obj = value;
17
+ const parts = [];
18
+ for (const key of Object.keys(obj).sort()) {
19
+ const v = obj[key];
20
+ if (v === void 0) continue;
21
+ parts.push(JSON.stringify(key) + ":" + canonicalize(v));
22
+ }
23
+ return "{" + parts.join(",") + "}";
24
+ }
25
+
1
26
  // src/frame.ts
2
27
  import { createHash } from "crypto";
3
28
  function validateFrameParams(params, profile) {
@@ -153,7 +178,7 @@ function attestationId(blob) {
153
178
  }
154
179
  async function verifyAttestationSignature(attestation, publicKeyHex) {
155
180
  try {
156
- const payloadJson = JSON.stringify(attestation.payload);
181
+ const payloadJson = canonicalize(attestation.payload);
157
182
  const payloadBytes = new TextEncoder().encode(payloadJson);
158
183
  const signatureBytes = Buffer.from(attestation.signature, "base64");
159
184
  const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
@@ -656,6 +681,7 @@ export {
656
681
  canonicalBounds,
657
682
  canonicalContext,
658
683
  canonicalFrame,
684
+ canonicalize,
659
685
  checkAttestationExpiry,
660
686
  clearProfiles,
661
687
  computeBoundsHash,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,22 +21,23 @@
21
21
  "dist",
22
22
  "src"
23
23
  ],
24
- "dependencies": {
25
- "@noble/ed25519": "^2.1.0"
26
- },
27
- "devDependencies": {
28
- "@types/node": "^20.10.0",
29
- "tsup": "^8.0.0",
30
- "typescript": "^5.3.0",
31
- "vitest": "^1.0.0"
32
- },
33
24
  "scripts": {
34
25
  "build": "tsup src/index.ts --format cjs,esm --dts",
35
26
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
36
27
  "test": "vitest run",
37
28
  "test:watch": "vitest",
29
+ "prepublishOnly": "pnpm build",
38
30
  "release:patch": "pnpm version patch && git push --follow-tags",
39
31
  "release:minor": "pnpm version minor && git push --follow-tags",
40
32
  "release:major": "pnpm version major && git push --follow-tags"
33
+ },
34
+ "dependencies": {
35
+ "@noble/ed25519": "^2.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.10.0",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.3.0",
41
+ "vitest": "^1.0.0"
41
42
  }
42
- }
43
+ }
@@ -7,6 +7,7 @@
7
7
  import { createHash } from 'crypto';
8
8
  import * as ed from '@noble/ed25519';
9
9
  import type { Attestation, AttestationPayload } from './types';
10
+ import { canonicalize } from './canonicalize';
10
11
 
11
12
  /**
12
13
  * Decodes a base64url-encoded attestation blob.
@@ -50,7 +51,9 @@ export async function verifyAttestationSignature(
50
51
  publicKeyHex: string
51
52
  ): Promise<void> {
52
53
  try {
53
- const payloadJson = JSON.stringify(attestation.payload);
54
+ // JCS-canonical bytes (RFC 8785) — must match the canonicalization the
55
+ // signer used. The AS signs the same canonical form (see keys.ts).
56
+ const payloadJson = canonicalize(attestation.payload);
54
57
  const payloadBytes = new TextEncoder().encode(payloadJson);
55
58
  const signatureBytes = Buffer.from(attestation.signature, 'base64');
56
59
  const publicKeyBytes = Buffer.from(publicKeyHex, 'hex');
@@ -0,0 +1,65 @@
1
+ /**
2
+ * JCS — JSON Canonicalization (RFC 8785) for signing.
3
+ *
4
+ * Both attestation and receipt signatures are computed over a canonical byte
5
+ * serialization of the payload, so that two independent implementations (an AS,
6
+ * a gateway, an external verifier — in any language) agree on exactly which
7
+ * bytes were signed regardless of how they happened to construct the object.
8
+ *
9
+ * `JSON.stringify` is NOT sufficient: it preserves key *insertion* order, so the
10
+ * same logical payload built with keys in a different order serializes to
11
+ * different bytes and the signature fails to verify. Canonicalization removes
12
+ * that dependency by sorting keys.
13
+ *
14
+ * This is RFC 8785-compatible and matches the v0.5 spec's normative signing
15
+ * canonicalization (core.md "Signing Canonicalization"):
16
+ * 1. UTF-8.
17
+ * 2. Object keys sorted (RFC 8785 sorts by UTF-16 code units — exactly what
18
+ * JavaScript's default String comparison does, so `Object.keys().sort()` is
19
+ * correct here).
20
+ * 3. No insignificant whitespace.
21
+ * 4. Numbers in the shortest round-trippable form — the ECMAScript
22
+ * Number-to-String algorithm, which `JSON.stringify(number)` produces and
23
+ * RFC 8785 adopts verbatim.
24
+ * 5. Strings escaped per RFC 8259; non-ASCII passed through as UTF-8 (this is
25
+ * `JSON.stringify`'s behaviour for strings).
26
+ * 6. Array order preserved.
27
+ *
28
+ * Because it relies only on `JSON.stringify` for leaves plus `Object.keys`,
29
+ * `Array`, and `String` sort — all of which are environment-independent in
30
+ * JavaScript — Node and the browser produce byte-identical output. The
31
+ * conformance test pins a (payload → canonical bytes) vector that any other
32
+ * implementation can check against.
33
+ */
34
+ export function canonicalize(value: unknown): string {
35
+ if (value === undefined) {
36
+ throw new Error('canonicalize: undefined is not serializable');
37
+ }
38
+ if (typeof value === 'number' && !Number.isFinite(value)) {
39
+ throw new Error(`canonicalize: ${value} is not a valid JSON number`);
40
+ }
41
+
42
+ // Primitives: string, finite number, boolean, null — JSON.stringify is already
43
+ // canonical (RFC 8259 string escaping, ECMAScript number formatting).
44
+ if (value === null || typeof value !== 'object') {
45
+ return JSON.stringify(value);
46
+ }
47
+
48
+ if (Array.isArray(value)) {
49
+ // Array order is preserved; undefined elements become null (matching
50
+ // JSON.stringify), which keeps array length stable across serializers.
51
+ const items = value.map((el) => (el === undefined ? 'null' : canonicalize(el)));
52
+ return '[' + items.join(',') + ']';
53
+ }
54
+
55
+ // Plain object: sort keys, omit undefined-valued properties (as JSON.stringify
56
+ // does), recurse on values.
57
+ const obj = value as Record<string, unknown>;
58
+ const parts: string[] = [];
59
+ for (const key of Object.keys(obj).sort()) {
60
+ const v = obj[key];
61
+ if (v === undefined) continue;
62
+ parts.push(JSON.stringify(key) + ':' + canonicalize(v));
63
+ }
64
+ return '{' + parts.join(',') + '}';
65
+ }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  export * from './types';
8
+ export * from './canonicalize';
8
9
  export * from './frame';
9
10
  export * from './attestation';
10
11
  export * from './gatekeeper';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suveren/gateway",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Suveren gateway — local agent gateway built in compliance with the Human Agency Protocol (HAP). Runs the UI, control plane, and MCP server in one Node process.",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -26,7 +26,7 @@
26
26
  "@hpke/core": "^1.9.0",
27
27
  "express": "^4.18.0",
28
28
  "http-proxy-middleware": "^3.0.0",
29
- "@hap/core": "npm:@humanagencyp/hap-core@^0.4.6",
29
+ "@hap/core": "npm:@humanagencyp/hap-core@^0.5.0",
30
30
  "@modelcontextprotocol/sdk": "^1.12.1",
31
31
  "zod": "^3.22.0"
32
32
  },