@humanagencyp/hap-core 0.5.1 → 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.
@@ -518,6 +539,52 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
518
539
  */
519
540
  declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
520
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
+
521
588
  /**
522
589
  * Frame Canonicalization for Agent Profiles
523
590
  *
@@ -715,4 +782,4 @@ declare function listProfiles(): string[];
715
782
  declare function getAllProfiles(): AgentProfile[];
716
783
  declare function clearProfiles(): void;
717
784
 
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 };
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.
@@ -518,6 +539,52 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
518
539
  */
519
540
  declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
520
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
+
521
588
  /**
522
589
  * Frame Canonicalization for Agent Profiles
523
590
  *
@@ -715,4 +782,4 @@ declare function listProfiles(): string[];
715
782
  declare function getAllProfiles(): AgentProfile[];
716
783
  declare function clearProfiles(): void;
717
784
 
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 };
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
@@ -42,12 +42,15 @@ __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,
47
49
  encodeAttestationBlob: () => encodeAttestationBlob,
48
50
  frameHash: () => frameHash,
49
51
  getAllProfiles: () => getAllProfiles,
50
52
  getProfile: () => getProfile,
53
+ intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
51
54
  isV4Attestation: () => isV4Attestation,
52
55
  listProfiles: () => listProfiles,
53
56
  registerProfile: () => registerProfile,
@@ -116,8 +119,25 @@ function computeContentHash(binding, content) {
116
119
  return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
117
120
  }
118
121
 
119
- // src/frame.ts
122
+ // src/intent-disclosure.ts
120
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");
121
141
  function validateFrameParams(params, profile) {
122
142
  const errors = [];
123
143
  if (!profile.frameSchema) {
@@ -154,7 +174,7 @@ function canonicalFrame(params, profile) {
154
174
  return lines.join("\n");
155
175
  }
156
176
  function frameHash(canonicalFrameString) {
157
- const hash = (0, import_crypto2.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
177
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
158
178
  return `sha256:${hash}`;
159
179
  }
160
180
  function computeFrameHash(params, profile) {
@@ -238,17 +258,17 @@ function canonicalContext(params, profile) {
238
258
  }
239
259
  function computeBoundsHash(params, profile) {
240
260
  const canonical = canonicalBounds(params, profile);
241
- const hash = (0, import_crypto2.createHash)("sha256").update(canonical, "utf8").digest("hex");
261
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
242
262
  return `sha256:${hash}`;
243
263
  }
244
264
  function computeContextHash(params, profile) {
245
265
  const canonical = canonicalContext(params, profile);
246
- const hash = (0, import_crypto2.createHash)("sha256").update(canonical, "utf8").digest("hex");
266
+ const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
247
267
  return `sha256:${hash}`;
248
268
  }
249
269
 
250
270
  // src/attestation.ts
251
- var import_crypto3 = require("crypto");
271
+ var import_crypto4 = require("crypto");
252
272
  var ed = __toESM(require("@noble/ed25519"));
253
273
  function decodeAttestationBlob(blob) {
254
274
  try {
@@ -266,7 +286,7 @@ function encodeAttestationBlob(attestation) {
266
286
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
267
287
  }
268
288
  function attestationId(blob) {
269
- const hash = (0, import_crypto3.createHash)("sha256").update(blob, "utf8").digest("hex");
289
+ const hash = (0, import_crypto4.createHash)("sha256").update(blob, "utf8").digest("hex");
270
290
  return `sha256:${hash}`;
271
291
  }
272
292
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -396,11 +416,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
396
416
  try {
397
417
  checkAttestationExpiry(attestation.payload, now);
398
418
  } catch {
399
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
419
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
400
420
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
401
421
  continue;
402
422
  }
403
- for (const rd of attestation.payload.resolved_domains) {
423
+ for (const rd of attestation.payload.resolved_domains ?? []) {
404
424
  coveredDomains.add(rd.domain);
405
425
  }
406
426
  }
@@ -475,11 +495,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
475
495
  try {
476
496
  checkAttestationExpiry(attestation.payload, now);
477
497
  } catch {
478
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
498
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
479
499
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
480
500
  continue;
481
501
  }
482
- for (const rd of attestation.payload.resolved_domains) {
502
+ for (const rd of attestation.payload.resolved_domains ?? []) {
483
503
  coveredDomains.add(rd.domain);
484
504
  }
485
505
  }
@@ -783,12 +803,15 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
783
803
  computeContentHash,
784
804
  computeContextHash,
785
805
  computeFrameHash,
806
+ computeIntentDisclosureHash,
807
+ computeIntentHash,
786
808
  contentCanonicalBytes,
787
809
  decodeAttestationBlob,
788
810
  encodeAttestationBlob,
789
811
  frameHash,
790
812
  getAllProfiles,
791
813
  getProfile,
814
+ intentDisclosureCanonicalBytes,
792
815
  isV4Attestation,
793
816
  listProfiles,
794
817
  registerProfile,
package/dist/index.mjs CHANGED
@@ -50,8 +50,25 @@ 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/frame.ts
71
+ import { createHash as createHash3 } from "crypto";
55
72
  function validateFrameParams(params, profile) {
56
73
  const errors = [];
57
74
  if (!profile.frameSchema) {
@@ -88,7 +105,7 @@ function canonicalFrame(params, profile) {
88
105
  return lines.join("\n");
89
106
  }
90
107
  function frameHash(canonicalFrameString) {
91
- const hash = createHash2("sha256").update(canonicalFrameString, "utf8").digest("hex");
108
+ const hash = createHash3("sha256").update(canonicalFrameString, "utf8").digest("hex");
92
109
  return `sha256:${hash}`;
93
110
  }
94
111
  function computeFrameHash(params, profile) {
@@ -172,17 +189,17 @@ function canonicalContext(params, profile) {
172
189
  }
173
190
  function computeBoundsHash(params, profile) {
174
191
  const canonical = canonicalBounds(params, profile);
175
- const hash = createHash2("sha256").update(canonical, "utf8").digest("hex");
192
+ const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
176
193
  return `sha256:${hash}`;
177
194
  }
178
195
  function computeContextHash(params, profile) {
179
196
  const canonical = canonicalContext(params, profile);
180
- const hash = createHash2("sha256").update(canonical, "utf8").digest("hex");
197
+ const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
181
198
  return `sha256:${hash}`;
182
199
  }
183
200
 
184
201
  // src/attestation.ts
185
- import { createHash as createHash3 } from "crypto";
202
+ import { createHash as createHash4 } from "crypto";
186
203
  import * as ed from "@noble/ed25519";
187
204
  function decodeAttestationBlob(blob) {
188
205
  try {
@@ -200,7 +217,7 @@ function encodeAttestationBlob(attestation) {
200
217
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
201
218
  }
202
219
  function attestationId(blob) {
203
- const hash = createHash3("sha256").update(blob, "utf8").digest("hex");
220
+ const hash = createHash4("sha256").update(blob, "utf8").digest("hex");
204
221
  return `sha256:${hash}`;
205
222
  }
206
223
  async function verifyAttestationSignature(attestation, publicKeyHex) {
@@ -330,11 +347,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
330
347
  try {
331
348
  checkAttestationExpiry(attestation.payload, now);
332
349
  } catch {
333
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
350
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
334
351
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
335
352
  continue;
336
353
  }
337
- for (const rd of attestation.payload.resolved_domains) {
354
+ for (const rd of attestation.payload.resolved_domains ?? []) {
338
355
  coveredDomains.add(rd.domain);
339
356
  }
340
357
  }
@@ -409,11 +426,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
409
426
  try {
410
427
  checkAttestationExpiry(attestation.payload, now);
411
428
  } catch {
412
- const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
429
+ const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
413
430
  errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
414
431
  continue;
415
432
  }
416
- for (const rd of attestation.payload.resolved_domains) {
433
+ for (const rd of attestation.payload.resolved_domains ?? []) {
417
434
  coveredDomains.add(rd.domain);
418
435
  }
419
436
  }
@@ -716,12 +733,15 @@ export {
716
733
  computeContentHash,
717
734
  computeContextHash,
718
735
  computeFrameHash,
736
+ computeIntentDisclosureHash,
737
+ computeIntentHash,
719
738
  contentCanonicalBytes,
720
739
  decodeAttestationBlob,
721
740
  encodeAttestationBlob,
722
741
  frameHash,
723
742
  getAllProfiles,
724
743
  getProfile,
744
+ intentDisclosureCanonicalBytes,
725
745
  isV4Attestation,
726
746
  listProfiles,
727
747
  registerProfile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanagencyp/hap-core",
3
- "version": "0.5.1",
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": {
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
@@ -7,6 +7,7 @@
7
7
  export * from './types';
8
8
  export * from './canonicalize';
9
9
  export * from './content-binding';
10
+ export * from './intent-disclosure';
10
11
  export * from './frame';
11
12
  export * from './attestation';
12
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.