@suveren/gateway 0.2.9 → 0.2.11

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Suveren</title>
7
- <script type="module" crossorigin src="/assets/index-DIC0vtHi.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-D3srGi9r.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BUgn3e63.css">
9
9
  </head>
10
10
  <body>
@@ -161,6 +161,32 @@ interface ProfileBoundsField {
161
161
  /** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
162
162
  enum?: string[];
163
163
  }
164
+ /**
165
+ * v0.5 Content Provenance — how a profile's action content is hashed into a
166
+ * signed receipt (`contentHash`). The ephemeral-content analog of Output
167
+ * Provenance: it binds the *bytes* of the action rather than a location.
168
+ *
169
+ * Profile-bound and OPTIONAL. Absent → no content hash is produced (full
170
+ * backward compatibility). The gateway computes the hash; the SP only ever
171
+ * receives the hash, never the content, so HAP's privacy-minimal design holds.
172
+ *
173
+ * The profile declares only the *policy* — whether to bind and how to
174
+ * canonicalize. It does NOT name the tool field: that is tool-specific and is
175
+ * resolved at runtime (the same content-field resolver the footer uses for
176
+ * `kind:"text"`; the whole record payload for `kind:"jcs"`).
177
+ */
178
+ interface ContentBinding {
179
+ /** Canonicalization version. A verifier MUST pin the version named here. */
180
+ version: string;
181
+ /**
182
+ * - 'jcs' → structured writes: RFC 8785 JCS over the record payload.
183
+ * - 'text' → free text: NFC + LF + trailing-whitespace strip (see
184
+ * canonicalizeText), auto-detected content field.
185
+ */
186
+ kind: 'jcs' | 'text';
187
+ /** text only: hash the content BEFORE any appended Suveren footer. */
188
+ pre_footer?: boolean;
189
+ }
164
190
  /**
165
191
  * Context field definition within a v0.4 profile.
166
192
  */
@@ -280,6 +306,13 @@ interface AgentProfile {
280
306
  max: number;
281
307
  };
282
308
  retention_minimum: number;
309
+ /**
310
+ * v0.5 Content Provenance (OPTIONAL, profile-bound). When present, the
311
+ * gateway computes a `contentHash` for gated writes under this profile and
312
+ * passes it (hash only) to the SP, which signs it into the receipt. Absent
313
+ * → no content hash. See {@link ContentBinding}.
314
+ */
315
+ content_binding?: ContentBinding;
283
316
  /**
284
317
  * Tool gating configuration — how MCP tools map to execution context.
285
318
  * @deprecated Tool gating now lives in integration manifests (content/integrations/*.json).
@@ -436,6 +469,55 @@ type GatekeeperResult = {
436
469
  */
437
470
  declare function canonicalize(value: unknown): string;
438
471
 
472
+ /**
473
+ * Content binding — Level 2 content proof (HAP v0.5 Content Provenance).
474
+ *
475
+ * A receipt normally proves who/why/bounds/when but NOT the action's content.
476
+ * Content binding closes that gap: the gateway computes a `content_hash` over
477
+ * the action's content per the profile's {@link ContentBinding} and hands the
478
+ * SP only the hash. The SP signs it into the receipt verbatim — it never sees
479
+ * the content, so HAP's privacy-minimal design is preserved. Anyone holding
480
+ * the content can recompute the hash and check it against the signed receipt.
481
+ *
482
+ * The hash only verifies if the verifier reproduces the EXACT bytes we hashed,
483
+ * so canonicalization is normative and versioned (pin via `ContentBinding.version`):
484
+ *
485
+ * - kind:"jcs" → RFC 8785 JCS of the record payload (see {@link canonicalize}).
486
+ * - kind:"text" → UTF-8 of the string after {@link canonicalizeText}
487
+ * (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
488
+ * trailing blank lines removed), taken pre-footer when `pre_footer` is set.
489
+ *
490
+ * Both Node and the browser produce byte-identical output: JCS relies only on
491
+ * environment-independent primitives, and the text rule uses String.normalize +
492
+ * plain string ops. The SHA-256 is computed with Node `crypto` here (the same
493
+ * pattern as frame.ts); browser callers that need to recompute use their own
494
+ * SubtleCrypto digest over the identical canonical bytes.
495
+ */
496
+
497
+ /**
498
+ * Canonicalize free text per the v0.5 'text' rule. Idempotent.
499
+ *
500
+ * 1. Unicode NFC normalization.
501
+ * 2. CRLF / CR → LF.
502
+ * 3. Strip trailing spaces/tabs from every line.
503
+ * 4. Remove trailing blank lines.
504
+ */
505
+ declare function canonicalizeText(input: string): string;
506
+ /**
507
+ * Compute the canonical bytes that a content hash is taken over, WITHOUT
508
+ * hashing — exposed so verifiers can debug a mismatch by inspecting the exact
509
+ * serialization both sides should agree on.
510
+ */
511
+ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Record<string, unknown> | string): string;
512
+ /**
513
+ * Compute a profile-bound content hash, formatted `sha256:<hex>` (matching the
514
+ * frame/bounds/context hash format used elsewhere in HAP).
515
+ *
516
+ * @param binding the profile's content_binding declaration
517
+ * @param content the record payload (jcs) or the resolved text field (text)
518
+ */
519
+ declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
520
+
439
521
  /**
440
522
  * Frame Canonicalization for Agent Profiles
441
523
  *
@@ -633,4 +715,4 @@ declare function listProfiles(): string[];
633
715
  declare function getAllProfiles(): AgentProfile[];
634
716
  declare function clearProfiles(): void;
635
717
 
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 };
718
+ export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, 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, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
@@ -161,6 +161,32 @@ interface ProfileBoundsField {
161
161
  /** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
162
162
  enum?: string[];
163
163
  }
164
+ /**
165
+ * v0.5 Content Provenance — how a profile's action content is hashed into a
166
+ * signed receipt (`contentHash`). The ephemeral-content analog of Output
167
+ * Provenance: it binds the *bytes* of the action rather than a location.
168
+ *
169
+ * Profile-bound and OPTIONAL. Absent → no content hash is produced (full
170
+ * backward compatibility). The gateway computes the hash; the SP only ever
171
+ * receives the hash, never the content, so HAP's privacy-minimal design holds.
172
+ *
173
+ * The profile declares only the *policy* — whether to bind and how to
174
+ * canonicalize. It does NOT name the tool field: that is tool-specific and is
175
+ * resolved at runtime (the same content-field resolver the footer uses for
176
+ * `kind:"text"`; the whole record payload for `kind:"jcs"`).
177
+ */
178
+ interface ContentBinding {
179
+ /** Canonicalization version. A verifier MUST pin the version named here. */
180
+ version: string;
181
+ /**
182
+ * - 'jcs' → structured writes: RFC 8785 JCS over the record payload.
183
+ * - 'text' → free text: NFC + LF + trailing-whitespace strip (see
184
+ * canonicalizeText), auto-detected content field.
185
+ */
186
+ kind: 'jcs' | 'text';
187
+ /** text only: hash the content BEFORE any appended Suveren footer. */
188
+ pre_footer?: boolean;
189
+ }
164
190
  /**
165
191
  * Context field definition within a v0.4 profile.
166
192
  */
