@humanagencyp/hap-core 0.5.2 → 0.6.1

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
@@ -46,6 +46,14 @@ interface AttestationPayload {
46
46
  * without invalidating the attestation signature (companion invariant C2).
47
47
  */
48
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[];
49
57
  /**
50
58
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
51
59
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -59,6 +67,38 @@ interface AttestationPayload {
59
67
  issued_at: number;
60
68
  expires_at: number;
61
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
+ }
62
102
  interface Attestation {
63
103
  header: AttestationHeader;
64
104
  payload: AttestationPayload;
@@ -585,6 +625,48 @@ declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approv
585
625
  */
586
626
  declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
587
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
+ * Punctuation is ASCII (comma, not em-dash) so the line survives header and
659
+ * plain-text encoding intact across email, calendar, and publishing channels.
660
+ * The leading verb ("Sent") is the default; presentation layers may swap it
661
+ * (e.g. the gateway uses "Published" for the publish profile).
662
+ *
663
+ * `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
664
+ * upstream; a different operator self-vouches under its own name.
665
+ */
666
+ declare function deriveIdentityLine(subject: Subject | undefined | null, { operatorName }: {
667
+ operatorName: string;
668
+ }): string;
669
+
588
670
  /**
589
671
  * Frame Canonicalization for Agent Profiles
590
672
  *
@@ -782,4 +864,4 @@ declare function listProfiles(): string[];
782
864
  declare function getAllProfiles(): AgentProfile[];
783
865
  declare function clearProfiles(): void;
784
866
 
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 };
867
+ 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
@@ -46,6 +46,14 @@ interface AttestationPayload {
46
46
  * without invalidating the attestation signature (companion invariant C2).
47
47
  */
48
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[];
49
57
  /**
50
58
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
51
59
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -59,6 +67,38 @@ interface AttestationPayload {
59
67
  issued_at: number;
60
68
  expires_at: number;
61
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
+ }
62
102
  interface Attestation {
63
103
  header: AttestationHeader;
64
104
  payload: AttestationPayload;
@@ -585,6 +625,48 @@ declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approv
585
625
  */
586
626
  declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
587
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
+ * Punctuation is ASCII (comma, not em-dash) so the line survives header and
659
+ * plain-text encoding intact across email, calendar, and publishing channels.
660
+ * The leading verb ("Sent") is the default; presentation layers may swap it
661
+ * (e.g. the gateway uses "Published" for the publish profile).
662
+ *
663
+ * `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
664
+ * upstream; a different operator self-vouches under its own name.
665
+ */
666
+ declare function deriveIdentityLine(subject: Subject | undefined | null, { operatorName }: {
667
+ operatorName: string;
668
+ }): string;
669
+
588
670
  /**
589
671
  * Frame Canonicalization for Agent Profiles
590
672
  *
@@ -782,4 +864,4 @@ declare function listProfiles(): string[];
782
864
  declare function getAllProfiles(): AgentProfile[];
783
865
  declare function clearProfiles(): void;
784
866
 
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 };
867
+ 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
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  computeIntentHash: () => computeIntentHash,
47
47
  contentCanonicalBytes: () => contentCanonicalBytes,
48
48
  decodeAttestationBlob: () => decodeAttestationBlob,
49
+ deriveIdentityLine: () => deriveIdentityLine,
49
50
  encodeAttestationBlob: () => encodeAttestationBlob,
50
51
  frameHash: () => frameHash,
51
52
  getAllProfiles: () => getAllProfiles,
@@ -57,6 +58,7 @@ __export(index_exports, {
57
58
  validateBoundsParams: () => validateBoundsParams,
58
59
  validateContextParams: () => validateContextParams,
59
60
  validateFrameParams: () => validateFrameParams,
61
+ validateSubject: () => validateSubject,
60
62
  verify: () => verify,
61
63
  verifyAttestation: () => verifyAttestation,
62
64
  verifyAttestationSignature: () => verifyAttestationSignature,
@@ -136,6 +138,43 @@ function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
136
138
  return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
137
139
  }
138
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}, identity verified (EUDI)`;
174
+ }
175
+ return `Sent by an AI agent of ${name}, verified by ${operatorName}`;
176
+ }
177
+
139
178
  // src/frame.ts
140
179
  var import_crypto3 = require("crypto");
141
180
  function validateFrameParams(params, profile) {
@@ -807,6 +846,7 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
807
846
  computeIntentHash,
808
847
  contentCanonicalBytes,
809
848
  decodeAttestationBlob,
849
+ deriveIdentityLine,
810
850
  encodeAttestationBlob,
811
851
  frameHash,
812
852
  getAllProfiles,
@@ -818,6 +858,7 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
818
858
  validateBoundsParams,
819
859
  validateContextParams,
820
860
  validateFrameParams,
861
+ validateSubject,
821
862
  verify,
822
863
  verifyAttestation,
823
864
  verifyAttestationSignature,
package/dist/index.mjs CHANGED
@@ -67,6 +67,43 @@ function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
67
67
  return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
68
68
  }
69
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}, identity verified (EUDI)`;
103
+ }
104
+ return `Sent by an AI agent of ${name}, verified by ${operatorName}`;
105
+ }
106
+
70
107
  // src/frame.ts
71
108
  import { createHash as createHash3 } from "crypto";
72
109
  function validateFrameParams(params, profile) {
@@ -737,6 +774,7 @@ export {
737
774
  computeIntentHash,
738
775
  contentCanonicalBytes,
739
776
  decodeAttestationBlob,
777
+ deriveIdentityLine,
740
778
  encodeAttestationBlob,
741
779
  frameHash,
742
780
  getAllProfiles,
@@ -748,6 +786,7 @@ export {
748
786
  validateBoundsParams,
749
787
  validateContextParams,
750
788
  validateFrameParams,
789
+ validateSubject,
751
790
  verify,
752
791
  verifyAttestation,
753
792
  verifyAttestationSignature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Core types, cryptographic primitives, and verification logic for the Human Agency Protocol",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,82 @@
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
+ * Punctuation is ASCII (comma, not em-dash) so the line survives header and
63
+ * plain-text encoding intact across email, calendar, and publishing channels.
64
+ * The leading verb ("Sent") is the default; presentation layers may swap it
65
+ * (e.g. the gateway uses "Published" for the publish profile).
66
+ *
67
+ * `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
68
+ * upstream; a different operator self-vouches under its own name.
69
+ */
70
+ export function deriveIdentityLine(
71
+ subject: Subject | undefined | null,
72
+ { operatorName }: { operatorName: string },
73
+ ): string {
74
+ const name = subject?.assurance === 'high' ? subject.disclose?.name : undefined;
75
+ if (!name) {
76
+ return `Sent by an AI agent via ${operatorName}`;
77
+ }
78
+ if (subject!.method === 'eudi') {
79
+ return `Sent by an AI agent of ${name}, identity verified (EUDI)`;
80
+ }
81
+ return `Sent by an AI agent of ${name}, verified by ${operatorName}`;
82
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from './types';
8
8
  export * from './canonicalize';
9
9
  export * from './content-binding';
10
10
  export * from './intent-disclosure';
11
+ export * from './identity';
11
12
  export * from './frame';
12
13
  export * from './attestation';
13
14
  export * from './gatekeeper';
package/src/types.ts CHANGED
@@ -51,6 +51,14 @@ export interface AttestationPayload {
51
51
  * without invalidating the attestation signature (companion invariant C2).
52
52
  */
53
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[];
54
62
  /**
55
63
  * v0.4 — commitment mode chosen by the decision owner at attestation time.
56
64
  * - 'automatic': agent may invoke bounded tools without per-action review.
@@ -65,6 +73,37 @@ export interface AttestationPayload {
65
73
  expires_at: number;
66
74
  }
67
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
+
68
107
  export interface Attestation {
69
108
  header: AttestationHeader;
70
109
  payload: AttestationPayload;