@humanagencyp/hap-core 0.5.1 → 0.6.0

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,37 @@ 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;
49
+ /**
50
+ * v0.6 (Identity Assurance) — optional signed overlay carrying the verified
51
+ * real-world identity of the Decision Owner(s), one entry per owner. Present
52
+ * only when identity is disclosed; an attestation with no `subjects` renders
53
+ * as `low` (pseudonymous, no name). See {@link Subject} and
54
+ * {@link deriveIdentityLine}.
55
+ */
56
+ subjects?: Subject[];
28
57
  /**
29
58
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
30
59
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -38,6 +67,38 @@ interface AttestationPayload {
38
67
  issued_at: number;
39
68
  expires_at: number;
40
69
  }
70
+ /**
71
+ * v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
72
+ * real-world identity to the attestation, gated by HOW the identity was verified.
73
+ *
74
+ * Two display levels (`assurance`): `low` discloses no name; `high` MAY disclose a
75
+ * name. At `high`, two trust roots: `as` (the AS operator vouches — valid only
76
+ * within its own domain) and `external` (an external eID such as EUDI — carries the
77
+ * owner's own signature, AS-independent). See review.md → "Identity Assurance".
78
+ */
79
+ interface Subject {
80
+ /** The Decision Owner DID this subject describes (matches an entry in resolved_owners). */
81
+ did: string;
82
+ /** `low` → no name shown; `high` → the name MAY be shown. */
83
+ assurance: 'low' | 'high';
84
+ /** How identity was established. */
85
+ method: 'self_declared' | 'as_vouched' | 'eudi';
86
+ /** Who vouches: `self` (owner's claim), `as` (operator), `external` (eID scheme). */
87
+ trust_root: 'self' | 'as' | 'external';
88
+ /** Verifier id — the AS operator (as_vouched) or the eID scheme (eudi). Required at `high`. */
89
+ verifier?: string;
90
+ /** Disclosed attributes. `name` present ONLY at `assurance:"high"` and when disclosure is on. */
91
+ disclose?: {
92
+ name: string;
93
+ };
94
+ /** When the underlying verification was performed (unix seconds). */
95
+ verified_at?: number;
96
+ /**
97
+ * eudi only (Phase 2) — the owner's per-event wallet signature, making the
98
+ * identity claim non-repudiable independent of the AS. Null/absent otherwise.
99
+ */
100
+ owner_signature?: string | null;
101
+ }
41
102
  interface Attestation {
42
103
  header: AttestationHeader;
43
104
  payload: AttestationPayload;
@@ -518,6 +579,89 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
518
579
  */
519
580
  declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
520
581
 
582
+ /**
583
+ * Intent hashing + intent-disclosure binding (HAP v0.5).
584
+ *
585
+ * Two shared, environment-independent hashes that the attester (gateway), the
586
+ * Authority Server, and any approver MUST compute identically:
587
+ *
588
+ * 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
589
+ * is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
590
+ * — the same rule as content-binding's `canonicalizeText`) before hashing,
591
+ * so independent multi-owner approvers reproduce an identical hash from the
592
+ * same logical statement. See protocol.md → "Intent canonicalization".
593
+ *
594
+ * 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
595
+ * binding from companion spec `intent-disclosure@0.1`. It commits the
596
+ * encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
597
+ * into the SIGNED attestation payload, so a compromised AS cannot swap the
598
+ * ciphertext/wrapped keys or widen/shrink the approver set without breaking
599
+ * the attestation signature.
600
+ *
601
+ * Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
602
+ */
603
+ /**
604
+ * `gate_content_hashes.intent` for an intent statement.
605
+ * Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
606
+ */
607
+ declare function computeIntentHash(intentText: string): string;
608
+ /**
609
+ * The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
610
+ * exposed so an implementer debugging a mismatch can inspect the serialization
611
+ * both sides must agree on:
612
+ *
613
+ * intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
614
+ *
615
+ * where `intent_ciphertext` is the exact base64url string of the disclosure
616
+ * object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
617
+ * RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
618
+ */
619
+ declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
620
+ /**
621
+ * `intent_disclosure_hash` — `sha256:<hex>` over
622
+ * {@link intentDisclosureCanonicalBytes}. Computed by the attester after
623
+ * encryption and signed into the attestation; recomputed by the AS (at
624
+ * issuance) and by each approver (on receipt) and compared, fail-closed.
625
+ */
626
+ declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
627
+
628
+ /**
629
+ * Identity Assurance (HAP v0.6) — validation + footer derivation for the signed
630
+ * {@link Subject} overlay. See review.md → "Identity Assurance".
631
+ *
632
+ * Two pure helpers, the single source of truth for both the gateway footer and
633
+ * the AS verify page:
634
+ * - {@link validateSubject} — enforces the level/method/trust-root invariants.
635
+ * - {@link deriveIdentityLine} — the human-readable footer line for a subject.
636
+ */
637
+
638
+ interface SubjectValidation {
639
+ valid: boolean;
640
+ errors: string[];
641
+ }
642
+ /**
643
+ * Enforce the Identity-Assurance invariants on a single subject:
644
+ * - `self_declared` ⇒ `low` / `self`, no disclosed name.
645
+ * - `as_vouched` ⇒ `high` / `as`, `verifier` required.
646
+ * - `eudi` ⇒ `high` / `external`, `verifier` + `owner_signature` required.
647
+ * - `disclose.name` only at `assurance:"high"`.
648
+ */
649
+ declare function validateSubject(subject: Subject): SubjectValidation;
650
+ /**
651
+ * The footer line for a subject — the ONE place this wording lives.
652
+ * The name appears only at `high` with a disclosed name; otherwise no name.
653
+ *
654
+ * - low / no disclosure → "Sent by an AI agent via «operator»"
655
+ * - high / as_vouched → "Sent by an AI agent of «name» — verified by «operator»"
656
+ * - high / eudi → "Sent by an AI agent of «name» — identity verified (EUDI)"
657
+ *
658
+ * `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
659
+ * upstream; a different operator self-vouches under its own name.
660
+ */
661
+ declare function deriveIdentityLine(subject: Subject | undefined | null, { operatorName }: {
662
+ operatorName: string;
663
+ }): string;
664
+
521
665
  /**
522
666
  * Frame Canonicalization for Agent Profiles
523
667
  *
@@ -715,4 +859,4 @@ declare function listProfiles(): string[];
715
859
  declare function getAllProfiles(): AgentProfile[];
716
860
  declare function clearProfiles(): void;
717
861
 
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 };
862
+ 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, type Subject, type SubjectValidation, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, 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,37 @@ 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;
49
+ /**
50
+ * v0.6 (Identity Assurance) — optional signed overlay carrying the verified
51
+ * real-world identity of the Decision Owner(s), one entry per owner. Present
52
+ * only when identity is disclosed; an attestation with no `subjects` renders
53
+ * as `low` (pseudonymous, no name). See {@link Subject} and
54
+ * {@link deriveIdentityLine}.
55
+ */
56
+ subjects?: Subject[];
28
57
  /**
29
58
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
30
59
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -38,6 +67,38 @@ interface AttestationPayload {
38
67
  issued_at: number;
39
68
  expires_at: number;
40
69
  }
70
+ /**
71
+ * v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
72
+ * real-world identity to the attestation, gated by HOW the identity was verified.
73
+ *
74
+ * Two display levels (`assurance`): `low` discloses no name; `high` MAY disclose a
75
+ * name. At `high`, two trust roots: `as` (the AS operator vouches — valid only
76
+ * within its own domain) and `external` (an external eID such as EUDI — carries the
77
+ * owner's own signature, AS-independent). See review.md → "Identity Assurance".
78
+ */
79
+ interface Subject {
80
+ /** The Decision Owner DID this subject describes (matches an entry in resolved_owners). */
81
+ did: string;
82
+ /** `low` → no name shown; `high` → the name MAY be shown. */
83
+ assurance: 'low' | 'high';
84
+ /** How identity was established. */
85
+ method: 'self_declared' | 'as_vouched' | 'eudi';
86
+ /** Who vouches: `self` (owner's claim), `as` (operator), `external` (eID scheme). */
87
+ trust_root: 'self' | 'as' | 'external';
88
+ /** Verifier id — the AS operator (as_vouched) or the eID scheme (eudi). Required at `high`. */
89
+ verifier?: string;
90
+ /** Disclosed attributes. `name` present ONLY at `assurance:"high"` and when disclosure is on. */
91
+ disclose?: {
92
+ name: string;
93
+ };
94
+ /** When the underlying verification was performed (unix seconds). */
95
+ verified_at?: number;
96
+ /**
97
+ * eudi only (Phase 2) — the owner's per-event wallet signature, making the
98
+ * identity claim non-repudiable independent of the AS. Null/absent otherwise.
99
+ */
100
+ owner_signature?: string | null;
101
+ }
41
102
  interface Attestation {
42
103
  header: AttestationHeader;
43
104
  payload: AttestationPayload;
@@ -518,6 +579,89 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
518
579
  */
519
580
  declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
520
581
 
582
+ /**
583
+ * Intent hashing + intent-disclosure binding (HAP v0.5).
584
+ *
585
+ * Two shared, environment-independent hashes that the attester (gateway), the
586
+ * Authority Server, and any approver MUST compute identically:
587
+ *
588
+ * 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
589
+ * is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
590
+ * — the same rule as content-binding's `canonicalizeText`) before hashing,
591
+ * so independent multi-owner approvers reproduce an identical hash from the
592
+ * same logical statement. See protocol.md → "Intent canonicalization".
593
+ *
594
+ * 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
595
+ * binding from companion spec `intent-disclosure@0.1`. It commits the
596
+ * encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
597
+ * into the SIGNED attestation payload, so a compromised AS cannot swap the
598
+ * ciphertext/wrapped keys or widen/shrink the approver set without breaking
599
+ * the attestation signature.
600
+ *
601
+ * Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
602
+ */
603
+ /**
604
+ * `gate_content_hashes.intent` for an intent statement.
605
+ * Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
606
+ */
607
+ declare function computeIntentHash(intentText: string): string;
608
+ /**
609
+ * The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
610
+ * exposed so an implementer debugging a mismatch can inspect the serialization
611
+ * both sides must agree on:
612
+ *
613
+ * intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
614
+ *
615
+ * where `intent_ciphertext` is the exact base64url string of the disclosure
616
+ * object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
617
+ * RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
618
+ */
619
+ declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
620
+ /**
621
+ * `intent_disclosure_hash` — `sha256:<hex>` over
622
+ * {@link intentDisclosureCanonicalBytes}. Computed by the attester after
623
+ * encryption and signed into the attestation; recomputed by the AS (at
624
+ * issuance) and by each approver (on receipt) and compared, fail-closed.
625
+ */
626
+ declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
627
+
628
+ /**
629
+ * Identity Assurance (HAP v0.6) — validation + footer derivation for the signed
630
+ * {@link Subject} overlay. See review.md → "Identity Assurance".
631
+ *
632
+ * Two pure helpers, the single source of truth for both the gateway footer and
633
+ * the AS verify page:
634
+ * - {@link validateSubject} — enforces the level/method/trust-root invariants.
635
+ * - {@link deriveIdentityLine} — the human-readable footer line for a subject.
636
+ */
637
+
638
+ interface SubjectValidation {
639
+ valid: boolean;
640
+ errors: string[];
641
+ }
642
+ /**
643
+ * Enforce the Identity-Assurance invariants on a single subject:
644
+ * - `self_declared` ⇒ `low` / `self`, no disclosed name.
645
+ * - `as_vouched` ⇒ `high` / `as`, `verifier` required.
646
+ * - `eudi` ⇒ `high` / `external`, `verifier` + `owner_signature` required.
647
+ * - `disclose.name` only at `assurance:"high"`.
648
+ */
649
+ declare function validateSubject(subject: Subject): SubjectValidation;
650
+ /**
651
+ * The footer line for a subject — the ONE place this wording lives.
652
+ * The name appears only at `high` with a disclosed name; otherwise no name.
653
+ *
654
+ * - low / no disclosure → "Sent by an AI agent via «operator»"
655
+ * - high / as_vouched → "Sent by an AI agent of «name» — verified by «operator»"
656
+ * - high / eudi → "Sent by an AI agent of «name» — identity verified (EUDI)"
657
+ *
658
+ * `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
659
+ * upstream; a different operator self-vouches under its own name.
660
+ */
661
+ declare function deriveIdentityLine(subject: Subject | undefined | null, { operatorName }: {
662
+ operatorName: string;
663
+ }): string;
664
+
521
665
  /**
522
666
  * Frame Canonicalization for Agent Profiles
523
667
  *
@@ -715,4 +859,4 @@ declare function listProfiles(): string[];
715
859
  declare function getAllProfiles(): AgentProfile[];
716
860
  declare function clearProfiles(): void;
717
861
 
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 };
862
+ 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, type Subject, type SubjectValidation, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
package/dist/index.js CHANGED
@@ -42,18 +42,23 @@ __export(index_exports, {
42
42
  computeContentHash: () => computeContentHash,
43
43
  computeContextHash: () => computeContextHash,
44
44
  computeFrameHash: () => computeFrameHash,
45
+ computeIntentDisclosureHash: () => computeIntentDisclosureHash,
46
+ computeIntentHash: () => computeIntentHash,
45
47
  contentCanonicalBytes: () => contentCanonicalBytes,
46
48
  decodeAttestationBlob: () => decodeAttestationBlob,
49
+ deriveIdentityLine: () => deriveIdentityLine,
47
50
  encodeAttestationBlob: () => encodeAttestationBlob,
48
51
  frameHash: () => frameHash,
49
52
  getAllProfiles: () => getAllProfiles,
50
53
  getProfile: () => getProfile,
54
+ intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
51
55
  isV4Attestation: () => isV4Attestation,
52
56
  listProfiles: () => listProfiles,
53
57
  registerProfile: () => registerProfile,
54
58
  validateBoundsParams: () => validateBoundsParams,
55
59
  validateContextParams: () => validateContextParams,
56
60
  validateFrameParams: () => validateFrameParams,
61
+ validateSubject: () => validateSubject,
57
62
  verify: () => verify,
58
63
  verifyAttestation: () => verifyAttestation,
59
64
  verifyAttestationSignature: () => verifyAttestationSignature,
@@ -116,8 +121,62 @@ function computeContentHash(binding, content) {
116
121
  return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
117
122
  }
118
123
 
119
- // src/frame.ts
124
+ // src/intent-disclosure.ts
120
125
  var import_crypto2 = require("crypto");
126
+ function sha256Hex2(bytes) {
127
+ return (0, import_crypto2.createHash)("sha256").update(bytes, "utf8").digest("hex");
128
+ }
129
+ function computeIntentHash(intentText) {
130
+ return `sha256:${sha256Hex2(canonicalizeText(intentText))}`;
131
+ }
132
+ function intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen) {
133
+ const sorted = [...approversFrozen].sort();
134
+ return `${intentCiphertext}
135
+ ${canonicalize(sorted)}`;
136
+ }
137
+ function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
138
+ return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
139
+ }
140
+
141
+ // src/identity.ts
142
+ function validateSubject(subject) {
143
+ const errors = [];
144
+ const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
145
+ if (!did) errors.push("subject.did is required");
146
+ if (assurance === "low") {
147
+ if (method !== "self_declared") errors.push(`low assurance requires method "self_declared", got "${method}"`);
148
+ if (trust_root !== "self") errors.push(`low assurance requires trust_root "self", got "${trust_root}"`);
149
+ if (disclose) errors.push("low assurance MUST NOT disclose a name");
150
+ } else if (assurance === "high") {
151
+ if (method === "as_vouched") {
152
+ if (trust_root !== "as") errors.push('as_vouched requires trust_root "as"');
153
+ if (!verifier) errors.push("as_vouched requires a verifier");
154
+ } else if (method === "eudi") {
155
+ if (trust_root !== "external") errors.push('eudi requires trust_root "external"');
156
+ if (!verifier) errors.push("eudi requires a verifier");
157
+ if (!owner_signature) errors.push("eudi requires an owner_signature");
158
+ } else {
159
+ errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
160
+ }
161
+ if (disclose && typeof disclose.name !== "string") errors.push("disclose.name must be a string");
162
+ } else {
163
+ errors.push(`assurance must be "low" or "high", got "${assurance}"`);
164
+ }
165
+ return { valid: errors.length === 0, errors };
166
+ }
167
+ function deriveIdentityLine(subject, { operatorName }) {
168
+ const name = subject?.assurance === "high" ? subject.disclose?.name : void 0;
169
+ if (!name) {
170
+ return `Sent by an AI agent via ${operatorName}`;
171
+ }
172
+ if (subject.method === "eudi") {
173
+ return `Sent by an AI agent of ${name} \u2014 identity verified (EUDI)`;
174
+ }
175
+ return `Sent by an AI agent of ${name} \u2014 verified by ${operatorName}`;
176
+ }
177
+
178
+ // src/frame.ts
179
+ var import_crypto3 = require("crypto");
121
180
  function validateFrameParams(params, profile) {
122
181
  const errors = [];
123
182
  if (!profile.frameSchema) {
@@ -154,7 +213,7 @@ function canonicalFrame(params, profile) {
154
213
  return lines.join("\n");
155
214
  }
156
215
  function frameHash(canonicalFrameString) {
157
- const hash = (0, import_crypto2.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
216
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
158
217
  return `sha256:${hash}`;
159
218
  }
160
219
  function computeFrameHash(params, profile) {
@@ -238,17 +297,17 @@ function canonicalContext(params, profile) {
238
297
  }
239
298
  function computeBoundsHash(params, profile) {
240
299
  const canonical = canonicalBounds(params, profile);
241
- const hash = (0, import_crypto2.createHash)("sha256").update(canonical, "utf8").digest("hex");
300
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
242
301
  return `sha256:${hash}`;
243
302
  }
244
303
  function computeContextHash(params, profile) {
245
304
  const canonical = canonicalContext(params, profile);
246
- const hash = (0, import_crypto2.createHash)("sha256").update(canonical, "utf8").digest("hex");
305
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
247
306
  return `sha256:${hash}`;
248
307
  }
249
308
 
250
309
  // src/attestation.ts
251
- var import_crypto3 = require("crypto");
310
+ var import_crypto4 = require("crypto");
252
311
  var ed = __toESM(require("@noble/ed25519"));
253
312
  function decodeAttestationBlob(blob) {
254
313
  try {
@@ -266,7 +325,7 @@ function encodeAttestationBlob(attestation) {
266
325
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
267
326
  }
268
327
  function attestationId(blob) {
269
- const hash = (0, import_crypto3.createHash)("sha256").update(blob, "utf8").digest("hex");
328
+ const hash = (0, import_crypto4.createHash)("sha256").update(blob, "utf8").digest("hex");
270
329
  return `sha256:${hash}`;
271
330
  }
272
331
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -396,11 +455,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
396
455
  try {
397
456
  checkAttestationExpiry(attestation.payload, now);
398
457
  } catch {
399
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
458
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
400
459
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
401
460
  continue;
402
461
  }
403
- for (const rd of attestation.payload.resolved_domains) {
462
+ for (const rd of attestation.payload.resolved_domains ?? []) {
404
463
  coveredDomains.add(rd.domain);
405
464
  }
406
465
  }
@@ -475,11 +534,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
475
534
  try {
476
535
  checkAttestationExpiry(attestation.payload, now);
477
536
  } catch {
478
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
537
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
479
538
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
480
539
  continue;
481
540
  }
482
- for (const rd of attestation.payload.resolved_domains) {
541
+ for (const rd of attestation.payload.resolved_domains ?? []) {
483
542
  coveredDomains.add(rd.domain);
484
543
  }
485
544
  }
@@ -783,18 +842,23 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
783
842
  computeContentHash,
784
843
  computeContextHash,
785
844
  computeFrameHash,
845
+ computeIntentDisclosureHash,
846
+ computeIntentHash,
786
847
  contentCanonicalBytes,
787
848
  decodeAttestationBlob,
849
+ deriveIdentityLine,
788
850
  encodeAttestationBlob,
789
851
  frameHash,
790
852
  getAllProfiles,
791
853
  getProfile,
854
+ intentDisclosureCanonicalBytes,
792
855
  isV4Attestation,
793
856
  listProfiles,
794
857
  registerProfile,
795
858
  validateBoundsParams,
796
859
  validateContextParams,
797
860
  validateFrameParams,
861
+ validateSubject,
798
862
  verify,
799
863
  verifyAttestation,
800
864
  verifyAttestationSignature,
package/dist/index.mjs CHANGED
@@ -50,8 +50,62 @@ function computeContentHash(binding, content) {
50
50
  return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
51
51
  }
52
52
 
53
- // src/frame.ts
53
+ // src/intent-disclosure.ts
54
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/identity.ts
71
+ function validateSubject(subject) {
72
+ const errors = [];
73
+ const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
74
+ if (!did) errors.push("subject.did is required");
75
+ if (assurance === "low") {
76
+ if (method !== "self_declared") errors.push(`low assurance requires method "self_declared", got "${method}"`);
77
+ if (trust_root !== "self") errors.push(`low assurance requires trust_root "self", got "${trust_root}"`);
78
+ if (disclose) errors.push("low assurance MUST NOT disclose a name");
79
+ } else if (assurance === "high") {
80
+ if (method === "as_vouched") {
81
+ if (trust_root !== "as") errors.push('as_vouched requires trust_root "as"');
82
+ if (!verifier) errors.push("as_vouched requires a verifier");
83
+ } else if (method === "eudi") {
84
+ if (trust_root !== "external") errors.push('eudi requires trust_root "external"');
85
+ if (!verifier) errors.push("eudi requires a verifier");
86
+ if (!owner_signature) errors.push("eudi requires an owner_signature");
87
+ } else {
88
+ errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
89
+ }
90
+ if (disclose && typeof disclose.name !== "string") errors.push("disclose.name must be a string");
91
+ } else {
92
+ errors.push(`assurance must be "low" or "high", got "${assurance}"`);
93
+ }
94
+ return { valid: errors.length === 0, errors };
95
+ }
96
+ function deriveIdentityLine(subject, { operatorName }) {
97
+ const name = subject?.assurance === "high" ? subject.disclose?.name : void 0;
98
+ if (!name) {
99
+ return `Sent by an AI agent via ${operatorName}`;
100
+ }
101
+ if (subject.method === "eudi") {
102
+ return `Sent by an AI agent of ${name} \u2014 identity verified (EUDI)`;
103
+ }
104
+ return `Sent by an AI agent of ${name} \u2014 verified by ${operatorName}`;
105
+ }
106
+
107
+ // src/frame.ts
108
+ import { createHash as createHash3 } from "crypto";
55
109
  function validateFrameParams(params, profile) {
56
110
  const errors = [];
57
111
  if (!profile.frameSchema) {
@@ -88,7 +142,7 @@ function canonicalFrame(params, profile) {
88
142
  return lines.join("\n");
89
143
  }
90
144
  function frameHash(canonicalFrameString) {
91
- const hash = createHash2("sha256").update(canonicalFrameString, "utf8").digest("hex");
145
+ const hash = createHash3("sha256").update(canonicalFrameString, "utf8").digest("hex");
92
146
  return `sha256:${hash}`;
93
147
  }
94
148
  function computeFrameHash(params, profile) {
@@ -172,17 +226,17 @@ function canonicalContext(params, profile) {
172
226
  }
173
227
  function computeBoundsHash(params, profile) {
174
228
  const canonical = canonicalBounds(params, profile);
175
- const hash = createHash2("sha256").update(canonical, "utf8").digest("hex");
229
+ const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
176
230
  return `sha256:${hash}`;
177
231
  }
178
232
  function computeContextHash(params, profile) {
179
233
  const canonical = canonicalContext(params, profile);
180
- const hash = createHash2("sha256").update(canonical, "utf8").digest("hex");
234
+ const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
181
235
  return `sha256:${hash}`;
182
236
  }
183
237
 
184
238
  // src/attestation.ts
185
- import { createHash as createHash3 } from "crypto";
239
+ import { createHash as createHash4 } from "crypto";
186
240
  import * as ed from "@noble/ed25519";
187
241
  function decodeAttestationBlob(blob) {
188
242
  try {
@@ -200,7 +254,7 @@ function encodeAttestationBlob(attestation) {
200
254
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
201
255
  }
202
256
  function attestationId(blob) {
203
- const hash = createHash3("sha256").update(blob, "utf8").digest("hex");
257
+ const hash = createHash4("sha256").update(blob, "utf8").digest("hex");
204
258
  return `sha256:${hash}`;
205
259
  }
206
260
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -330,11 +384,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
330
384
  try {
331
385
  checkAttestationExpiry(attestation.payload, now);
332
386
  } catch {
333
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
387
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
334
388
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
335
389
  continue;
336
390
  }
337
- for (const rd of attestation.payload.resolved_domains) {
391
+ for (const rd of attestation.payload.resolved_domains ?? []) {
338
392
  coveredDomains.add(rd.domain);
339
393
  }
340
394
  }
@@ -409,11 +463,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
409
463
  try {
410
464
  checkAttestationExpiry(attestation.payload, now);
411
465
  } catch {
412
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
466
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
413
467
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
414
468
  continue;
415
469
  }
416
- for (const rd of attestation.payload.resolved_domains) {
470
+ for (const rd of attestation.payload.resolved_domains ?? []) {
417
471
  coveredDomains.add(rd.domain);
418
472
  }
419
473
  }
@@ -716,18 +770,23 @@ export {
716
770
  computeContentHash,
717
771
  computeContextHash,
718
772
  computeFrameHash,
773
+ computeIntentDisclosureHash,
774
+ computeIntentHash,
719
775
  contentCanonicalBytes,
720
776
  decodeAttestationBlob,
777
+ deriveIdentityLine,
721
778
  encodeAttestationBlob,
722
779
  frameHash,
723
780
  getAllProfiles,
724
781
  getProfile,
782
+ intentDisclosureCanonicalBytes,
725
783
  isV4Attestation,
726
784
  listProfiles,
727
785
  registerProfile,
728
786
  validateBoundsParams,
729
787
  validateContextParams,
730
788
  validateFrameParams,
789
+ validateSubject,
731
790
  verify,
732
791
  verifyAttestation,
733
792
  verifyAttestationSignature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
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
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Identity Assurance (HAP v0.6) — validation + footer derivation for the signed
3
+ * {@link Subject} overlay. See review.md → "Identity Assurance".
4
+ *
5
+ * Two pure helpers, the single source of truth for both the gateway footer and
6
+ * the AS verify page:
7
+ * - {@link validateSubject} — enforces the level/method/trust-root invariants.
8
+ * - {@link deriveIdentityLine} — the human-readable footer line for a subject.
9
+ */
10
+
11
+ import type { Subject } from './types';
12
+
13
+ export interface SubjectValidation {
14
+ valid: boolean;
15
+ errors: string[];
16
+ }
17
+
18
+ /**
19
+ * Enforce the Identity-Assurance invariants on a single subject:
20
+ * - `self_declared` ⇒ `low` / `self`, no disclosed name.
21
+ * - `as_vouched` ⇒ `high` / `as`, `verifier` required.
22
+ * - `eudi` ⇒ `high` / `external`, `verifier` + `owner_signature` required.
23
+ * - `disclose.name` only at `assurance:"high"`.
24
+ */
25
+ export function validateSubject(subject: Subject): SubjectValidation {
26
+ const errors: string[] = [];
27
+ const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
28
+
29
+ if (!did) errors.push('subject.did is required');
30
+
31
+ if (assurance === 'low') {
32
+ if (method !== 'self_declared') errors.push(`low assurance requires method "self_declared", got "${method}"`);
33
+ if (trust_root !== 'self') errors.push(`low assurance requires trust_root "self", got "${trust_root}"`);
34
+ if (disclose) errors.push('low assurance MUST NOT disclose a name');
35
+ } else if (assurance === 'high') {
36
+ if (method === 'as_vouched') {
37
+ if (trust_root !== 'as') errors.push('as_vouched requires trust_root "as"');
38
+ if (!verifier) errors.push('as_vouched requires a verifier');
39
+ } else if (method === 'eudi') {
40
+ if (trust_root !== 'external') errors.push('eudi requires trust_root "external"');
41
+ if (!verifier) errors.push('eudi requires a verifier');
42
+ if (!owner_signature) errors.push('eudi requires an owner_signature');
43
+ } else {
44
+ errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
45
+ }
46
+ if (disclose && typeof disclose.name !== 'string') errors.push('disclose.name must be a string');
47
+ } else {
48
+ errors.push(`assurance must be "low" or "high", got "${assurance as string}"`);
49
+ }
50
+
51
+ return { valid: errors.length === 0, errors };
52
+ }
53
+
54
+ /**
55
+ * The footer line for a subject — the ONE place this wording lives.
56
+ * The name appears only at `high` with a disclosed name; otherwise no name.
57
+ *
58
+ * - low / no disclosure → "Sent by an AI agent via «operator»"
59
+ * - high / as_vouched → "Sent by an AI agent of «name» — verified by «operator»"
60
+ * - high / eudi → "Sent by an AI agent of «name» — identity verified (EUDI)"
61
+ *
62
+ * `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
63
+ * upstream; a different operator self-vouches under its own name.
64
+ */
65
+ export function deriveIdentityLine(
66
+ subject: Subject | undefined | null,
67
+ { operatorName }: { operatorName: string },
68
+ ): string {
69
+ const name = subject?.assurance === 'high' ? subject.disclose?.name : undefined;
70
+ if (!name) {
71
+ return `Sent by an AI agent via ${operatorName}`;
72
+ }
73
+ if (subject!.method === 'eudi') {
74
+ return `Sent by an AI agent of ${name} — identity verified (EUDI)`;
75
+ }
76
+ return `Sent by an AI agent of ${name} — verified by ${operatorName}`;
77
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  export * from './types';
8
8
  export * from './canonicalize';
9
9
  export * from './content-binding';
10
+ export * from './intent-disclosure';
11
+ export * from './identity';
10
12
  export * from './frame';
11
13
  export * from './attestation';
12
14
  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,37 @@ 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;
54
+ /**
55
+ * v0.6 (Identity Assurance) — optional signed overlay carrying the verified
56
+ * real-world identity of the Decision Owner(s), one entry per owner. Present
57
+ * only when identity is disclosed; an attestation with no `subjects` renders
58
+ * as `low` (pseudonymous, no name). See {@link Subject} and
59
+ * {@link deriveIdentityLine}.
60
+ */
61
+ subjects?: Subject[];
33
62
  /**
34
63
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
35
64
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -44,6 +73,37 @@ export interface AttestationPayload {
44
73
  expires_at: number;
45
74
  }
46
75
 
76
+ /**
77
+ * v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
78
+ * real-world identity to the attestation, gated by HOW the identity was verified.
79
+ *
80
+ * Two display levels (`assurance`): `low` discloses no name; `high` MAY disclose a
81
+ * name. At `high`, two trust roots: `as` (the AS operator vouches — valid only
82
+ * within its own domain) and `external` (an external eID such as EUDI — carries the
83
+ * owner's own signature, AS-independent). See review.md → "Identity Assurance".
84
+ */
85
+ export interface Subject {
86
+ /** The Decision Owner DID this subject describes (matches an entry in resolved_owners). */
87
+ did: string;
88
+ /** `low` → no name shown; `high` → the name MAY be shown. */
89
+ assurance: 'low' | 'high';
90
+ /** How identity was established. */
91
+ method: 'self_declared' | 'as_vouched' | 'eudi';
92
+ /** Who vouches: `self` (owner's claim), `as` (operator), `external` (eID scheme). */
93
+ trust_root: 'self' | 'as' | 'external';
94
+ /** Verifier id — the AS operator (as_vouched) or the eID scheme (eudi). Required at `high`. */
95
+ verifier?: string;
96
+ /** Disclosed attributes. `name` present ONLY at `assurance:"high"` and when disclosure is on. */
97
+ disclose?: { name: string };
98
+ /** When the underlying verification was performed (unix seconds). */
99
+ verified_at?: number;
100
+ /**
101
+ * eudi only (Phase 2) — the owner's per-event wallet signature, making the
102
+ * identity claim non-repudiable independent of the AS. Null/absent otherwise.
103
+ */
104
+ owner_signature?: string | null;
105
+ }
106
+
47
107
  export interface Attestation {
48
108
  header: AttestationHeader;
49
109
  payload: AttestationPayload;