@@ -280,6 +306,13 @@ interface AgentProfile {
280
306
  max: number;
281
307
  };
282
308
  retention_minimum: number;
309
+ /**
310
+ * v0.5 Content Provenance (OPTIONAL, profile-bound). When present, the
311
+ * gateway computes a `contentHash` for gated writes under this profile and
312
+ * passes it (hash only) to the SP, which signs it into the receipt. Absent
313
+ * → no content hash. See {@link ContentBinding}.
314
+ */
315
+ content_binding?: ContentBinding;
283
316
  /**
284
317
  * Tool gating configuration — how MCP tools map to execution context.
285
318
  * @deprecated Tool gating now lives in integration manifests (content/integrations/*.json).
@@ -436,6 +469,55 @@ type GatekeeperResult = {
436
469
  */
437
470
  declare function canonicalize(value: unknown): string;
438
471
 
472
+ /**
473
+ * Content binding — Level 2 content proof (HAP v0.5 Content Provenance).
474
+ *
475
+ * A receipt normally proves who/why/bounds/when but NOT the action's content.
476
+ * Content binding closes that gap: the gateway computes a `content_hash` over
477
+ * the action's content per the profile's {@link ContentBinding} and hands the
478
+ * SP only the hash. The SP signs it into the receipt verbatim — it never sees
479
+ * the content, so HAP's privacy-minimal design is preserved. Anyone holding
480
+ * the content can recompute the hash and check it against the signed receipt.
481
+ *
482
+ * The hash only verifies if the verifier reproduces the EXACT bytes we hashed,
483
+ * so canonicalization is normative and versioned (pin via `ContentBinding.version`):
484
+ *
485
+ * - kind:"jcs" → RFC 8785 JCS of the record payload (see {@link canonicalize}).
486
+ * - kind:"text" → UTF-8 of the string after {@link canonicalizeText}
487
+ * (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
488
+ * trailing blank lines removed), taken pre-footer when `pre_footer` is set.
489
+ *
490
+ * Both Node and the browser produce byte-identical output: JCS relies only on
491
+ * environment-independent primitives, and the text rule uses String.normalize +
492
+ * plain string ops. The SHA-256 is computed with Node `crypto` here (the same
493
+ * pattern as frame.ts); browser callers that need to recompute use their own
494
+ * SubtleCrypto digest over the identical canonical bytes.
495
+ */
496
+
497
+ /**
498
+ * Canonicalize free text per the v0.5 'text' rule. Idempotent.
499
+ *
500
+ * 1. Unicode NFC normalization.
501
+ * 2. CRLF / CR → LF.
502
+ * 3. Strip trailing spaces/tabs from every line.
503
+ * 4. Remove trailing blank lines.
504
+ */
505
+ declare function canonicalizeText(input: string): string;
506
+ /**
507
+ * Compute the canonical bytes that a content hash is taken over, WITHOUT
508
+ * hashing — exposed so verifiers can debug a mismatch by inspecting the exact
509
+ * serialization both sides should agree on.
510
+ */
511
+ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Record<string, unknown> | string): string;
512
+ /**
513
+ * Compute a profile-bound content hash, formatted `sha256:<hex>` (matching the
514
+ * frame/bounds/context hash format used elsewhere in HAP).
515
+ *
516
+ * @param binding the profile's content_binding declaration
517
+ * @param content the record payload (jcs) or the resolved text field (text)
518
+ */
519
+ declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
520
+
439
521
  /**
440
522
  * Frame Canonicalization for Agent Profiles
441
523
  *
@@ -633,4 +715,4 @@ declare function listProfiles(): string[];
633
715
  declare function getAllProfiles(): AgentProfile[];
634
716
  declare function clearProfiles(): void;
635
717
 
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 };
718
+ export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, 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, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
@@ -35,11 +35,14 @@ __export(index_exports, {
35
35
  canonicalContext: () => canonicalContext,
36
36
  canonicalFrame: () => canonicalFrame,
37
37
  canonicalize: () => canonicalize,
38
+ canonicalizeText: () => canonicalizeText,
38
39
  checkAttestationExpiry: () => checkAttestationExpiry,
39
40
  clearProfiles: () => clearProfiles,
40
41
  computeBoundsHash: () => computeBoundsHash,
42
+ computeContentHash: () => computeContentHash,
41
43
  computeContextHash: () => computeContextHash,
42
44
  computeFrameHash: () => computeFrameHash,
45
+ contentCanonicalBytes: () => contentCanonicalBytes,
43
46
  decodeAttestationBlob: () => decodeAttestationBlob,
44
47
  encodeAttestationBlob: () => encodeAttestationBlob,
45
48
  frameHash: () => frameHash,
@@ -86,8 +89,35 @@ function canonicalize(value) {
86
89
  return "{" + parts.join(",") + "}";
87
90
  }
88
91
 
89
- // src/frame.ts
92
+ // src/content-binding.ts
90
93
  var import_crypto = require("crypto");
94
+ function canonicalizeText(input) {
95
+ const nfc = input.normalize("NFC");
96
+ const lf = nfc.replace(/\r\n?/g, "\n");
97
+ const lines = lf.split("\n").map((line) => line.replace(/[ \t]+$/, ""));
98
+ return lines.join("\n").replace(/\n+$/, "");
99
+ }
100
+ function sha256Hex(bytes) {
101
+ return (0, import_crypto.createHash)("sha256").update(bytes, "utf8").digest("hex");
102
+ }
103
+ function contentCanonicalBytes(kind, content) {
104
+ if (kind === "jcs") {
105
+ if (typeof content === "string") {
106
+ throw new Error('content_binding kind="jcs" expects a record payload (object), got a string');
107
+ }
108
+ return canonicalize(content);
109
+ }
110
+ if (typeof content !== "string") {
111
+ throw new Error('content_binding kind="text" expects a string, got an object');
112
+ }
113
+ return canonicalizeText(content);
114
+ }
115
+ function computeContentHash(binding, content) {
116
+ return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
117
+ }
118
+
119
+ // src/frame.ts
120
+ var import_crypto2 = require("crypto");
91
121
  function validateFrameParams(params, profile) {
92
122
  const errors = [];
93
123
  if (!profile.frameSchema) {
@@ -124,7 +154,7 @@ function canonicalFrame(params, profile) {
124
154
  return lines.join("\n");
125
155
  }
126
156
  function frameHash(canonicalFrameString) {
127
- const hash = (0, import_crypto.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
157
+ const hash = (0, import_crypto2.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
128
158
  return `sha256:${hash}`;
129
159
  }
130
160
  function computeFrameHash(params, profile) {
@@ -208,17 +238,17 @@ function canonicalContext(params, profile) {
208
238
  }
209
239
  function computeBoundsHash(params, profile) {
210
240
  const canonical = canonicalBounds(params, profile);
211
- const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
241
+ const hash = (0, import_crypto2.createHash)("sha256").update(canonical, "utf8").digest("hex");
212
242
  return `sha256:${hash}`;
213
243
  }
214
244
  function computeContextHash(params, profile) {
215
245
  const canonical = canonicalContext(params, profile);
216
- const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
246
+ const hash = (0, import_crypto2.createHash)("sha256").update(canonical, "utf8").digest("hex");
217
247
  return `sha256:${hash}`;
218
248
  }
219
249
 
220
250
  // src/attestation.ts
221
- var import_crypto2 = require("crypto");
251
+ var import_crypto3 = require("crypto");
222
252
  var ed = __toESM(require("@noble/ed25519"));
223
253
  function decodeAttestationBlob(blob) {
224
254
  try {
@@ -236,7 +266,7 @@ function encodeAttestationBlob(attestation) {
236
266
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
237
267
  }
238
268
  function attestationId(blob) {
239
- const hash = (0, import_crypto2.createHash)("sha256").update(blob, "utf8").digest("hex");
269
+ const hash = (0, import_crypto3.createHash)("sha256").update(blob, "utf8").digest("hex");
240
270
  return `sha256:${hash}`;
241
271
  }
242
272
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -746,11 +776,14 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
746
776
  canonicalContext,
747
777
  canonicalFrame,
748
778
  canonicalize,
779
+ canonicalizeText,
749
780
  checkAttestationExpiry,
750
781
  clearProfiles,
751
782
  computeBoundsHash,
783
+ computeContentHash,
752
784
  computeContextHash,
753
785
  computeFrameHash,
786
+ contentCanonicalBytes,
754
787
  decodeAttestationBlob,
755
788
  encodeAttestationBlob,
756
789
  frameHash,
@@ -23,8 +23,35 @@ function canonicalize(value) {
23
23
  return "{" + parts.join(",") + "}";
24
24
  }
25
25
 
26
- // src/frame.ts
26
+ // src/content-binding.ts
27
27
  import { createHash } from "crypto";
28
+ function canonicalizeText(input) {
29
+ const nfc = input.normalize("NFC");
30
+ const lf = nfc.replace(/\r\n?/g, "\n");
31
+ const lines = lf.split("\n").map((line) => line.replace(/[ \t]+$/, ""));
32
+ return lines.join("\n").replace(/\n+$/, "");
33
+ }
34
+ function sha256Hex(bytes) {
35
+ return createHash("sha256").update(bytes, "utf8").digest("hex");
36
+ }
37
+ function contentCanonicalBytes(kind, content) {
38
+ if (kind === "jcs") {
39
+ if (typeof content === "string") {
40
+ throw new Error('content_binding kind="jcs" expects a record payload (object), got a string');
41
+ }
42
+ return canonicalize(content);
43
+ }
44
+ if (typeof content !== "string") {
45
+ throw new Error('content_binding kind="text" expects a string, got an object');
46
+ }
47
+ return canonicalizeText(content);
48
+ }
49
+ function computeContentHash(binding, content) {
50
+ return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
51
+ }
52
+
53
+ // src/frame.ts
54
+ import { createHash as createHash2 } from "crypto";
28
55
  function validateFrameParams(params, profile) {
29
56
  const errors = [];
30
57
  if (!profile.frameSchema) {
@@ -61,7 +88,7 @@ function canonicalFrame(params, profile) {
61
88
  return lines.join("\n");
62
89
  }
63
90
  function frameHash(canonicalFrameString) {
64
- const hash = createHash("sha256").update(canonicalFrameString, "utf8").digest("hex");
91
+ const hash = createHash2("sha256").update(canonicalFrameString, "utf8").digest("hex");
65
92
  return `sha256:${hash}`;
66
93
  }
67
94
  function computeFrameHash(params, profile) {
@@ -145,17 +172,17 @@ function canonicalContext(params, profile) {
145
172
  }
146
173
  function computeBoundsHash(params, profile) {
147
174
  const canonical = canonicalBounds(params, profile);
148
- const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
175
+ const hash = createHash2("sha256").update(canonical, "utf8").digest("hex");
149
176
  return `sha256:${hash}`;
150
177
  }
151
178
  function computeContextHash(params, profile) {
152
179
  const canonical = canonicalContext(params, profile);
153
- const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
180
+ const hash = createHash2("sha256").update(canonical, "utf8").digest("hex");
154
181
  return `sha256:${hash}`;
155
182
  }
156
183
 
157
184
  // src/attestation.ts
158
- import { createHash as createHash2 } from "crypto";
185
+ import { createHash as createHash3 } from "crypto";
159
186
  import * as ed from "@noble/ed25519";
160
187
  function decodeAttestationBlob(blob) {
161
188
  try {
@@ -173,7 +200,7 @@ function encodeAttestationBlob(attestation) {
173
200
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
174
201
  }
175
202
  function attestationId(blob) {
176
- const hash = createHash2("sha256").update(blob, "utf8").digest("hex");
203
+ const hash = createHash3("sha256").update(blob, "utf8").digest("hex");
177
204
  return `sha256:${hash}`;
178
205
  }
179
206
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -682,11 +709,14 @@ export {
682
709
  canonicalContext,
683
710
  canonicalFrame,
684
711
  canonicalize,
712
+ canonicalizeText,
685
713
  checkAttestationExpiry,
686
714
  clearProfiles,
687
715
  computeBoundsHash,
716
+ computeContentHash,
688
717
  computeContextHash,
689
718
  computeFrameHash,
719
+ contentCanonicalBytes,
690
720
  decodeAttestationBlob,
691
721
  encodeAttestationBlob,
692
722
  frameHash,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Content binding — Level 2 content proof (HAP v0.5 Content Provenance).
3
+ *
4
+ * A receipt normally proves who/why/bounds/when but NOT the action's content.
5
+ * Content binding closes that gap: the gateway computes a `content_hash` over
6
+ * the action's content per the profile's {@link ContentBinding} and hands the
7
+ * SP only the hash. The SP signs it into the receipt verbatim — it never sees
8
+ * the content, so HAP's privacy-minimal design is preserved. Anyone holding
9
+ * the content can recompute the hash and check it against the signed receipt.
10
+ *
11
+ * The hash only verifies if the verifier reproduces the EXACT bytes we hashed,
12
+ * so canonicalization is normative and versioned (pin via `ContentBinding.version`):
13
+ *
14
+ * - kind:"jcs" → RFC 8785 JCS of the record payload (see {@link canonicalize}).
15
+ * - kind:"text" → UTF-8 of the string after {@link canonicalizeText}
16
+ * (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
17
+ * trailing blank lines removed), taken pre-footer when `pre_footer` is set.
18
+ *
19
+ * Both Node and the browser produce byte-identical output: JCS relies only on
20
+ * environment-independent primitives, and the text rule uses String.normalize +
21
+ * plain string ops. The SHA-256 is computed with Node `crypto` here (the same
22
+ * pattern as frame.ts); browser callers that need to recompute use their own
23
+ * SubtleCrypto digest over the identical canonical bytes.
24
+ */
25
+
26
+ import { createHash } from 'crypto';
27
+ import { canonicalize } from './canonicalize';
28
+ import type { ContentBinding } from './types';
29
+
30
+ /**
31
+ * Canonicalize free text per the v0.5 'text' rule. Idempotent.
32
+ *
33
+ * 1. Unicode NFC normalization.
34
+ * 2. CRLF / CR → LF.
35
+ * 3. Strip trailing spaces/tabs from every line.
36
+ * 4. Remove trailing blank lines.
37
+ */
38
+ export function canonicalizeText(input: string): string {
39
+ const nfc = input.normalize('NFC');
40
+ const lf = nfc.replace(/\r\n?/g, '\n');
41
+ const lines = lf.split('\n').map((line) => line.replace(/[ \t]+$/, ''));
42
+ return lines.join('\n').replace(/\n+$/, '');
43
+ }
44
+
45
+ /** sha256 of a UTF-8 string → 64 hex chars. */
46
+ function sha256Hex(bytes: string): string {
47
+ return createHash('sha256').update(bytes, 'utf8').digest('hex');
48
+ }
49
+
50
+ /**
51
+ * Compute the canonical bytes that a content hash is taken over, WITHOUT
52
+ * hashing — exposed so verifiers can debug a mismatch by inspecting the exact
53
+ * serialization both sides should agree on.
54
+ */
55
+ export function contentCanonicalBytes(
56
+ kind: ContentBinding['kind'],
57
+ content: Record<string, unknown> | string,
58
+ ): string {
59
+ if (kind === 'jcs') {
60
+ if (typeof content === 'string') {
61
+ throw new Error('content_binding kind="jcs" expects a record payload (object), got a string');
62
+ }
63
+ return canonicalize(content);
64
+ }
65
+ if (typeof content !== 'string') {
66
+ throw new Error('content_binding kind="text" expects a string, got an object');
67
+ }
68
+ return canonicalizeText(content);
69
+ }
70
+
71
+ /**
72
+ * Compute a profile-bound content hash, formatted `sha256:<hex>` (matching the
73
+ * frame/bounds/context hash format used elsewhere in HAP).
74
+ *
75
+ * @param binding the profile's content_binding declaration
76
+ * @param content the record payload (jcs) or the resolved text field (text)
77
+ */
78
+ export function computeContentHash(
79
+ binding: ContentBinding,
80
+ content: Record<string, unknown> | string,
81
+ ): string {
82
+ return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
83
+ }
@@ -6,6 +6,7 @@
6
6
 
