@humanagencyp/hap-core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -14,7 +14,7 @@ interface ResolvedDomain {
14
14
  }
15
15
  interface AttestationPayload {
16
16
  attestation_id: string;
17
- version: '0.3' | '0.4';
17
+ version: '0.3' | '0.4' | '0.5';
18
18
  profile_id: string;
19
19
  /** v0.3 (deprecated) — hash of the authorization frame */
20
20
  frame_hash?: string;
@@ -23,8 +23,29 @@ interface AttestationPayload {
23
23
  /** v0.4 — hash of the context parameters */
24
24
  context_hash?: string;
25
25
  execution_context_hash: string;
26
- resolved_domains: ResolvedDomain[];
26
+ /**
27
+ * v0.4 (deprecated in v0.5) — domain-scoped owners. Optional so v0.5
28
+ * attestations that carry only `resolved_owners` remain valid. During the
29
+ * transition the AS emits BOTH `resolved_domains` (internal coverage) and
30
+ * `resolved_owners` (the v0.5 signed wire field).
31
+ */
32
+ resolved_domains?: ResolvedDomain[];
33
+ /**
34
+ * v0.5 — the Decision Owner DIDs this attestation covers (person-centric
35
+ * model; replaces the abstract domain in `resolved_domains`). Carried in the
36
+ * signed payload so verifiers bind the action to the human(s) who authorized it.
37
+ */
38
+ resolved_owners?: string[];
27
39
  gate_content_hashes: Record<string, string>;
40
+ /**
41
+ * v0.5 (companion spec `intent-disclosure@0.1`) — present iff the attestation
42
+ * carries an encrypted-intent disclosure object. `sha256:`-prefixed hash that
43
+ * binds the disclosure's `intent_ciphertext` + `approvers_frozen` into the
44
+ * signed payload (see {@link computeIntentDisclosureHash}), so a compromised
45
+ * AS cannot swap the ciphertext/wrapped keys or alter the approver set
46
+ * without invalidating the attestation signature (companion invariant C2).
47
+ */
48
+ intent_disclosure_hash?: string;
28
49
  /**
29
50
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
30
51
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -161,6 +182,32 @@ interface ProfileBoundsField {
161
182
  /** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
162
183
  enum?: string[];
163
184
  }
185
+ /**
186
+ * v0.5 Content Provenance — how a profile's action content is hashed into a
187
+ * signed receipt (`contentHash`). The ephemeral-content analog of Output
188
+ * Provenance: it binds the *bytes* of the action rather than a location.
189
+ *
190
+ * Profile-bound and OPTIONAL. Absent → no content hash is produced (full
191
+ * backward compatibility). The gateway computes the hash; the SP only ever
192
+ * receives the hash, never the content, so HAP's privacy-minimal design holds.
193
+ *
194
+ * The profile declares only the *policy* — whether to bind and how to
195
+ * canonicalize. It does NOT name the tool field: that is tool-specific and is
196
+ * resolved at runtime (the same content-field resolver the footer uses for
197
+ * `kind:"text"`; the whole record payload for `kind:"jcs"`).
198
+ */
199
+ interface ContentBinding {
200
+ /** Canonicalization version. A verifier MUST pin the version named here. */
201
+ version: string;
202
+ /**
203
+ * - 'jcs' → structured writes: RFC 8785 JCS over the record payload.
204
+ * - 'text' → free text: NFC + LF + trailing-whitespace strip (see
205
+ * canonicalizeText), auto-detected content field.
206
+ */
207
+ kind: 'jcs' | 'text';
208
+ /** text only: hash the content BEFORE any appended Suveren footer. */
209
+ pre_footer?: boolean;
210
+ }
164
211
  /**
165
212
  * Context field definition within a v0.4 profile.
166
213
  */
@@ -280,6 +327,13 @@ interface AgentProfile {
280
327
  max: number;
281
328
  };
282
329
  retention_minimum: number;
330
+ /**
331
+ * v0.5 Content Provenance (OPTIONAL, profile-bound). When present, the
332
+ * gateway computes a `contentHash` for gated writes under this profile and
333
+ * passes it (hash only) to the SP, which signs it into the receipt. Absent
334
+ * → no content hash. See {@link ContentBinding}.
335
+ */
336
+ content_binding?: ContentBinding;
283
337
  /**
284
338
  * Tool gating configuration — how MCP tools map to execution context.
285
339
  * @deprecated Tool gating now lives in integration manifests (content/integrations/*.json).
@@ -436,6 +490,101 @@ type GatekeeperResult = {
436
490
  */
437
491
  declare function canonicalize(value: unknown): string;
438
492
 
493
+ /**
494
+ * Content binding — Level 2 content proof (HAP v0.5 Content Provenance).
495
+ *
496
+ * A receipt normally proves who/why/bounds/when but NOT the action's content.
497
+ * Content binding closes that gap: the gateway computes a `content_hash` over
498
+ * the action's content per the profile's {@link ContentBinding} and hands the
499
+ * SP only the hash. The SP signs it into the receipt verbatim — it never sees
500
+ * the content, so HAP's privacy-minimal design is preserved. Anyone holding
501
+ * the content can recompute the hash and check it against the signed receipt.
502
+ *
503
+ * The hash only verifies if the verifier reproduces the EXACT bytes we hashed,
504
+ * so canonicalization is normative and versioned (pin via `ContentBinding.version`):
505
+ *
506
+ * - kind:"jcs" → RFC 8785 JCS of the record payload (see {@link canonicalize}).
507
+ * - kind:"text" → UTF-8 of the string after {@link canonicalizeText}
508
+ * (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
509
+ * trailing blank lines removed), taken pre-footer when `pre_footer` is set.
510
+ *
511
+ * Both Node and the browser produce byte-identical output: JCS relies only on
512
+ * environment-independent primitives, and the text rule uses String.normalize +
513
+ * plain string ops. The SHA-256 is computed with Node `crypto` here (the same
514
+ * pattern as frame.ts); browser callers that need to recompute use their own
515
+ * SubtleCrypto digest over the identical canonical bytes.
516
+ */
517
+
518
+ /**
519
+ * Canonicalize free text per the v0.5 'text' rule. Idempotent.
520
+ *
521
+ * 1. Unicode NFC normalization.
522
+ * 2. CRLF / CR → LF.
523
+ * 3. Strip trailing spaces/tabs from every line.
524
+ * 4. Remove trailing blank lines.
525
+ */
526
+ declare function canonicalizeText(input: string): string;
527
+ /**
528
+ * Compute the canonical bytes that a content hash is taken over, WITHOUT
529
+ * hashing — exposed so verifiers can debug a mismatch by inspecting the exact
530
+ * serialization both sides should agree on.
531
+ */
532
+ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Record<string, unknown> | string): string;
533
+ /**
534
+ * Compute a profile-bound content hash, formatted `sha256:<hex>` (matching the
535
+ * frame/bounds/context hash format used elsewhere in HAP).
536
+ *
537
+ * @param binding the profile's content_binding declaration
538
+ * @param content the record payload (jcs) or the resolved text field (text)
539
+ */
540
+ declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
541
+
542
+ /**
543
+ * Intent hashing + intent-disclosure binding (HAP v0.5).
544
+ *
545
+ * Two shared, environment-independent hashes that the attester (gateway), the
546
+ * Authority Server, and any approver MUST compute identically:
547
+ *
548
+ * 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
549
+ * is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
550
+ * — the same rule as content-binding's `canonicalizeText`) before hashing,
551
+ * so independent multi-owner approvers reproduce an identical hash from the
552
+ * same logical statement. See protocol.md → "Intent canonicalization".
553
+ *
554
+ * 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
555
+ * binding from companion spec `intent-disclosure@0.1`. It commits the
556
+ * encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
557
+ * into the SIGNED attestation payload, so a compromised AS cannot swap the
558
+ * ciphertext/wrapped keys or widen/shrink the approver set without breaking
559
+ * the attestation signature.
560
+ *
561
+ * Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
562
+ */
563
+ /**
564
+ * `gate_content_hashes.intent` for an intent statement.
565
+ * Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
566
+ */
567
+ declare function computeIntentHash(intentText: string): string;
568
+ /**
569
+ * The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
570
+ * exposed so an implementer debugging a mismatch can inspect the serialization
571
+ * both sides must agree on:
572
+ *
573
+ * intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
574
+ *
575
+ * where `intent_ciphertext` is the exact base64url string of the disclosure
576
+ * object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
577
+ * RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
578
+ */
579
+ declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
580
+ /**
581
+ * `intent_disclosure_hash` — `sha256:<hex>` over
582
+ * {@link intentDisclosureCanonicalBytes}. Computed by the attester after
583
+ * encryption and signed into the attestation; recomputed by the AS (at
584
+ * issuance) and by each approver (on receipt) and compared, fail-closed.
585
+ */
586
+ declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
587
+
439
588
  /**
440
589
  * Frame Canonicalization for Agent Profiles
441
590
  *
@@ -633,4 +782,4 @@ declare function listProfiles(): string[];
633
782
  declare function getAllProfiles(): AgentProfile[];
634
783
  declare function clearProfiles(): void;
635
784
 
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 };
785
+ 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, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ interface ResolvedDomain {
14
14
  }
15
15
  interface AttestationPayload {
16
16
  attestation_id: string;
17
- version: '0.3' | '0.4';
17
+ version: '0.3' | '0.4' | '0.5';
18
18
  profile_id: string;
19
19
  /** v0.3 (deprecated) — hash of the authorization frame */
20
20
  frame_hash?: string;
@@ -23,8 +23,29 @@ interface AttestationPayload {
23
23
  /** v0.4 — hash of the context parameters */
24
24
  context_hash?: string;
25
25
  execution_context_hash: string;
26
- resolved_domains: ResolvedDomain[];
26
+ /**
27
+ * v0.4 (deprecated in v0.5) — domain-scoped owners. Optional so v0.5
28
+ * attestations that carry only `resolved_owners` remain valid. During the
29
+ * transition the AS emits BOTH `resolved_domains` (internal coverage) and
30
+ * `resolved_owners` (the v0.5 signed wire field).
31
+ */
32
+ resolved_domains?: ResolvedDomain[];
33
+ /**
34
+ * v0.5 — the Decision Owner DIDs this attestation covers (person-centric
35
+ * model; replaces the abstract domain in `resolved_domains`). Carried in the
36
+ * signed payload so verifiers bind the action to the human(s) who authorized it.
37
+ */
38
+ resolved_owners?: string[];
27
39
  gate_content_hashes: Record<string, string>;
40
+ /**
41
+ * v0.5 (companion spec `intent-disclosure@0.1`) — present iff the attestation
42
+ * carries an encrypted-intent disclosure object. `sha256:`-prefixed hash that
43
+ * binds the disclosure's `intent_ciphertext` + `approvers_frozen` into the
44
+ * signed payload (see {@link computeIntentDisclosureHash}), so a compromised
45
+ * AS cannot swap the ciphertext/wrapped keys or alter the approver set
46
+ * without invalidating the attestation signature (companion invariant C2).
47
+ */
48
+ intent_disclosure_hash?: string;
28
49
  /**
29
50
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
30
51
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -161,6 +182,32 @@ interface ProfileBoundsField {
161
182
  /** @deprecated v0.4: use boundType: { kind: 'enum', values: [...] }. */
162
183
  enum?: string[];
163
184
  }
185
+ /**
186
+ * v0.5 Content Provenance — how a profile's action content is hashed into a
187
+ * signed receipt (`contentHash`). The ephemeral-content analog of Output
188
+ * Provenance: it binds the *bytes* of the action rather than a location.
189
+ *
190
+ * Profile-bound and OPTIONAL. Absent → no content hash is produced (full
191
+ * backward compatibility). The gateway computes the hash; the SP only ever
192
+ * receives the hash, never the content, so HAP's privacy-minimal design holds.
193
+ *
194
+ * The profile declares only the *policy* — whether to bind and how to
195
+ * canonicalize. It does NOT name the tool field: that is tool-specific and is
196
+ * resolved at runtime (the same content-field resolver the footer uses for
197
+ * `kind:"text"`; the whole record payload for `kind:"jcs"`).
198
+ */
199
+ interface ContentBinding {
200
+ /** Canonicalization version. A verifier MUST pin the version named here. */
201
+ version: string;
202
+ /**
203
+ * - 'jcs' → structured writes: RFC 8785 JCS over the record payload.
204
+ * - 'text' → free text: NFC + LF + trailing-whitespace strip (see
205
+ * canonicalizeText), auto-detected content field.
206
+ */
207
+ kind: 'jcs' | 'text';
208
+ /** text only: hash the content BEFORE any appended Suveren footer. */
209
+ pre_footer?: boolean;
210
+ }
164
211
  /**
165
212
  * Context field definition within a v0.4 profile.
166
213
  */
@@ -280,6 +327,13 @@ interface AgentProfile {
280
327
  max: number;
281
328
  };
282
329
  retention_minimum: number;
330
+ /**
331
+ * v0.5 Content Provenance (OPTIONAL, profile-bound). When present, the
332
+ * gateway computes a `contentHash` for gated writes under this profile and
333
+ * passes it (hash only) to the SP, which signs it into the receipt. Absent
334
+ * → no content hash. See {@link ContentBinding}.
335
+ */
336
+ content_binding?: ContentBinding;
283
337
  /**
284
338
  * Tool gating configuration — how MCP tools map to execution context.
285
339
  * @deprecated Tool gating now lives in integration manifests (content/integrations/*.json).
@@ -436,6 +490,101 @@ type GatekeeperResult = {
436
490
  */
437
491
  declare function canonicalize(value: unknown): string;
438
492
 
493
+ /**
494
+ * Content binding — Level 2 content proof (HAP v0.5 Content Provenance).
495
+ *
496
+ * A receipt normally proves who/why/bounds/when but NOT the action's content.
497
+ * Content binding closes that gap: the gateway computes a `content_hash` over
498
+ * the action's content per the profile's {@link ContentBinding} and hands the
499
+ * SP only the hash. The SP signs it into the receipt verbatim — it never sees
500
+ * the content, so HAP's privacy-minimal design is preserved. Anyone holding
501
+ * the content can recompute the hash and check it against the signed receipt.
502
+ *
503
+ * The hash only verifies if the verifier reproduces the EXACT bytes we hashed,
504
+ * so canonicalization is normative and versioned (pin via `ContentBinding.version`):
505
+ *
506
+ * - kind:"jcs" → RFC 8785 JCS of the record payload (see {@link canonicalize}).
507
+ * - kind:"text" → UTF-8 of the string after {@link canonicalizeText}
508
+ * (Unicode NFC, LF line endings, trailing per-line whitespace stripped,
509
+ * trailing blank lines removed), taken pre-footer when `pre_footer` is set.
510
+ *
511
+ * Both Node and the browser produce byte-identical output: JCS relies only on
512
+ * environment-independent primitives, and the text rule uses String.normalize +
513
+ * plain string ops. The SHA-256 is computed with Node `crypto` here (the same
514
+ * pattern as frame.ts); browser callers that need to recompute use their own
515
+ * SubtleCrypto digest over the identical canonical bytes.
516
+ */
517
+
518
+ /**
519
+ * Canonicalize free text per the v0.5 'text' rule. Idempotent.
520
+ *
521
+ * 1. Unicode NFC normalization.
522
+ * 2. CRLF / CR → LF.
523
+ * 3. Strip trailing spaces/tabs from every line.
524
+ * 4. Remove trailing blank lines.
525
+ */
526
+ declare function canonicalizeText(input: string): string;
527
+ /**
528
+ * Compute the canonical bytes that a content hash is taken over, WITHOUT
529
+ * hashing — exposed so verifiers can debug a mismatch by inspecting the exact
530
+ * serialization both sides should agree on.
531
+ */
532
+ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Record<string, unknown> | string): string;
533
+ /**
534
+ * Compute a profile-bound content hash, formatted `sha256:<hex>` (matching the
535
+ * frame/bounds/context hash format used elsewhere in HAP).
536
+ *
537
+ * @param binding the profile's content_binding declaration
538
+ * @param content the record payload (jcs) or the resolved text field (text)
539
+ */
540
+ declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
541
+
542
+ /**
543
+ * Intent hashing + intent-disclosure binding (HAP v0.5).
544
+ *
545
+ * Two shared, environment-independent hashes that the attester (gateway), the
546
+ * Authority Server, and any approver MUST compute identically:
547
+ *
548
+ * 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
549
+ * is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
550
+ * — the same rule as content-binding's `canonicalizeText`) before hashing,
551
+ * so independent multi-owner approvers reproduce an identical hash from the
552
+ * same logical statement. See protocol.md → "Intent canonicalization".
553
+ *
554
+ * 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
555
+ * binding from companion spec `intent-disclosure@0.1`. It commits the
556
+ * encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
557
+ * into the SIGNED attestation payload, so a compromised AS cannot swap the
558
+ * ciphertext/wrapped keys or widen/shrink the approver set without breaking
559
+ * the attestation signature.
560
+ *
561
+ * Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
562
+ */
563
+ /**
564
+ * `gate_content_hashes.intent` for an intent statement.
565
+ * Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
566
+ */
567
+ declare function computeIntentHash(intentText: string): string;
568
+ /**
569
+ * The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
570
+ * exposed so an implementer debugging a mismatch can inspect the serialization
571
+ * both sides must agree on:
572
+ *
573
+ * intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
574
+ *
575
+ * where `intent_ciphertext` is the exact base64url string of the disclosure
576
+ * object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
577
+ * RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
578
+ */
579
+ declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
580
+ /**
581
+ * `intent_disclosure_hash` — `sha256:<hex>` over
582
+ * {@link intentDisclosureCanonicalBytes}. Computed by the attester after
583
+ * encryption and signed into the attestation; recomputed by the AS (at
584
+ * issuance) and by each approver (on receipt) and compared, fail-closed.
585
+ */
586
+ declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
587
+
439
588
  /**
440
589
  * Frame Canonicalization for Agent Profiles
441
590
  *
@@ -633,4 +782,4 @@ declare function listProfiles(): string[];
633
782
  declare function getAllProfiles(): AgentProfile[];
634
783
  declare function clearProfiles(): void;
635
784
 
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 };
785
+ 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, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
package/dist/index.js CHANGED
@@ -35,16 +35,22 @@ __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
+ computeIntentDisclosureHash: () => computeIntentDisclosureHash,
46
+ computeIntentHash: () => computeIntentHash,
47
+ contentCanonicalBytes: () => contentCanonicalBytes,
43
48
  decodeAttestationBlob: () => decodeAttestationBlob,
44
49
  encodeAttestationBlob: () => encodeAttestationBlob,
45
50
  frameHash: () => frameHash,
46
51
  getAllProfiles: () => getAllProfiles,
47
52
  getProfile: () => getProfile,
53
+ intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
48
54
  isV4Attestation: () => isV4Attestation,
49
55
  listProfiles: () => listProfiles,
50
56
  registerProfile: () => registerProfile,
@@ -86,8 +92,52 @@ function canonicalize(value) {
86
92
  return "{" + parts.join(",") + "}";
87
93
  }
88
94
 
89
- // src/frame.ts
95
+ // src/content-binding.ts
90
96
  var import_crypto = require("crypto");
97
+ function canonicalizeText(input) {
98
+ const nfc = input.normalize("NFC");
99
+ const lf = nfc.replace(/\r\n?/g, "\n");
100
+ const lines = lf.split("\n").map((line) => line.replace(/[ \t]+$/, ""));
101
+ return lines.join("\n").replace(/\n+$/, "");
102
+ }
103
+ function sha256Hex(bytes) {
104
+ return (0, import_crypto.createHash)("sha256").update(bytes, "utf8").digest("hex");
105
+ }
106
+ function contentCanonicalBytes(kind, content) {
107
+ if (kind === "jcs") {
108
+ if (typeof content === "string") {
109
+ throw new Error('content_binding kind="jcs" expects a record payload (object), got a string');
110
+ }
111
+ return canonicalize(content);
112
+ }
113
+ if (typeof content !== "string") {
114
+ throw new Error('content_binding kind="text" expects a string, got an object');
115
+ }
116
+ return canonicalizeText(content);
117
+ }
118
+ function computeContentHash(binding, content) {
119
+ return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
120
+ }
121
+
122
+ // src/intent-disclosure.ts
123
+ var import_crypto2 = require("crypto");
124
+ function sha256Hex2(bytes) {
125
+ return (0, import_crypto2.createHash)("sha256").update(bytes, "utf8").digest("hex");
126
+ }
127
+ function computeIntentHash(intentText) {
128
+ return `sha256:${sha256Hex2(canonicalizeText(intentText))}`;
129
+ }
130
+ function intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen) {
131
+ const sorted = [...approversFrozen].sort();
132
+ return `${intentCiphertext}
133
+ ${canonicalize(sorted)}`;
134
+ }
135
+ function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
136
+ return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
137
+ }
138
+
139
+ // src/frame.ts
140
+ var import_crypto3 = require("crypto");
91
141
  function validateFrameParams(params, profile) {
92
142
  const errors = [];
93
143
  if (!profile.frameSchema) {
@@ -124,7 +174,7 @@ function canonicalFrame(params, profile) {
124
174
  return lines.join("\n");
125
175
  }
126
176
  function frameHash(canonicalFrameString) {
127
- const hash = (0, import_crypto.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
177
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
128
178
  return `sha256:${hash}`;
129
179
  }
130
180
  function computeFrameHash(params, profile) {
@@ -208,17 +258,17 @@ function canonicalContext(params, profile) {
208
258
  }
209
259
  function computeBoundsHash(params, profile) {
210
260
  const canonical = canonicalBounds(params, profile);
211
- const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
261
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
212
262
  return `sha256:${hash}`;
213
263
  }
214
264
  function computeContextHash(params, profile) {
215
265
  const canonical = canonicalContext(params, profile);
216
- const hash = (0, import_crypto.createHash)("sha256").update(canonical, "utf8").digest("hex");
266
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
217
267
  return `sha256:${hash}`;
218
268
  }
219
269
 
220
270
  // src/attestation.ts
221
- var import_crypto2 = require("crypto");
271
+ var import_crypto4 = require("crypto");
222
272
  var ed = __toESM(require("@noble/ed25519"));
223
273
  function decodeAttestationBlob(blob) {
224
274
  try {
@@ -236,7 +286,7 @@ function encodeAttestationBlob(attestation) {
236
286
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
237
287
  }
238
288
  function attestationId(blob) {
239
- const hash = (0, import_crypto2.createHash)("sha256").update(blob, "utf8").digest("hex");
289
+ const hash = (0, import_crypto4.createHash)("sha256").update(blob, "utf8").digest("hex");
240
290
  return `sha256:${hash}`;
241
291
  }
242
292
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -366,11 +416,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
366
416
  try {
367
417
  checkAttestationExpiry(attestation.payload, now);
368
418
  } catch {
369
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
419
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
370
420
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
371
421
  continue;
372
422
  }
373
- for (const rd of attestation.payload.resolved_domains) {
423
+ for (const rd of attestation.payload.resolved_domains ?? []) {
374
424
  coveredDomains.add(rd.domain);
375
425
  }
376
426
  }
@@ -445,11 +495,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
445
495
  try {
446
496
  checkAttestationExpiry(attestation.payload, now);
447
497
  } catch {
448
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
498
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
449
499
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
450
500
  continue;
451
501
  }
452
- for (const rd of attestation.payload.resolved_domains) {
502
+ for (const rd of attestation.payload.resolved_domains ?? []) {
453
503
  coveredDomains.add(rd.domain);
454
504
  }
455
505
  }
@@ -746,16 +796,22 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
746
796
  canonicalContext,
747
797
  canonicalFrame,
748
798
  canonicalize,
799
+ canonicalizeText,
749
800
  checkAttestationExpiry,
750
801
  clearProfiles,
751
802
  computeBoundsHash,
803
+ computeContentHash,
752
804
  computeContextHash,
753
805
  computeFrameHash,
806
+ computeIntentDisclosureHash,
807
+ computeIntentHash,
808
+ contentCanonicalBytes,
754
809
  decodeAttestationBlob,
755
810
  encodeAttestationBlob,
756
811
  frameHash,
757
812
  getAllProfiles,
758
813
  getProfile,
814
+ intentDisclosureCanonicalBytes,
759
815
  isV4Attestation,
760
816
  listProfiles,
761
817
  registerProfile,
package/dist/index.mjs CHANGED
@@ -23,8 +23,52 @@ 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/intent-disclosure.ts
54
+ import { createHash as createHash2 } from "crypto";
55
+ function sha256Hex2(bytes) {
56
+ return createHash2("sha256").update(bytes, "utf8").digest("hex");
57
+ }
58
+ function computeIntentHash(intentText) {
59
+ return `sha256:${sha256Hex2(canonicalizeText(intentText))}`;
60
+ }
61
+ function intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen) {
62
+ const sorted = [...approversFrozen].sort();
63
+ return `${intentCiphertext}
64
+ ${canonicalize(sorted)}`;
65
+ }
66
+ function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
67
+ return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
68
+ }
69
+
70
+ // src/frame.ts
71
+ import { createHash as createHash3 } from "crypto";
28
72
  function validateFrameParams(params, profile) {
29
73
  const errors = [];
30
74
  if (!profile.frameSchema) {
@@ -61,7 +105,7 @@ function canonicalFrame(params, profile) {
61
105
  return lines.join("\n");
62
106
  }
63
107
  function frameHash(canonicalFrameString) {
64
- const hash = createHash("sha256").update(canonicalFrameString, "utf8").digest("hex");
108
+ const hash = createHash3("sha256").update(canonicalFrameString, "utf8").digest("hex");
65
109
  return `sha256:${hash}`;
66
110
  }
67
111
  function computeFrameHash(params, profile) {
@@ -145,17 +189,17 @@ function canonicalContext(params, profile) {
145
189
  }
146
190
  function computeBoundsHash(params, profile) {
147
191
  const canonical = canonicalBounds(params, profile);
148
- const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
192
+ const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
149
193
  return `sha256:${hash}`;
150
194
  }
151
195
  function computeContextHash(params, profile) {
152
196
  const canonical = canonicalContext(params, profile);
153
- const hash = createHash("sha256").update(canonical, "utf8").digest("hex");
197
+ const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
154
198
  return `sha256:${hash}`;
155
199
  }
156
200
 
157
201
  // src/attestation.ts
158
- import { createHash as createHash2 } from "crypto";
202
+ import { createHash as createHash4 } from "crypto";
159
203
  import * as ed from "@noble/ed25519";
160
204
  function decodeAttestationBlob(blob) {
161
205
  try {
@@ -173,7 +217,7 @@ function encodeAttestationBlob(attestation) {
173
217
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
174
218
  }
175
219
  function attestationId(blob) {
176
- const hash = createHash2("sha256").update(blob, "utf8").digest("hex");
220
+ const hash = createHash4("sha256").update(blob, "utf8").digest("hex");
177
221
  return `sha256:${hash}`;
178
222
  }
179
223
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -303,11 +347,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
303
347
  try {
304
348
  checkAttestationExpiry(attestation.payload, now);
305
349
  } catch {
306
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
350
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
307
351
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
308
352
  continue;
309
353
  }
310
- for (const rd of attestation.payload.resolved_domains) {
354
+ for (const rd of attestation.payload.resolved_domains ?? []) {
311
355
  coveredDomains.add(rd.domain);
312
356
  }
313
357
  }
@@ -382,11 +426,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
382
426
  try {
383
427
  checkAttestationExpiry(attestation.payload, now);
384
428
  } catch {
385
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
429
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
386
430
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
387
431
  continue;
388
432
  }
389
- for (const rd of attestation.payload.resolved_domains) {
433
+ for (const rd of attestation.payload.resolved_domains ?? []) {
390
434
  coveredDomains.add(rd.domain);
391
435
  }
392
436
  }
@@ -682,16 +726,22 @@ export {
682
726
  canonicalContext,
683
727
  canonicalFrame,
684
728
  canonicalize,
729
+ canonicalizeText,
685
730
  checkAttestationExpiry,
686
731
  clearProfiles,
687
732
  computeBoundsHash,
733
+ computeContentHash,
688
734
  computeContextHash,
689
735
  computeFrameHash,
736
+ computeIntentDisclosureHash,
737
+ computeIntentHash,
738
+ contentCanonicalBytes,
690
739
  decodeAttestationBlob,
691
740
  encodeAttestationBlob,
692
741
  frameHash,
693
742
  getAllProfiles,
694
743
  getProfile,
744
+ intentDisclosureCanonicalBytes,
695
745
  isV4Attestation,
696
746
  listProfiles,
697
747
  registerProfile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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
+ }
package/src/gatekeeper.ts CHANGED
@@ -131,12 +131,12 @@ async function verifyV3(
131
131
  try {
132
132
  checkAttestationExpiry(attestation.payload, now);
133
133
  } catch {
134
- const domainNames = attestation.payload.resolved_domains.map(d => d.domain).join(', ');
134
+ const domainNames = (attestation.payload.resolved_domains ?? []).map(d => d.domain).join(', ');
135
135
  errors.push({ code: 'TTL_EXPIRED', message: `Attestation for domain "${domainNames}" has expired` });
136
136
  continue;
137
137
  }
138
138
 
139
- for (const rd of attestation.payload.resolved_domains) {
139
+ for (const rd of attestation.payload.resolved_domains ?? []) {
140
140
  coveredDomains.add(rd.domain);
141
141
  }
142
142
  }
@@ -244,12 +244,12 @@ async function verifyV4(
244
244
  try {
245
245
  checkAttestationExpiry(attestation.payload, now);
246
246
  } catch {
247
- const domainNames = attestation.payload.resolved_domains.map(d => d.domain).join(', ');
247
+ const domainNames = (attestation.payload.resolved_domains ?? []).map(d => d.domain).join(', ');
248
248
  errors.push({ code: 'TTL_EXPIRED', message: `Attestation for domain "${domainNames}" has expired` });
249
249
  continue;
250
250
  }
251
251
 
252
- for (const rd of attestation.payload.resolved_domains) {
252
+ for (const rd of attestation.payload.resolved_domains ?? []) {
253
253
  coveredDomains.add(rd.domain);
254
254
  }
255
255
  }
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  export * from './types';
8
8
  export * from './canonicalize';
9
+ export * from './content-binding';
10
+ export * from './intent-disclosure';
9
11
  export * from './frame';
10
12
  export * from './attestation';
11
13
  export * from './gatekeeper';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Intent hashing + intent-disclosure binding (HAP v0.5).
3
+ *
4
+ * Two shared, environment-independent hashes that the attester (gateway), the
5
+ * Authority Server, and any approver MUST compute identically:
6
+ *
7
+ * 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
8
+ * is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
9
+ * — the same rule as content-binding's `canonicalizeText`) before hashing,
10
+ * so independent multi-owner approvers reproduce an identical hash from the
11
+ * same logical statement. See protocol.md → "Intent canonicalization".
12
+ *
13
+ * 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
14
+ * binding from companion spec `intent-disclosure@0.1`. It commits the
15
+ * encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
16
+ * into the SIGNED attestation payload, so a compromised AS cannot swap the
17
+ * ciphertext/wrapped keys or widen/shrink the approver set without breaking
18
+ * the attestation signature.
19
+ *
20
+ * Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
21
+ */
22
+
23
+ import { createHash } from 'crypto';
24
+ import { canonicalize } from './canonicalize';
25
+ import { canonicalizeText } from './content-binding';
26
+
27
+ function sha256Hex(bytes: string): string {
28
+ return createHash('sha256').update(bytes, 'utf8').digest('hex');
29
+ }
30
+
31
+ /**
32
+ * `gate_content_hashes.intent` for an intent statement.
33
+ * Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
34
+ */
35
+ export function computeIntentHash(intentText: string): string {
36
+ return `sha256:${sha256Hex(canonicalizeText(intentText))}`;
37
+ }
38
+
39
+ /**
40
+ * The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
41
+ * exposed so an implementer debugging a mismatch can inspect the serialization
42
+ * both sides must agree on:
43
+ *
44
+ * intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
45
+ *
46
+ * where `intent_ciphertext` is the exact base64url string of the disclosure
47
+ * object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
48
+ * RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
49
+ */
50
+ export function intentDisclosureCanonicalBytes(
51
+ intentCiphertext: string,
52
+ approversFrozen: string[],
53
+ ): string {
54
+ const sorted = [...approversFrozen].sort();
55
+ return `${intentCiphertext}\n${canonicalize(sorted)}`;
56
+ }
57
+
58
+ /**
59
+ * `intent_disclosure_hash` — `sha256:<hex>` over
60
+ * {@link intentDisclosureCanonicalBytes}. Computed by the attester after
61
+ * encryption and signed into the attestation; recomputed by the AS (at
62
+ * issuance) and by each approver (on receipt) and compared, fail-closed.
63
+ */
64
+ export function computeIntentDisclosureHash(
65
+ intentCiphertext: string,
66
+ approversFrozen: string[],
67
+ ): string {
68
+ return `sha256:${sha256Hex(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
69
+ }
package/src/types.ts CHANGED
@@ -19,7 +19,7 @@ export interface ResolvedDomain {
19
19
 
20
20
  export interface AttestationPayload {
21
21
  attestation_id: string;
22
- version: '0.3' | '0.4';
22
+ version: '0.3' | '0.4' | '0.5';
23
23
  profile_id: string;
24
24
  /** v0.3 (deprecated) — hash of the authorization frame */
25
25
  frame_hash?: string;
@@ -28,8 +28,29 @@ export interface AttestationPayload {
28
28
  /** v0.4 — hash of the context parameters */
29
29
  context_hash?: string;
30
30
  execution_context_hash: string;
31
- resolved_domains: ResolvedDomain[];
31
+ /**
32
+ * v0.4 (deprecated in v0.5) — domain-scoped owners. Optional so v0.5
33
+ * attestations that carry only `resolved_owners` remain valid. During the
34
+ * transition the AS emits BOTH `resolved_domains` (internal coverage) and
35
+ * `resolved_owners` (the v0.5 signed wire field).
36
+ */
37
+ resolved_domains?: ResolvedDomain[];
38
+ /**
39
+ * v0.5 — the Decision Owner DIDs this attestation covers (person-centric
40
+ * model; replaces the abstract domain in `resolved_domains`). Carried in the
41
+ * signed payload so verifiers bind the action to the human(s) who authorized it.
42
+ */
43
+ resolved_owners?: string[];
32
44
  gate_content_hashes: Record<string, string>;
45
+ /**
46
+ * v0.5 (companion spec `intent-disclosure@0.1`) — present iff the attestation
47
+ * carries an encrypted-intent disclosure object. `sha256:`-prefixed hash that
48
+ * binds the disclosure's `intent_ciphertext` + `approvers_frozen` into the
49
+ * signed payload (see {@link computeIntentDisclosureHash}), so a compromised
50
+ * AS cannot swap the ciphertext/wrapped keys or alter the approver set
51
+ * without invalidating the attestation signature (companion invariant C2).
52
+ */
53
+ intent_disclosure_hash?: string;
33
54
  /**
34
55
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
35
56
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -166,6 +187,33 @@ export interface ProfileBoundsField {
166
187
  enum?: string[];
167
188
  }
168
189
 
190
+ /**
191
+ * v0.5 Content Provenance — how a profile's action content is hashed into a
192
+ * signed receipt (`contentHash`). The ephemeral-content analog of Output
193
+ * Provenance: it binds the *bytes* of the action rather than a location.
194
+ *
195
+ * Profile-bound and OPTIONAL. Absent → no content hash is produced (full
196
+ * backward compatibility). The gateway computes the hash; the SP only ever
197
+ * receives the hash, never the content, so HAP's privacy-minimal design holds.
198
+ *
199
+ * The profile declares only the *policy* — whether to bind and how to
200
+ * canonicalize. It does NOT name the tool field: that is tool-specific and is
201
+ * resolved at runtime (the same content-field resolver the footer uses for
202
+ * `kind:"text"`; the whole record payload for `kind:"jcs"`).
203
+ */
204
+ export interface ContentBinding {
205
+ /** Canonicalization version. A verifier MUST pin the version named here. */
206
+ version: string;
207
+ /**
208
+ * - 'jcs' → structured writes: RFC 8785 JCS over the record payload.
209
+ * - 'text' → free text: NFC + LF + trailing-whitespace strip (see
210
+ * canonicalizeText), auto-detected content field.
211
+ */
212
+ kind: 'jcs' | 'text';
213
+ /** text only: hash the content BEFORE any appended Suveren footer. */
214
+ pre_footer?: boolean;
215
+ }
216
+
169
217
  /**
170
218
  * Context field definition within a v0.4 profile.
171
219
  */
@@ -295,6 +343,14 @@ export interface AgentProfile {
295
343
  ttl: { default: number; max: number };
296
344
  retention_minimum: number;
297
345
 
346
+ /**
347
+ * v0.5 Content Provenance (OPTIONAL, profile-bound). When present, the
348
+ * gateway computes a `contentHash` for gated writes under this profile and
349
+ * passes it (hash only) to the SP, which signs it into the receipt. Absent
350
+ * → no content hash. See {@link ContentBinding}.
351
+ */
352
+ content_binding?: ContentBinding;
353
+
298
354
  /**
299
355
  * Tool gating configuration — how MCP tools map to execution context.
300
356
  * @deprecated Tool gating now lives in integration manifests (content/integrations/*.json).