7
7
  export * from './types';
8
8
  export * from './canonicalize';
9
+ export * from './content-binding';
9
10
  export * from './frame';
10
11
  export * from './attestation';
11
12
  export * from './gatekeeper';
@@ -166,6 +166,33 @@ export interface ProfileBoundsField {
166
166
  enum?: string[];
167
167
  }
168
168
 
169
+ /**
170
+ * v0.5 Content Provenance — how a profile's action content is hashed into a
171
+ * signed receipt (`contentHash`). The ephemeral-content analog of Output
172
+ * Provenance: it binds the *bytes* of the action rather than a location.
173
+ *
174
+ * Profile-bound and OPTIONAL. Absent → no content hash is produced (full
175
+ * backward compatibility). The gateway computes the hash; the SP only ever
176
+ * receives the hash, never the content, so HAP's privacy-minimal design holds.
177
+ *
178
+ * The profile declares only the *policy* — whether to bind and how to
179
+ * canonicalize. It does NOT name the tool field: that is tool-specific and is
180
+ * resolved at runtime (the same content-field resolver the footer uses for
181
+ * `kind:"text"`; the whole record payload for `kind:"jcs"`).
182
+ */
183
+ export interface ContentBinding {
184
+ /** Canonicalization version. A verifier MUST pin the version named here. */
185
+ version: string;
186
+ /**
187
+ * - 'jcs' → structured writes: RFC 8785 JCS over the record payload.
188
+ * - 'text' → free text: NFC + LF + trailing-whitespace strip (see
189
+ * canonicalizeText), auto-detected content field.
190
+ */
191
+ kind: 'jcs' | 'text';
192
+ /** text only: hash the content BEFORE any appended Suveren footer. */
193
+ pre_footer?: boolean;
194
+ }
195
+
169
196
  /**
170
197
  * Context field definition within a v0.4 profile.
171
198
  */
@@ -295,6 +322,14 @@ export interface AgentProfile {
295
322
  ttl: { default: number; max: number };
296
323
  retention_minimum: number;
297
324
 
325
+ /**
326
+ * v0.5 Content Provenance (OPTIONAL, profile-bound). When present, the
327
+ * gateway computes a `contentHash` for gated writes under this profile and
328
+ * passes it (hash only) to the SP, which signs it into the receipt. Absent
329
+ * → no content hash. See {@link ContentBinding}.
330
+ */
331
+ content_binding?: ContentBinding;
332
+
298
333
  /**
299
334
  * Tool gating configuration — how MCP tools map to execution context.
300
335
  * @deprecated Tool gating now lives in integration manifests (content/integrations/*.json).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suveren/gateway",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",
@@ -107,5 +107,6 @@
107
107
  "default": 86400,
108
108
  "max": 31536000
109
109
  },
110
- "retention_minimum": 7776000
110
+ "retention_minimum": 7776000,
111
+ "content_binding": { "version": "1", "kind": "jcs" }
111
112
  }
@@ -101,5 +101,6 @@
101
101
  "default": 86400,
102
102
  "max": 31536000
103
103
  },
104
- "retention_minimum": 7776000
104
+ "retention_minimum": 7776000,
105
+ "content_binding": { "version": "1", "kind": "jcs" }
105
106
  }