life-as-api 0.2.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.
Files changed (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/agent/finance-agent.d.ts +44 -0
  4. package/dist/agent/finance-agent.js +75 -0
  5. package/dist/agent/finance-agent.js.map +1 -0
  6. package/dist/agent/legal-agent.d.ts +44 -0
  7. package/dist/agent/legal-agent.js +76 -0
  8. package/dist/agent/legal-agent.js.map +1 -0
  9. package/dist/agent/medical-agent.d.ts +44 -0
  10. package/dist/agent/medical-agent.js +75 -0
  11. package/dist/agent/medical-agent.js.map +1 -0
  12. package/dist/agent/trusted-agent-registry.d.ts +53 -0
  13. package/dist/agent/trusted-agent-registry.js +57 -0
  14. package/dist/agent/trusted-agent-registry.js.map +1 -0
  15. package/dist/audit/audit-chain-verifier.d.ts +10 -0
  16. package/dist/audit/audit-chain-verifier.js +67 -0
  17. package/dist/audit/audit-chain-verifier.js.map +1 -0
  18. package/dist/audit/audit-log.d.ts +48 -0
  19. package/dist/audit/audit-log.js +74 -0
  20. package/dist/audit/audit-log.js.map +1 -0
  21. package/dist/audit/sqlite-audit-log.d.ts +29 -0
  22. package/dist/audit/sqlite-audit-log.js +142 -0
  23. package/dist/audit/sqlite-audit-log.js.map +1 -0
  24. package/dist/auth/access-context-issuer.d.ts +18 -0
  25. package/dist/auth/access-context-issuer.js +88 -0
  26. package/dist/auth/access-context-issuer.js.map +1 -0
  27. package/dist/auth/assert-authorized-for-subject.d.ts +2 -0
  28. package/dist/auth/assert-authorized-for-subject.js +20 -0
  29. package/dist/auth/assert-authorized-for-subject.js.map +1 -0
  30. package/dist/auth/backend-context-minting-boundary.d.ts +23 -0
  31. package/dist/auth/backend-context-minting-boundary.js +281 -0
  32. package/dist/auth/backend-context-minting-boundary.js.map +1 -0
  33. package/dist/auth/default-authorization-policy.d.ts +7 -0
  34. package/dist/auth/default-authorization-policy.js +64 -0
  35. package/dist/auth/default-authorization-policy.js.map +1 -0
  36. package/dist/auth/fake-auth-adapter.d.ts +7 -0
  37. package/dist/auth/fake-auth-adapter.js +27 -0
  38. package/dist/auth/fake-auth-adapter.js.map +1 -0
  39. package/dist/auth/http-client.d.ts +12 -0
  40. package/dist/auth/http-client.js +7 -0
  41. package/dist/auth/http-client.js.map +1 -0
  42. package/dist/auth/in-memory-grant-resolver.d.ts +21 -0
  43. package/dist/auth/in-memory-grant-resolver.js +73 -0
  44. package/dist/auth/in-memory-grant-resolver.js.map +1 -0
  45. package/dist/auth/in-memory-tenant-boundary.d.ts +16 -0
  46. package/dist/auth/in-memory-tenant-boundary.js +37 -0
  47. package/dist/auth/in-memory-tenant-boundary.js.map +1 -0
  48. package/dist/auth/jwks-key-ring-auth-adapter.d.ts +38 -0
  49. package/dist/auth/jwks-key-ring-auth-adapter.js +123 -0
  50. package/dist/auth/jwks-key-ring-auth-adapter.js.map +1 -0
  51. package/dist/auth/jwks-key-ring.d.ts +25 -0
  52. package/dist/auth/jwks-key-ring.js +111 -0
  53. package/dist/auth/jwks-key-ring.js.map +1 -0
  54. package/dist/auth/jwt-auth-adapter.d.ts +15 -0
  55. package/dist/auth/jwt-auth-adapter.js +59 -0
  56. package/dist/auth/jwt-auth-adapter.js.map +1 -0
  57. package/dist/auth/jwt-core.d.ts +32 -0
  58. package/dist/auth/jwt-core.js +226 -0
  59. package/dist/auth/jwt-core.js.map +1 -0
  60. package/dist/auth/jwt-key-ring-auth-adapter.d.ts +16 -0
  61. package/dist/auth/jwt-key-ring-auth-adapter.js +58 -0
  62. package/dist/auth/jwt-key-ring-auth-adapter.js.map +1 -0
  63. package/dist/auth/oidc-discovery.d.ts +15 -0
  64. package/dist/auth/oidc-discovery.js +46 -0
  65. package/dist/auth/oidc-discovery.js.map +1 -0
  66. package/dist/auth/production-scoped-life-api.d.ts +58 -0
  67. package/dist/auth/production-scoped-life-api.js +802 -0
  68. package/dist/auth/production-scoped-life-api.js.map +1 -0
  69. package/dist/auth/proposal-scope-inspector.d.ts +3 -0
  70. package/dist/auth/proposal-scope-inspector.js +42 -0
  71. package/dist/auth/proposal-scope-inspector.js.map +1 -0
  72. package/dist/auth/proposal-scoped-life-api.d.ts +21 -0
  73. package/dist/auth/proposal-scoped-life-api.js +168 -0
  74. package/dist/auth/proposal-scoped-life-api.js.map +1 -0
  75. package/dist/auth/rate-limited-context-minting-boundary.d.ts +27 -0
  76. package/dist/auth/rate-limited-context-minting-boundary.js +77 -0
  77. package/dist/auth/rate-limited-context-minting-boundary.js.map +1 -0
  78. package/dist/auth/revocation-aware-auth-adapter.d.ts +21 -0
  79. package/dist/auth/revocation-aware-auth-adapter.js +88 -0
  80. package/dist/auth/revocation-aware-auth-adapter.js.map +1 -0
  81. package/dist/auth/sqlite-grant-resolver.d.ts +8 -0
  82. package/dist/auth/sqlite-grant-resolver.js +78 -0
  83. package/dist/auth/sqlite-grant-resolver.js.map +1 -0
  84. package/dist/auth/sqlite-tenant-boundary.d.ts +7 -0
  85. package/dist/auth/sqlite-tenant-boundary.js +41 -0
  86. package/dist/auth/sqlite-tenant-boundary.js.map +1 -0
  87. package/dist/auth/static-jwt-key-ring.d.ts +13 -0
  88. package/dist/auth/static-jwt-key-ring.js +73 -0
  89. package/dist/auth/static-jwt-key-ring.js.map +1 -0
  90. package/dist/auth/subject-scoped-life-api.d.ts +14 -0
  91. package/dist/auth/subject-scoped-life-api.js +51 -0
  92. package/dist/auth/subject-scoped-life-api.js.map +1 -0
  93. package/dist/auth/test-context-minting-boundary.d.ts +13 -0
  94. package/dist/auth/test-context-minting-boundary.js +74 -0
  95. package/dist/auth/test-context-minting-boundary.js.map +1 -0
  96. package/dist/auth/types.d.ts +77 -0
  97. package/dist/auth/types.js +16 -0
  98. package/dist/auth/types.js.map +1 -0
  99. package/dist/compliance/subject-data-report.d.ts +43 -0
  100. package/dist/compliance/subject-data-report.js +155 -0
  101. package/dist/compliance/subject-data-report.js.map +1 -0
  102. package/dist/consent/consent-guard.d.ts +4 -0
  103. package/dist/consent/consent-guard.js +37 -0
  104. package/dist/consent/consent-guard.js.map +1 -0
  105. package/dist/consent/grant-registry.d.ts +7 -0
  106. package/dist/consent/grant-registry.js +13 -0
  107. package/dist/consent/grant-registry.js.map +1 -0
  108. package/dist/consent/intersect-selector.d.ts +3 -0
  109. package/dist/consent/intersect-selector.js +66 -0
  110. package/dist/consent/intersect-selector.js.map +1 -0
  111. package/dist/contract/diagnostics.d.ts +32 -0
  112. package/dist/contract/diagnostics.js +112 -0
  113. package/dist/contract/diagnostics.js.map +1 -0
  114. package/dist/contract/project-contracts.d.ts +5 -0
  115. package/dist/contract/project-contracts.js +159 -0
  116. package/dist/contract/project-contracts.js.map +1 -0
  117. package/dist/contract/types.d.ts +32 -0
  118. package/dist/contract/types.js +5 -0
  119. package/dist/contract/types.js.map +1 -0
  120. package/dist/crypto/aes-gcm.d.ts +11 -0
  121. package/dist/crypto/aes-gcm.js +78 -0
  122. package/dist/crypto/aes-gcm.js.map +1 -0
  123. package/dist/crypto/claim-serializer.d.ts +26 -0
  124. package/dist/crypto/claim-serializer.js +59 -0
  125. package/dist/crypto/claim-serializer.js.map +1 -0
  126. package/dist/crypto/sha256.d.ts +12 -0
  127. package/dist/crypto/sha256.js +40 -0
  128. package/dist/crypto/sha256.js.map +1 -0
  129. package/dist/crypto/static-key.d.ts +11 -0
  130. package/dist/crypto/static-key.js +60 -0
  131. package/dist/crypto/static-key.js.map +1 -0
  132. package/dist/crypto/types.d.ts +20 -0
  133. package/dist/crypto/types.js +8 -0
  134. package/dist/crypto/types.js.map +1 -0
  135. package/dist/datasource/trusted-data-source-registry.d.ts +51 -0
  136. package/dist/datasource/trusted-data-source-registry.js +57 -0
  137. package/dist/datasource/trusted-data-source-registry.js.map +1 -0
  138. package/dist/erasure/tombstone.d.ts +13 -0
  139. package/dist/erasure/tombstone.js +20 -0
  140. package/dist/erasure/tombstone.js.map +1 -0
  141. package/dist/evidence/evidence-backed-claim-creator.d.ts +48 -0
  142. package/dist/evidence/evidence-backed-claim-creator.js +107 -0
  143. package/dist/evidence/evidence-backed-claim-creator.js.map +1 -0
  144. package/dist/evidence/in-memory-raw-document-store.d.ts +26 -0
  145. package/dist/evidence/in-memory-raw-document-store.js +84 -0
  146. package/dist/evidence/in-memory-raw-document-store.js.map +1 -0
  147. package/dist/evidence/raw-document-store.d.ts +12 -0
  148. package/dist/evidence/raw-document-store.js +8 -0
  149. package/dist/evidence/raw-document-store.js.map +1 -0
  150. package/dist/evidence/source-evidence-validator.d.ts +4 -0
  151. package/dist/evidence/source-evidence-validator.js +14 -0
  152. package/dist/evidence/source-evidence-validator.js.map +1 -0
  153. package/dist/evidence/sqlite-raw-document-store.d.ts +19 -0
  154. package/dist/evidence/sqlite-raw-document-store.js +122 -0
  155. package/dist/evidence/sqlite-raw-document-store.js.map +1 -0
  156. package/dist/guardian/guardian-registry.d.ts +96 -0
  157. package/dist/guardian/guardian-registry.js +95 -0
  158. package/dist/guardian/guardian-registry.js.map +1 -0
  159. package/dist/index.d.ts +44 -0
  160. package/dist/index.js +72 -0
  161. package/dist/index.js.map +1 -0
  162. package/dist/observability/health.d.ts +12 -0
  163. package/dist/observability/health.js +16 -0
  164. package/dist/observability/health.js.map +1 -0
  165. package/dist/observability/logger.d.ts +21 -0
  166. package/dist/observability/logger.js +40 -0
  167. package/dist/observability/logger.js.map +1 -0
  168. package/dist/observability/metrics.d.ts +15 -0
  169. package/dist/observability/metrics.js +52 -0
  170. package/dist/observability/metrics.js.map +1 -0
  171. package/dist/persistence/backup.d.ts +24 -0
  172. package/dist/persistence/backup.js +251 -0
  173. package/dist/persistence/backup.js.map +1 -0
  174. package/dist/persistence/decryptability-scan.d.ts +10 -0
  175. package/dist/persistence/decryptability-scan.js +144 -0
  176. package/dist/persistence/decryptability-scan.js.map +1 -0
  177. package/dist/persistence/in-memory-unit-of-work.d.ts +11 -0
  178. package/dist/persistence/in-memory-unit-of-work.js +32 -0
  179. package/dist/persistence/in-memory-unit-of-work.js.map +1 -0
  180. package/dist/persistence/key-availability.d.ts +9 -0
  181. package/dist/persistence/key-availability.js +125 -0
  182. package/dist/persistence/key-availability.js.map +1 -0
  183. package/dist/persistence/sqlite/migrations.d.ts +9 -0
  184. package/dist/persistence/sqlite/migrations.js +535 -0
  185. package/dist/persistence/sqlite/migrations.js.map +1 -0
  186. package/dist/persistence/sqlite/schema.d.ts +10 -0
  187. package/dist/persistence/sqlite/schema.js +101 -0
  188. package/dist/persistence/sqlite/schema.js.map +1 -0
  189. package/dist/persistence/sqlite/sqlite-fact-store.d.ts +24 -0
  190. package/dist/persistence/sqlite/sqlite-fact-store.js +207 -0
  191. package/dist/persistence/sqlite/sqlite-fact-store.js.map +1 -0
  192. package/dist/persistence/sqlite/sqlite-source-registry.d.ts +17 -0
  193. package/dist/persistence/sqlite/sqlite-source-registry.js +132 -0
  194. package/dist/persistence/sqlite/sqlite-source-registry.js.map +1 -0
  195. package/dist/persistence/sqlite/sqlite-unit-of-work.d.ts +8 -0
  196. package/dist/persistence/sqlite/sqlite-unit-of-work.js +41 -0
  197. package/dist/persistence/sqlite/sqlite-unit-of-work.js.map +1 -0
  198. package/dist/persistence/sqlite/types.d.ts +24 -0
  199. package/dist/persistence/sqlite/types.js +28 -0
  200. package/dist/persistence/sqlite/types.js.map +1 -0
  201. package/dist/persistence/unit-of-work.d.ts +6 -0
  202. package/dist/persistence/unit-of-work.js +15 -0
  203. package/dist/persistence/unit-of-work.js.map +1 -0
  204. package/dist/proposal/accepted-claim-appender.d.ts +7 -0
  205. package/dist/proposal/accepted-claim-appender.js +16 -0
  206. package/dist/proposal/accepted-claim-appender.js.map +1 -0
  207. package/dist/proposal/in-memory-proposal-queue.d.ts +21 -0
  208. package/dist/proposal/in-memory-proposal-queue.js +218 -0
  209. package/dist/proposal/in-memory-proposal-queue.js.map +1 -0
  210. package/dist/proposal/policy-aware-proposal-queue.d.ts +23 -0
  211. package/dist/proposal/policy-aware-proposal-queue.js +162 -0
  212. package/dist/proposal/policy-aware-proposal-queue.js.map +1 -0
  213. package/dist/proposal/proposal-policy.d.ts +37 -0
  214. package/dist/proposal/proposal-policy.js +72 -0
  215. package/dist/proposal/proposal-policy.js.map +1 -0
  216. package/dist/proposal/proposal-queue.d.ts +13 -0
  217. package/dist/proposal/proposal-queue.js +15 -0
  218. package/dist/proposal/proposal-queue.js.map +1 -0
  219. package/dist/proposal/source-aware-claim-appender.d.ts +13 -0
  220. package/dist/proposal/source-aware-claim-appender.js +47 -0
  221. package/dist/proposal/source-aware-claim-appender.js.map +1 -0
  222. package/dist/proposal/sqlite-proposal-queue.d.ts +26 -0
  223. package/dist/proposal/sqlite-proposal-queue.js +282 -0
  224. package/dist/proposal/sqlite-proposal-queue.js.map +1 -0
  225. package/dist/resolution/resolve-claims.d.ts +4 -0
  226. package/dist/resolution/resolve-claims.js +101 -0
  227. package/dist/resolution/resolve-claims.js.map +1 -0
  228. package/dist/resolution/trust-profile.d.ts +12 -0
  229. package/dist/resolution/trust-profile.js +23 -0
  230. package/dist/resolution/trust-profile.js.map +1 -0
  231. package/dist/resolution/types.d.ts +15 -0
  232. package/dist/resolution/types.js +8 -0
  233. package/dist/resolution/types.js.map +1 -0
  234. package/dist/sdk/create-backend-integrated-life-api-for-test.d.ts +23 -0
  235. package/dist/sdk/create-backend-integrated-life-api-for-test.js +240 -0
  236. package/dist/sdk/create-backend-integrated-life-api-for-test.js.map +1 -0
  237. package/dist/sdk/create-life-api.d.ts +55 -0
  238. package/dist/sdk/create-life-api.js +251 -0
  239. package/dist/sdk/create-life-api.js.map +1 -0
  240. package/dist/sdk/create-sqlite-life-api.d.ts +33 -0
  241. package/dist/sdk/create-sqlite-life-api.js +654 -0
  242. package/dist/sdk/create-sqlite-life-api.js.map +1 -0
  243. package/dist/sdk/life-api.d.ts +27 -0
  244. package/dist/sdk/life-api.js +17 -0
  245. package/dist/sdk/life-api.js.map +1 -0
  246. package/dist/sdk/validation.d.ts +8 -0
  247. package/dist/sdk/validation.js +216 -0
  248. package/dist/sdk/validation.js.map +1 -0
  249. package/dist/source/in-memory-source-registry.d.ts +15 -0
  250. package/dist/source/in-memory-source-registry.js +104 -0
  251. package/dist/source/in-memory-source-registry.js.map +1 -0
  252. package/dist/source/source-aware-fact-store.d.ts +19 -0
  253. package/dist/source/source-aware-fact-store.js +40 -0
  254. package/dist/source/source-aware-fact-store.js.map +1 -0
  255. package/dist/source/source-kind-predicate-policy.d.ts +4 -0
  256. package/dist/source/source-kind-predicate-policy.js +62 -0
  257. package/dist/source/source-kind-predicate-policy.js.map +1 -0
  258. package/dist/source/source-registry.d.ts +11 -0
  259. package/dist/source/source-registry.js +12 -0
  260. package/dist/source/source-registry.js.map +1 -0
  261. package/dist/store/in-memory-fact-store.d.ts +25 -0
  262. package/dist/store/in-memory-fact-store.js +192 -0
  263. package/dist/store/in-memory-fact-store.js.map +1 -0
  264. package/dist/surface/context-query.d.ts +21 -0
  265. package/dist/surface/context-query.js +113 -0
  266. package/dist/surface/context-query.js.map +1 -0
  267. package/dist/surface/evidence-backed-context-query.d.ts +62 -0
  268. package/dist/surface/evidence-backed-context-query.js +240 -0
  269. package/dist/surface/evidence-backed-context-query.js.map +1 -0
  270. package/dist/types/access-context.d.ts +25 -0
  271. package/dist/types/access-context.js +6 -0
  272. package/dist/types/access-context.js.map +1 -0
  273. package/dist/types/claim.d.ts +48 -0
  274. package/dist/types/claim.js +5 -0
  275. package/dist/types/claim.js.map +1 -0
  276. package/dist/types/consent.d.ts +41 -0
  277. package/dist/types/consent.js +3 -0
  278. package/dist/types/consent.js.map +1 -0
  279. package/dist/types/crypto-boundary.d.ts +12 -0
  280. package/dist/types/crypto-boundary.js +15 -0
  281. package/dist/types/crypto-boundary.js.map +1 -0
  282. package/dist/types/evidence.d.ts +31 -0
  283. package/dist/types/evidence.js +8 -0
  284. package/dist/types/evidence.js.map +1 -0
  285. package/dist/types/fact-store.d.ts +24 -0
  286. package/dist/types/fact-store.js +12 -0
  287. package/dist/types/fact-store.js.map +1 -0
  288. package/dist/types/ids.d.ts +36 -0
  289. package/dist/types/ids.js +11 -0
  290. package/dist/types/ids.js.map +1 -0
  291. package/dist/types/proposal.d.ts +38 -0
  292. package/dist/types/proposal.js +7 -0
  293. package/dist/types/proposal.js.map +1 -0
  294. package/dist/types/source.d.ts +33 -0
  295. package/dist/types/source.js +6 -0
  296. package/dist/types/source.js.map +1 -0
  297. package/dist/types/trust.d.ts +24 -0
  298. package/dist/types/trust.js +5 -0
  299. package/dist/types/trust.js.map +1 -0
  300. package/dist/utilities/deep-freeze.d.ts +1 -0
  301. package/dist/utilities/deep-freeze.js +17 -0
  302. package/dist/utilities/deep-freeze.js.map +1 -0
  303. package/package.json +55 -0
@@ -0,0 +1,802 @@
1
+ // Production-06d-d-b — ProductionScopedLifeApi facade.
2
+ // Production Auth Closure M3 Runtime — rejectProposal facade added (owner-only).
3
+ // Production Auth Closure M2 Runtime — createProposal facade added (agent + owner-self).
4
+ //
5
+ // PRODUCTION_API_ERASURE_SUBJECT_SCOPE_ENFORCED_001: production facade enforces
6
+ // owner-self-only erasure — kind check + personId self-match.
7
+ // PRODUCTION_API_ERASURE_OWNER_SELF_REQUIRED_001: requestErasure / getErasureStatus
8
+ // require owner kind AND ctx.personId === subject; agents denied even if subject-authorized.
9
+ // PRODUCTION_API_ERASURE_AGENT_DENIED_001: thrown when mintedCtx.context.kind !== "owner".
10
+ // PRODUCTION_API_ERASURE_CROSS_SUBJECT_DENIED_001: thrown when owner.personId !== subject.
11
+ // PRODUCTION_API_QUERY_SUBJECT_SCOPE_ENFORCED_001: queryContracts delegates to
12
+ // SubjectScopedLifeApi which enforces Cases A/B/C.
13
+ // PRODUCTION_API_PROPOSAL_SCOPE_ENFORCED_001: proposal operations delegate to
14
+ // ProposalScopedLifeApi which runs the full ProposalScopeInspector chain.
15
+ // PRODUCTION_API_PROPOSAL_ACCEPT_SCOPE_ENFORCED_001: acceptProposal enforces proposal
16
+ // subject-scope (empty candidates, multi-subject, subject mismatch all denied).
17
+ // Post-MVP Proposal Read Facade — scoped getProposal/listProposals (spec 9c4ecf2, accepted):
18
+ // PRODUCTION_API_GET_PROPOSAL_SUBJECT_SCOPE_ENFORCED_001: getProposal enforces subject scope;
19
+ // cross-subject/unknown-subject proposals return undefined.
20
+ // PRODUCTION_API_GET_PROPOSAL_SAFE_NOT_FOUND_001: missing/cross-subject/unknown/multi all → undefined.
21
+ // PRODUCTION_API_LIST_PROPOSALS_SUBJECT_SCOPE_ENFORCED_001: listProposals auto-scoped to bound personId.
22
+ // PRODUCTION_API_LIST_PROPOSALS_NO_CROSS_SUBJECT_RESULTS_001: result contains only own-subject proposals.
23
+ // PRODUCTION_API_PROPOSAL_READ_AGENT_AUTHORIZED_PERSON_REQUIRED_001: agent without authorizedPersonId
24
+ // throws before any proposal fetch.
25
+ // PRODUCTION_API_PROPOSAL_READ_UNSUPPORTED_CONTEXT_DENIED_001: non-owner/non-agent denied.
26
+ // PRODUCTION_API_PROPOSAL_READ_UNKNOWN_SUBJECT_DENIED_001: empty candidateClaims → safe-not-found/omit.
27
+ // PRODUCTION_API_PROPOSAL_READ_MULTI_SUBJECT_DENIED_001: multi-subject → same treatment.
28
+ // PRODUCTION_API_PROPOSAL_READ_NO_RAW_BYPASS_001: raw LifeApi.getProposal/listProposals not re-exposed.
29
+ // PRODUCTION_FACTORY_ASSEMBLY_DEFERRED_001: remains active — factory still Promise<never>;
30
+ // this file defines the facade type/impl; factory wiring is a separate slice.
31
+ //
32
+ // M2 createProposal invariants:
33
+ // PRODUCTION_API_CREATE_PROPOSAL_AGENT_SUBJECT_SCOPE_ENFORCED_001: agent path requires all
34
+ // candidateClaims.subject === authorizedPersonId.
35
+ // PRODUCTION_API_CREATE_PROPOSAL_AGENT_AUTHORIZED_PERSON_REQUIRED_001: authorizedPersonId must
36
+ // be present on agent minted context; absent → fail closed.
37
+ // PRODUCTION_API_CREATE_PROPOSAL_CANDIDATE_SUBJECT_REQUIRED_001: candidateClaims must be
38
+ // non-empty (shared: agent + owner).
39
+ // PRODUCTION_API_CREATE_PROPOSAL_MULTI_SUBJECT_DENIED_001: candidateClaims with multiple
40
+ // distinct subjects denied (shared: agent + owner).
41
+ // PRODUCTION_API_CREATE_PROPOSAL_CROSS_SUBJECT_DENIED_001: agent: subject ≠ authorizedPersonId.
42
+ // PRODUCTION_API_CREATE_PROPOSAL_OWNER_SELF_SCOPE_ENFORCED_001: owner path requires all
43
+ // candidateClaims.subject === ownerCtx.personId (Option A).
44
+ // PRODUCTION_API_CREATE_PROPOSAL_OWNER_CROSS_SUBJECT_DENIED_001: owner: subject ≠ personId.
45
+ // PRODUCTION_API_CREATE_PROPOSAL_UNSUPPORTED_CONTEXT_DENIED_001: non-owner/non-agent kinds
46
+ // denied before input inspection.
47
+ // PRODUCTION_API_CREATE_PROPOSAL_NO_RAW_BYPASS_001: all callers must use
48
+ // ProductionScopedLifeApi.createProposal; raw LifeApi.createProposal not re-exposed.
49
+ //
50
+ // Platform-03 — Guardianship facade wiring:
51
+ // GUARDIAN_SCOPE_ENFORCED_AT_FACADE_001: _assertGuardianScope gates acceptProposal,
52
+ // rejectProposal (shares the "proposal:accept" scope with acceptProposal), and
53
+ // createProposal ("proposal:create") — called at the top, before any other logic.
54
+ // GUARDIAN_READ_IMPLIED_BY_MINT_001: queryContracts, getProposal, listProposals receive NO
55
+ // extra scope check — read access is implied by a successfully minted guardian context.
56
+ // GUARDIAN_ERASURE_DENIED_001: requestErasure / getErasureStatus are ALWAYS denied for
57
+ // guardian contexts, regardless of guardianScopes — erasure is owner-personal and
58
+ // non-delegable. Without this explicit gate, a guardian's owner-context personId === wardId
59
+ // would otherwise pass assertOwnerSelfForErasure (context.kind === "owner" and
60
+ // personId === wardId === subject), silently allowing delegated erasure.
61
+ // GUARDIAN_GRANT_CREATED_CODE_DECLARED_HOST_EMITTED_001: there is no grant-creation method
62
+ // on this facade — GUARDIAN_GRANT_CREATED is declared in AuditReasonCode for host-side
63
+ // grant stores to emit; no method here produces it.
64
+ // GUARDIAN_QUERY_AUDIT_MARKING_DEFERRED_001: audit events written internally by
65
+ // EvidenceBackedContextQueryService (deep inside queryContracts) do NOT carry guardian
66
+ // marking — deferred; src/surface/evidence-backed-context-query.ts is not modified here.
67
+ import { buildAuditEvent } from "../audit/audit-log.js";
68
+ import { asISOTime } from "../types/ids.js";
69
+ import { SubjectScopedLifeApi } from "./subject-scoped-life-api.js";
70
+ import { ProposalScopedLifeApi } from "./proposal-scoped-life-api.js";
71
+ import { safeLog } from "../observability/logger.js";
72
+ import { safeRecord } from "../observability/metrics.js";
73
+ // ─── assertOwnerSelfForErasure ────────────────────────────────────────────────
74
+ // PRODUCTION_API_ERASURE_OWNER_SELF_REQUIRED_001: enforces both conditions:
75
+ // 1. context.kind === "owner" (PRODUCTION_API_ERASURE_AGENT_DENIED_001 if not)
76
+ // 2. owner.personId === subject (PRODUCTION_API_ERASURE_CROSS_SUBJECT_DENIED_001 if not)
77
+ //
78
+ // Why not assertAuthorizedForSubject: an AgentConsentContext with authorizedPersonId === X
79
+ // passes assertAuthorizedForSubject for subject X — but agents must never trigger or read
80
+ // erasure operations. The kind check blocks agents (and all non-owner contexts) unconditionally.
81
+ export function assertOwnerSelfForErasure(subject, mintedCtx) {
82
+ if (mintedCtx.context.kind !== "owner") {
83
+ throw Object.assign(new Error(`PRODUCTION_API_ERASURE_AGENT_DENIED_001: context kind "${mintedCtx.context.kind}" ` +
84
+ `may not request or read erasure; only the subject's owner context is permitted`), { code: "PRODUCTION_API_ERASURE_AGENT_DENIED_001" });
85
+ }
86
+ const ownerCtx = mintedCtx.context;
87
+ if (ownerCtx.personId !== subject) {
88
+ throw Object.assign(new Error(`PRODUCTION_API_ERASURE_CROSS_SUBJECT_DENIED_001: owner personId does not match ` +
89
+ `erasure subject; cross-subject erasure is not permitted`), { code: "PRODUCTION_API_ERASURE_CROSS_SUBJECT_DENIED_001" });
90
+ }
91
+ }
92
+ // ─── assertOwnerOwnsProposalSubjectForReject ──────────────────────────────────
93
+ // PRODUCTION_API_REJECT_PROPOSAL_OWNER_SUBJECT_SCOPE_ENFORCED_001
94
+ // PRODUCTION_API_REJECT_PROPOSAL_UNKNOWN_SUBJECT_DENIED_001
95
+ export function assertOwnerOwnsProposalSubjectForReject(proposal, mintedCtx) {
96
+ const { candidateClaims } = proposal;
97
+ if (candidateClaims.length === 0) {
98
+ throw Object.assign(new Error("PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001: proposal has no candidate claims — " +
99
+ "subject cannot be determined"), { code: "PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001" });
100
+ }
101
+ const uniqueSubjects = new Set(candidateClaims.map(c => c.subject));
102
+ if (uniqueSubjects.size !== 1) {
103
+ throw Object.assign(new Error("PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001: proposal has multiple candidate subjects — " +
104
+ "subject cannot be determined"), { code: "PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001" });
105
+ }
106
+ const proposalSubject = [...uniqueSubjects][0];
107
+ // Use ownerCtx.personId — NOT authorizedPersonId
108
+ // PRODUCTION_API_REJECT_PROPOSAL_OWNER_SUBJECT_SCOPE_ENFORCED_001
109
+ const ownerCtx = mintedCtx.context;
110
+ if (ownerCtx.personId !== proposalSubject) {
111
+ throw Object.assign(new Error("PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001: proposal subject does not match owner"), { code: "PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001" });
112
+ }
113
+ }
114
+ // ─── extractSingleSubject ─────────────────────────────────────────────────────
115
+ // Returns the unique candidate subject as string, or undefined if:
116
+ // - candidateClaims is empty (PRODUCTION_API_PROPOSAL_READ_UNKNOWN_SUBJECT_DENIED_001)
117
+ // - candidateClaims span > 1 distinct subjects (PRODUCTION_API_PROPOSAL_READ_MULTI_SUBJECT_DENIED_001)
118
+ // Never throws — callers decide what to do with undefined.
119
+ // Module-private: not exported.
120
+ function extractSingleSubject(proposal) {
121
+ const { candidateClaims } = proposal;
122
+ if (candidateClaims.length === 0)
123
+ return undefined;
124
+ const subjects = new Set(candidateClaims.map(c => c.subject));
125
+ if (subjects.size !== 1)
126
+ return undefined;
127
+ return [...subjects][0];
128
+ }
129
+ // ─── resolveProposalReadSubject ───────────────────────────────────────────────
130
+ // Returns the bound person scope for proposal reads, or throws a named error.
131
+ // Owner → ownerCtx.personId
132
+ // Agent → authorizedPersonId (absent → throw PRODUCTION_API_PROPOSAL_READ_AGENT_AUTHORIZED_PERSON_REQUIRED_001)
133
+ // Other → throw PRODUCTION_API_PROPOSAL_READ_UNSUPPORTED_CONTEXT_DENIED_001
134
+ // Module-private: not exported.
135
+ function resolveProposalReadSubject(mintedCtx) {
136
+ const kind = mintedCtx.context.kind;
137
+ if (kind === "owner") {
138
+ const ownerCtx = mintedCtx.context;
139
+ return ownerCtx.personId;
140
+ }
141
+ if (kind === "agent") {
142
+ const authorizedPersonId = mintedCtx.authorizedPersonId;
143
+ if (authorizedPersonId === undefined) {
144
+ throw Object.assign(new Error("PRODUCTION_API_PROPOSAL_READ_AGENT_AUTHORIZED_PERSON_REQUIRED_001: " +
145
+ "authorizedPersonId missing from minted agent context — broad grants not permitted for proposal reads"), { code: "PRODUCTION_API_PROPOSAL_READ_AGENT_AUTHORIZED_PERSON_REQUIRED_001" });
146
+ }
147
+ return authorizedPersonId;
148
+ }
149
+ throw Object.assign(new Error(`PRODUCTION_API_PROPOSAL_READ_UNSUPPORTED_CONTEXT_DENIED_001: ` +
150
+ `context kind "${kind}" is not permitted to read proposals`), { code: "PRODUCTION_API_PROPOSAL_READ_UNSUPPORTED_CONTEXT_DENIED_001" });
151
+ }
152
+ // ─── ProductionScopedLifeApiImpl ──────────────────────────────────────────────
153
+ export class ProductionScopedLifeApiImpl {
154
+ _api;
155
+ _minted;
156
+ _audit;
157
+ _mutationLog;
158
+ _uow;
159
+ _logger;
160
+ _metricsRecorder;
161
+ _subjectApi;
162
+ _proposalApi;
163
+ // PRODUCTION_AUDIT_ALLOW_MUTATION_ATOMIC_001 (C2a): _mutationLog is the internal allow-path
164
+ // audit log sharing config.db; _uow wraps allow-path audit write + mutation in one SAVEPOINT.
165
+ // _audit remains the caller-supplied deny-path / standalone audit log.
166
+ constructor(_api, _minted, _audit, _mutationLog, _uow, _logger, _metricsRecorder) {
167
+ this._api = _api;
168
+ this._minted = _minted;
169
+ this._audit = _audit;
170
+ this._mutationLog = _mutationLog;
171
+ this._uow = _uow;
172
+ this._logger = _logger;
173
+ this._metricsRecorder = _metricsRecorder;
174
+ this._subjectApi = new SubjectScopedLifeApi(_api, _minted);
175
+ this._proposalApi = new ProposalScopedLifeApi(_api, _minted, _audit, _mutationLog, _uow, _logger);
176
+ }
177
+ // Emit proposal.mutation.outcome counter. Swallowed via safeRecord.
178
+ // PRODUCTION_METRICS_FAILURE_NON_INTERFERING_001
179
+ _emitProposalOutcome(operation, status, contextKind) {
180
+ safeRecord(this._metricsRecorder, "proposal.mutation.outcome", 1, {
181
+ component: "ProductionScopedLifeApiImpl",
182
+ operation,
183
+ status,
184
+ ...(contextKind !== undefined ? { contextKind } : {}),
185
+ });
186
+ }
187
+ // PRODUCTION_AUDIT_FAIL_CLOSED_001: if the audit write fails, throw the fail-closed
188
+ // error — NOT the original denial. Operation must not proceed without a recorded event.
189
+ async _writeAuditOrFail(event) {
190
+ try {
191
+ await this._audit.write(event);
192
+ }
193
+ catch (err) {
194
+ safeLog(this._logger, {
195
+ level: "error", code: "LOG_AUDIT_WRITE_FAILED_001",
196
+ component: "ProductionScopedLifeApiImpl", operation: "_writeAuditOrFail",
197
+ message: "audit write failed — operation aborted fail-closed",
198
+ timestamp: new Date().toISOString(),
199
+ ...(err instanceof Error ? { errorName: err.name } : {}),
200
+ });
201
+ safeRecord(this._metricsRecorder, "audit.write.failure", 1, {
202
+ component: "ProductionScopedLifeApiImpl", operation: "_writeAuditOrFail", status: "failed",
203
+ });
204
+ throw Object.assign(new Error("PRODUCTION_AUDIT_FAIL_CLOSED_001: audit write failed — operation aborted fail-closed"), { code: "PRODUCTION_AUDIT_FAIL_CLOSED_001" });
205
+ }
206
+ }
207
+ _actorLabel() {
208
+ return `${this._minted.principalType}:${this._minted.principalId}`;
209
+ }
210
+ _now() {
211
+ return asISOTime(new Date().toISOString());
212
+ }
213
+ // Platform-03 — Guardianship: spread into every buildAuditEvent call in this file.
214
+ // GUARDIAN_AUDIT_MARKED_001: only set when this context is a guardian context.
215
+ _guardianFields() {
216
+ return this._minted.guardianOf !== undefined
217
+ ? { actingAsGuardian: true, wardId: this._minted.guardianOf }
218
+ : {};
219
+ }
220
+ // GUARDIAN_SCOPE_ENFORCED_AT_FACADE_001: guardian contexts must carry requiredScope in
221
+ // guardianScopes. Non-guardian contexts (guardianOf undefined) are unaffected — no-op.
222
+ async _assertGuardianScope(operation, requiredScope) {
223
+ if (this._minted.guardianOf === undefined)
224
+ return; // not a guardian context — no extra gate
225
+ const scopes = this._minted.guardianScopes ?? [];
226
+ if (scopes.includes(requiredScope))
227
+ return;
228
+ const wardId = this._minted.guardianOf;
229
+ await this._writeAuditOrFail(buildAuditEvent({
230
+ actor: this._actorLabel(),
231
+ decision: "deny",
232
+ reasonCode: "GUARDIAN_ACTION_UNAUTHORIZED",
233
+ selectorFingerprint: `guardian:${operation}:ward=${wardId}`,
234
+ resultCount: 0,
235
+ resultClaimIds: [],
236
+ actingAsGuardian: true,
237
+ wardId,
238
+ now: this._now(),
239
+ }));
240
+ throw Object.assign(new Error(`GUARDIAN_ACTION_UNAUTHORIZED_001: operation "${operation}" is not permitted — ` +
241
+ `the guardianship scope does not include "${requiredScope}"`), { code: "GUARDIAN_ACTION_UNAUTHORIZED_001" });
242
+ }
243
+ // GUARDIAN_ERASURE_DENIED_001: erasure is owner-personal and non-delegable — guardian
244
+ // contexts are always denied, regardless of guardianScopes. Called BEFORE
245
+ // assertOwnerSelfForErasure in both requestErasure and getErasureStatus.
246
+ async _denyGuardianErasure(operation) {
247
+ if (this._minted.guardianOf === undefined)
248
+ return;
249
+ const wardId = this._minted.guardianOf;
250
+ await this._writeAuditOrFail(buildAuditEvent({
251
+ actor: this._actorLabel(),
252
+ decision: "deny",
253
+ reasonCode: "GUARDIAN_ACTION_UNAUTHORIZED",
254
+ selectorFingerprint: `guardian:${operation}:ward=${wardId}`,
255
+ resultCount: 0,
256
+ resultClaimIds: [],
257
+ actingAsGuardian: true,
258
+ wardId,
259
+ now: this._now(),
260
+ }));
261
+ throw Object.assign(new Error("GUARDIAN_ERASURE_DENIED_001: erasure is owner-personal and non-delegable — " +
262
+ "guardian contexts may never request or read erasure status"), { code: "GUARDIAN_ERASURE_DENIED_001" });
263
+ }
264
+ // PRODUCTION_API_ERASURE_SUBJECT_SCOPE_ENFORCED_001: assertOwnerSelfForErasure fires
265
+ // before any delegation. Agent contexts are denied regardless of their authorizedPersonId.
266
+ // PRODUCTION_AUDIT_ERASURE_DENIAL_RECORDED_001: deny paths recorded via _audit (caller-supplied,
267
+ // rollback-independent). Allow path audit is written by the underlying LifeApi.requestErasure
268
+ // into _erasureMutationAuditLog (C2b); double-auditing is avoided.
269
+ // PRODUCTION_AUDIT_REQUEST_ERASURE_AUDIT_ATOMIC_001 (C2b): _uow.run() provides the SAVEPOINT
270
+ // scope — the underlying api writes audit + tombstone on config.db inside the same SAVEPOINT.
271
+ async requestErasure(subject) {
272
+ // GUARDIAN_ERASURE_DENIED_001: fires before assertOwnerSelfForErasure — guardian
273
+ // contexts are always denied, regardless of guardianScopes.
274
+ await this._denyGuardianErasure("requestErasure");
275
+ try {
276
+ assertOwnerSelfForErasure(subject, this._minted);
277
+ }
278
+ catch (err) {
279
+ const code = err.code;
280
+ const reasonCode = code === "PRODUCTION_API_ERASURE_CROSS_SUBJECT_DENIED_001"
281
+ ? "ERASURE_CROSS_SUBJECT_DENIED"
282
+ : "ERASURE_AGENT_DENIED";
283
+ await this._writeAuditOrFail(buildAuditEvent({
284
+ actor: this._actorLabel(),
285
+ decision: "deny",
286
+ reasonCode,
287
+ selectorFingerprint: `erasure:subject:${subject}`,
288
+ resultCount: 0,
289
+ resultClaimIds: [],
290
+ now: this._now(),
291
+ ...this._guardianFields(),
292
+ }));
293
+ throw err;
294
+ }
295
+ const ownerCtx = this._minted.context;
296
+ try {
297
+ return await this._uow.run(async () => this._api.requestErasure(ownerCtx, subject));
298
+ }
299
+ catch (err) {
300
+ const code = err.code;
301
+ const isKnownCode = typeof code === "string" && (code === "PRODUCTION_AUDIT_FAIL_CLOSED_001" ||
302
+ code === "ERASURE_SUBJECT_ALREADY_ERASED_001" ||
303
+ code === "ERASURE_REQUEST_DENIED_001");
304
+ if (!isKnownCode) {
305
+ safeLog(this._logger, {
306
+ level: "warn", code: "LOG_REQUEST_ERASURE_ROLLBACK_001",
307
+ component: "ProductionScopedLifeApiImpl", operation: "requestErasure",
308
+ message: "requestErasure allow-path rolled back due to internal error",
309
+ timestamp: new Date().toISOString(),
310
+ ...(err instanceof Error ? { errorName: err.name } : {}),
311
+ });
312
+ safeRecord(this._metricsRecorder, "allow_path.rollback", 1, {
313
+ component: "ProductionScopedLifeApiImpl", operation: "requestErasure", status: "failed",
314
+ });
315
+ }
316
+ throw err;
317
+ }
318
+ }
319
+ // PRODUCTION_AUDIT_ERASURE_DENIAL_RECORDED_001: deny paths recorded.
320
+ // PRODUCTION_AUDIT_READ_ALLOW_PATH_RECORDED_001 (C3): allow path fail-closed.
321
+ async getErasureStatus(subject) {
322
+ // GUARDIAN_ERASURE_DENIED_001: fires before assertOwnerSelfForErasure — guardian
323
+ // contexts are always denied, regardless of guardianScopes.
324
+ await this._denyGuardianErasure("getErasureStatus");
325
+ try {
326
+ assertOwnerSelfForErasure(subject, this._minted);
327
+ }
328
+ catch (err) {
329
+ const code = err.code;
330
+ const reasonCode = code === "PRODUCTION_API_ERASURE_CROSS_SUBJECT_DENIED_001"
331
+ ? "ERASURE_CROSS_SUBJECT_DENIED"
332
+ : "ERASURE_AGENT_DENIED";
333
+ await this._writeAuditOrFail(buildAuditEvent({
334
+ actor: this._actorLabel(),
335
+ decision: "deny",
336
+ reasonCode,
337
+ selectorFingerprint: `erasure:subject:${subject}`,
338
+ resultCount: 0,
339
+ resultClaimIds: [],
340
+ now: this._now(),
341
+ ...this._guardianFields(),
342
+ }));
343
+ throw err;
344
+ }
345
+ const ownerCtx = this._minted.context;
346
+ const status = await this._api.getErasureStatus(ownerCtx, subject);
347
+ await this._writeAuditOrFail(buildAuditEvent({
348
+ actor: this._actorLabel(),
349
+ decision: "allow",
350
+ reasonCode: "ERASURE_STATUS_ALLOWED",
351
+ selectorFingerprint: `erasure:status:subject=${subject}`,
352
+ resultCount: 0,
353
+ resultClaimIds: [],
354
+ now: this._now(),
355
+ ...this._guardianFields(),
356
+ }));
357
+ return status;
358
+ }
359
+ // PRODUCTION_API_QUERY_SUBJECT_SCOPE_ENFORCED_001: delegates to SubjectScopedLifeApi
360
+ // which enforces Cases A (subject match), B (person-scoped deny no-subject), C (pass).
361
+ // GUARDIAN_READ_IMPLIED_BY_MINT_001: no additional guardian scope check — read access is
362
+ // implied by a successfully minted guardian context.
363
+ // GUARDIAN_QUERY_AUDIT_MARKING_DEFERRED_001: internal audit events written deep inside
364
+ // EvidenceBackedContextQueryService do NOT carry actingAsGuardian/wardId — deferred.
365
+ async queryContracts(input) {
366
+ return this._subjectApi.queryContracts(input);
367
+ }
368
+ // PRODUCTION_API_PROPOSAL_ACCEPT_SCOPE_ENFORCED_001: delegates to ProposalScopedLifeApi
369
+ // which enforces the full ProposalScopeInspector chain before delegation.
370
+ // GUARDIAN_SCOPE_ENFORCED_AT_FACADE_001: acceptProposal and rejectProposal share the
371
+ // "proposal:accept" scope (both are proposal decisions).
372
+ async acceptProposal(proposalId, options) {
373
+ await this._assertGuardianScope("acceptProposal", "proposal:accept");
374
+ try {
375
+ const result = await this._proposalApi.acceptProposal(proposalId, options);
376
+ this._emitProposalOutcome("acceptProposal", "success", this._minted.context.kind);
377
+ return result;
378
+ }
379
+ catch (err) {
380
+ const code = err.code;
381
+ const status = code === "PRODUCTION_AUDIT_FAIL_CLOSED_001" ? "failed" : "denied";
382
+ this._emitProposalOutcome("acceptProposal", status, this._minted.context.kind);
383
+ throw err;
384
+ }
385
+ }
386
+ // PRODUCTION_API_CREATE_PROPOSAL_NO_RAW_BYPASS_001
387
+ // PRODUCTION_API_CREATE_PROPOSAL_UNSUPPORTED_CONTEXT_DENIED_001: denied before input inspection.
388
+ // PRODUCTION_AUDIT_PROPOSAL_MUTATION_RECORDED_001 / PRODUCTION_AUDIT_FACADE_DENIAL_RECORDED_001:
389
+ // every deny path and the allow path (before mutation) is recorded.
390
+ async createProposal(input) {
391
+ await this._assertGuardianScope("createProposal", "proposal:create");
392
+ const kind = this._minted.context.kind;
393
+ if (kind === "owner") {
394
+ // PRODUCTION_API_CREATE_PROPOSAL_OWNER_SELF_SCOPE_ENFORCED_001 (Option A)
395
+ const ownerCtx = this._minted.context;
396
+ if (input.candidateClaims.length === 0) {
397
+ await this._writeAuditOrFail(buildAuditEvent({
398
+ actor: this._actorLabel(),
399
+ decision: "deny",
400
+ reasonCode: "PROPOSAL_EMPTY_CANDIDATES_DENIED",
401
+ selectorFingerprint: "proposal:create:owner:empty-candidates",
402
+ resultCount: 0,
403
+ resultClaimIds: [],
404
+ now: this._now(),
405
+ ...this._guardianFields(),
406
+ }));
407
+ this._emitProposalOutcome("createProposal", "denied", kind);
408
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_CANDIDATE_SUBJECT_REQUIRED_001: " +
409
+ "candidateClaims must not be empty"), { code: "PRODUCTION_API_CREATE_PROPOSAL_CANDIDATE_SUBJECT_REQUIRED_001" });
410
+ }
411
+ const ownerSubjects = new Set(input.candidateClaims.map(c => c.subject));
412
+ if (ownerSubjects.size !== 1) {
413
+ await this._writeAuditOrFail(buildAuditEvent({
414
+ actor: this._actorLabel(),
415
+ decision: "deny",
416
+ reasonCode: "PROPOSAL_MULTI_SUBJECT_DENIED",
417
+ selectorFingerprint: "proposal:create:owner:multi-subject",
418
+ resultCount: 0,
419
+ resultClaimIds: [],
420
+ now: this._now(),
421
+ ...this._guardianFields(),
422
+ }));
423
+ this._emitProposalOutcome("createProposal", "denied", kind);
424
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_MULTI_SUBJECT_DENIED_001: " +
425
+ "candidateClaims must resolve to exactly one subject"), { code: "PRODUCTION_API_CREATE_PROPOSAL_MULTI_SUBJECT_DENIED_001" });
426
+ }
427
+ const ownerProposalSubject = [...ownerSubjects][0];
428
+ if (ownerProposalSubject !== ownerCtx.personId) {
429
+ await this._writeAuditOrFail(buildAuditEvent({
430
+ actor: this._actorLabel(),
431
+ decision: "deny",
432
+ reasonCode: "PROPOSAL_CROSS_SUBJECT_DENIED",
433
+ selectorFingerprint: `proposal:create:owner:cross-subject:subject=${ownerProposalSubject}`,
434
+ resultCount: 0,
435
+ resultClaimIds: [],
436
+ now: this._now(),
437
+ ...this._guardianFields(),
438
+ }));
439
+ this._emitProposalOutcome("createProposal", "denied", kind);
440
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_OWNER_CROSS_SUBJECT_DENIED_001: " +
441
+ "proposal subject does not match owner personId — cross-subject creation is not permitted"), { code: "PRODUCTION_API_CREATE_PROPOSAL_OWNER_CROSS_SUBJECT_DENIED_001" });
442
+ }
443
+ // Allow path — PRODUCTION_AUDIT_ALLOW_MUTATION_ATOMIC_001 (C2a): audit + mutation atomic.
444
+ const ownerAllowEvent = buildAuditEvent({
445
+ actor: this._actorLabel(),
446
+ decision: "allow",
447
+ reasonCode: "PROPOSAL_ALLOWED",
448
+ selectorFingerprint: `proposal:create:subject=${ownerProposalSubject}:candidateCount=${input.candidateClaims.length}`,
449
+ resultCount: 0,
450
+ resultClaimIds: [],
451
+ now: this._now(),
452
+ ...this._guardianFields(),
453
+ });
454
+ const ownerResult = await this._uow.run(async () => {
455
+ try {
456
+ await this._mutationLog.write(ownerAllowEvent);
457
+ }
458
+ catch (err) {
459
+ safeLog(this._logger, {
460
+ level: "error", code: "LOG_AUDIT_WRITE_FAILED_001",
461
+ component: "ProductionScopedLifeApiImpl", operation: "createProposal",
462
+ message: "audit write failed — operation aborted fail-closed",
463
+ timestamp: new Date().toISOString(),
464
+ ...(err instanceof Error ? { errorName: err.name } : {}),
465
+ });
466
+ safeRecord(this._metricsRecorder, "audit.write.failure", 1, {
467
+ component: "ProductionScopedLifeApiImpl", operation: "createProposal", status: "failed",
468
+ });
469
+ this._emitProposalOutcome("createProposal", "failed", kind);
470
+ throw Object.assign(new Error("PRODUCTION_AUDIT_FAIL_CLOSED_001: audit write failed — operation aborted fail-closed"), { code: "PRODUCTION_AUDIT_FAIL_CLOSED_001" });
471
+ }
472
+ return this._api.createProposal(this._minted.context, input);
473
+ });
474
+ this._emitProposalOutcome("createProposal", "success", kind);
475
+ return ownerResult;
476
+ }
477
+ if (kind === "agent") {
478
+ // PRODUCTION_API_CREATE_PROPOSAL_AGENT_SUBJECT_SCOPE_ENFORCED_001
479
+ const authorizedPersonId = this._minted.authorizedPersonId;
480
+ if (authorizedPersonId === undefined) {
481
+ await this._writeAuditOrFail(buildAuditEvent({
482
+ actor: this._actorLabel(),
483
+ decision: "deny",
484
+ reasonCode: "PROPOSAL_BROAD_GRANT_DENIED",
485
+ selectorFingerprint: "proposal:create:agent:broad-grant",
486
+ resultCount: 0,
487
+ resultClaimIds: [],
488
+ now: this._now(),
489
+ ...this._guardianFields(),
490
+ }));
491
+ this._emitProposalOutcome("createProposal", "denied", kind);
492
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_AGENT_AUTHORIZED_PERSON_REQUIRED_001: " +
493
+ "authorizedPersonId missing from minted agent context — broad grants not permitted"), { code: "PRODUCTION_API_CREATE_PROPOSAL_AGENT_AUTHORIZED_PERSON_REQUIRED_001" });
494
+ }
495
+ if (input.candidateClaims.length === 0) {
496
+ await this._writeAuditOrFail(buildAuditEvent({
497
+ actor: this._actorLabel(),
498
+ decision: "deny",
499
+ reasonCode: "PROPOSAL_EMPTY_CANDIDATES_DENIED",
500
+ selectorFingerprint: "proposal:create:agent:empty-candidates",
501
+ resultCount: 0,
502
+ resultClaimIds: [],
503
+ now: this._now(),
504
+ ...this._guardianFields(),
505
+ }));
506
+ this._emitProposalOutcome("createProposal", "denied", kind);
507
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_CANDIDATE_SUBJECT_REQUIRED_001: " +
508
+ "candidateClaims must not be empty"), { code: "PRODUCTION_API_CREATE_PROPOSAL_CANDIDATE_SUBJECT_REQUIRED_001" });
509
+ }
510
+ const agentSubjects = new Set(input.candidateClaims.map(c => c.subject));
511
+ if (agentSubjects.size !== 1) {
512
+ await this._writeAuditOrFail(buildAuditEvent({
513
+ actor: this._actorLabel(),
514
+ decision: "deny",
515
+ reasonCode: "PROPOSAL_MULTI_SUBJECT_DENIED",
516
+ selectorFingerprint: "proposal:create:agent:multi-subject",
517
+ resultCount: 0,
518
+ resultClaimIds: [],
519
+ now: this._now(),
520
+ ...this._guardianFields(),
521
+ }));
522
+ this._emitProposalOutcome("createProposal", "denied", kind);
523
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_MULTI_SUBJECT_DENIED_001: " +
524
+ "candidateClaims must resolve to exactly one subject"), { code: "PRODUCTION_API_CREATE_PROPOSAL_MULTI_SUBJECT_DENIED_001" });
525
+ }
526
+ const agentProposalSubject = [...agentSubjects][0];
527
+ if (agentProposalSubject !== authorizedPersonId) {
528
+ await this._writeAuditOrFail(buildAuditEvent({
529
+ actor: this._actorLabel(),
530
+ decision: "deny",
531
+ reasonCode: "PROPOSAL_CROSS_SUBJECT_DENIED",
532
+ selectorFingerprint: `proposal:create:agent:cross-subject:subject=${agentProposalSubject}`,
533
+ resultCount: 0,
534
+ resultClaimIds: [],
535
+ now: this._now(),
536
+ ...this._guardianFields(),
537
+ }));
538
+ this._emitProposalOutcome("createProposal", "denied", kind);
539
+ throw Object.assign(new Error("PRODUCTION_API_CREATE_PROPOSAL_CROSS_SUBJECT_DENIED_001: " +
540
+ "proposal subject does not match agent authorizedPersonId"), { code: "PRODUCTION_API_CREATE_PROPOSAL_CROSS_SUBJECT_DENIED_001" });
541
+ }
542
+ // Allow path — PRODUCTION_AUDIT_ALLOW_MUTATION_ATOMIC_001 (C2a): audit + mutation atomic.
543
+ const agentAllowEvent = buildAuditEvent({
544
+ actor: this._actorLabel(),
545
+ decision: "allow",
546
+ reasonCode: "PROPOSAL_ALLOWED",
547
+ selectorFingerprint: `proposal:create:subject=${agentProposalSubject}:candidateCount=${input.candidateClaims.length}`,
548
+ resultCount: 0,
549
+ resultClaimIds: [],
550
+ now: this._now(),
551
+ ...this._guardianFields(),
552
+ });
553
+ const agentResult = await this._uow.run(async () => {
554
+ try {
555
+ await this._mutationLog.write(agentAllowEvent);
556
+ }
557
+ catch (err) {
558
+ safeLog(this._logger, {
559
+ level: "error", code: "LOG_AUDIT_WRITE_FAILED_001",
560
+ component: "ProductionScopedLifeApiImpl", operation: "createProposal",
561
+ message: "audit write failed — operation aborted fail-closed",
562
+ timestamp: new Date().toISOString(),
563
+ ...(err instanceof Error ? { errorName: err.name } : {}),
564
+ });
565
+ safeRecord(this._metricsRecorder, "audit.write.failure", 1, {
566
+ component: "ProductionScopedLifeApiImpl", operation: "createProposal", status: "failed",
567
+ });
568
+ this._emitProposalOutcome("createProposal", "failed", kind);
569
+ throw Object.assign(new Error("PRODUCTION_AUDIT_FAIL_CLOSED_001: audit write failed — operation aborted fail-closed"), { code: "PRODUCTION_AUDIT_FAIL_CLOSED_001" });
570
+ }
571
+ return this._api.createProposal(this._minted.context, input);
572
+ });
573
+ this._emitProposalOutcome("createProposal", "success", kind);
574
+ return agentResult;
575
+ }
576
+ await this._writeAuditOrFail(buildAuditEvent({
577
+ actor: this._actorLabel(),
578
+ decision: "deny",
579
+ reasonCode: "PROPOSAL_UNSUPPORTED_CONTEXT_DENIED",
580
+ selectorFingerprint: `proposal:create:context-kind=${kind}`,
581
+ resultCount: 0,
582
+ resultClaimIds: [],
583
+ now: this._now(),
584
+ ...this._guardianFields(),
585
+ }));
586
+ this._emitProposalOutcome("createProposal", "denied", kind);
587
+ throw Object.assign(new Error(`PRODUCTION_API_CREATE_PROPOSAL_UNSUPPORTED_CONTEXT_DENIED_001: ` +
588
+ `context kind "${kind}" is not permitted to create proposals`), { code: "PRODUCTION_API_CREATE_PROPOSAL_UNSUPPORTED_CONTEXT_DENIED_001" });
589
+ }
590
+ // PRODUCTION_API_GET_PROPOSAL_SUBJECT_SCOPE_ENFORCED_001
591
+ // PRODUCTION_API_GET_PROPOSAL_SAFE_NOT_FOUND_001
592
+ // PRODUCTION_API_PROPOSAL_READ_NO_RAW_BYPASS_001
593
+ // PRODUCTION_AUDIT_READ_ALLOW_PATH_RECORDED_001 (C3): allow path fail-closed; safe-not-found best-effort.
594
+ async getProposal(proposalId) {
595
+ // resolveProposalReadSubject throws for agent-without-authorizedPersonId and unsupported kinds.
596
+ // These named errors propagate — they are NOT collapsed to undefined.
597
+ // PRODUCTION_AUDIT_PROPOSAL_DENIAL_RECORDED_001: pre-fetch denials recorded.
598
+ let boundSubject;
599
+ try {
600
+ boundSubject = resolveProposalReadSubject(this._minted);
601
+ }
602
+ catch (err) {
603
+ const isAgentDenied = err.code === "PRODUCTION_API_PROPOSAL_READ_AGENT_AUTHORIZED_PERSON_REQUIRED_001";
604
+ await this._writeAuditOrFail(buildAuditEvent({
605
+ actor: this._actorLabel(),
606
+ decision: "deny",
607
+ reasonCode: isAgentDenied ? "PROPOSAL_READ_AGENT_DENIED" : "PROPOSAL_READ_UNSUPPORTED_CONTEXT_DENIED",
608
+ selectorFingerprint: isAgentDenied ? "proposal:read:agent-denied" : "proposal:read:unsupported-context",
609
+ resultCount: 0,
610
+ resultClaimIds: [],
611
+ now: this._now(),
612
+ ...this._guardianFields(),
613
+ }));
614
+ throw err;
615
+ }
616
+ // PRODUCTION_API_GET_PROPOSAL_SAFE_NOT_FOUND_001: all of the following collapse to undefined:
617
+ // - proposal not found
618
+ // - proposal has empty candidateClaims (unknown subject)
619
+ // - proposal has multiple distinct subjects
620
+ // - proposal subject !== bound person scope (cross-subject)
621
+ const proposal = this._api.getProposal(this._minted.context, proposalId);
622
+ if (proposal === undefined) {
623
+ try {
624
+ await this._writeAuditOrFail(buildAuditEvent({
625
+ actor: this._actorLabel(),
626
+ decision: "deny",
627
+ reasonCode: "PROPOSAL_READ_SAFE_NOT_FOUND",
628
+ selectorFingerprint: "proposal:read:safe-not-found",
629
+ resultCount: 0,
630
+ resultClaimIds: [],
631
+ now: this._now(),
632
+ ...this._guardianFields(),
633
+ }));
634
+ }
635
+ catch { /* best-effort: swallow, still return undefined */ }
636
+ return undefined;
637
+ }
638
+ const proposalSubject = extractSingleSubject(proposal);
639
+ if (proposalSubject === undefined || proposalSubject !== boundSubject) {
640
+ try {
641
+ await this._writeAuditOrFail(buildAuditEvent({
642
+ actor: this._actorLabel(),
643
+ decision: "deny",
644
+ reasonCode: "PROPOSAL_READ_SAFE_NOT_FOUND",
645
+ selectorFingerprint: "proposal:read:safe-not-found",
646
+ resultCount: 0,
647
+ resultClaimIds: [],
648
+ now: this._now(),
649
+ ...this._guardianFields(),
650
+ }));
651
+ }
652
+ catch { /* best-effort: swallow, still return undefined */ }
653
+ return undefined;
654
+ }
655
+ await this._writeAuditOrFail(buildAuditEvent({
656
+ actor: this._actorLabel(),
657
+ decision: "allow",
658
+ reasonCode: "PROPOSAL_READ_ALLOWED",
659
+ selectorFingerprint: `proposal:read:proposalId=${proposalId}`,
660
+ resultCount: 1,
661
+ resultClaimIds: [],
662
+ now: this._now(),
663
+ ...this._guardianFields(),
664
+ }));
665
+ return proposal;
666
+ }
667
+ // PRODUCTION_API_LIST_PROPOSALS_SUBJECT_SCOPE_ENFORCED_001
668
+ // PRODUCTION_API_LIST_PROPOSALS_NO_CROSS_SUBJECT_RESULTS_001
669
+ // PRODUCTION_API_PROPOSAL_READ_NO_RAW_BYPASS_001
670
+ // PRODUCTION_AUDIT_READ_ALLOW_PATH_RECORDED_001 (C3): allow path fail-closed.
671
+ async listProposals(input) {
672
+ // resolveProposalReadSubject throws for agent-without-authorizedPersonId and unsupported kinds.
673
+ // PRODUCTION_AUDIT_PROPOSAL_DENIAL_RECORDED_001: pre-fetch denials recorded.
674
+ let boundSubject;
675
+ try {
676
+ boundSubject = resolveProposalReadSubject(this._minted);
677
+ }
678
+ catch (err) {
679
+ const isAgentDenied = err.code === "PRODUCTION_API_PROPOSAL_READ_AGENT_AUTHORIZED_PERSON_REQUIRED_001";
680
+ await this._writeAuditOrFail(buildAuditEvent({
681
+ actor: this._actorLabel(),
682
+ decision: "deny",
683
+ reasonCode: isAgentDenied ? "PROPOSAL_READ_AGENT_DENIED" : "PROPOSAL_READ_UNSUPPORTED_CONTEXT_DENIED",
684
+ selectorFingerprint: "proposal:read:list:denied",
685
+ resultCount: 0,
686
+ resultClaimIds: [],
687
+ now: this._now(),
688
+ ...this._guardianFields(),
689
+ }));
690
+ throw err;
691
+ }
692
+ // Broad internal fetch — raw result must not escape; immediately filtered below.
693
+ // Only status filter is forwarded; underGrant and proposedByAgent are not exposed.
694
+ const rawFilter = input?.status !== undefined ? { status: input.status } : undefined;
695
+ const raw = this._api.listProposals(this._minted.context, rawFilter);
696
+ // PRODUCTION_API_LIST_PROPOSALS_NO_CROSS_SUBJECT_RESULTS_001:
697
+ // Only proposals whose single extracted subject equals the bound person scope are returned.
698
+ // Unknown-subject (empty candidateClaims) and multi-subject proposals are silently omitted.
699
+ const filtered = raw.filter(p => {
700
+ const s = extractSingleSubject(p);
701
+ return s !== undefined && s === boundSubject;
702
+ });
703
+ await this._writeAuditOrFail(buildAuditEvent({
704
+ actor: this._actorLabel(),
705
+ decision: "allow",
706
+ reasonCode: "PROPOSAL_READ_ALLOWED",
707
+ selectorFingerprint: `proposal:read:list:count=${filtered.length}`,
708
+ resultCount: filtered.length,
709
+ resultClaimIds: [],
710
+ now: this._now(),
711
+ ...this._guardianFields(),
712
+ }));
713
+ return Object.freeze(filtered);
714
+ }
715
+ // PRODUCTION_API_REJECT_PROPOSAL_OWNER_ONLY_001
716
+ // PRODUCTION_API_REJECT_PROPOSAL_NO_RAW_BYPASS_001
717
+ // GUARDIAN_SCOPE_ENFORCED_AT_FACADE_001: rejectProposal shares the "proposal:accept" scope
718
+ // with acceptProposal — both are proposal decisions.
719
+ async rejectProposal(proposalId, options) {
720
+ await this._assertGuardianScope("rejectProposal", "proposal:accept");
721
+ // PRODUCTION_API_REJECT_PROPOSAL_AGENT_DENIED_001: fires before any proposal fetch
722
+ // PRODUCTION_AUDIT_PROPOSAL_MUTATION_RECORDED_001 / PRODUCTION_AUDIT_PROPOSAL_DENIAL_RECORDED_001
723
+ if (this._minted.context.kind !== "owner") {
724
+ await this._writeAuditOrFail(buildAuditEvent({
725
+ actor: this._actorLabel(),
726
+ decision: "deny",
727
+ reasonCode: "PROPOSAL_UNSUPPORTED_CONTEXT_DENIED",
728
+ selectorFingerprint: `proposal:reject:proposalId=${proposalId}:context-kind=${this._minted.context.kind}`,
729
+ resultCount: 0,
730
+ resultClaimIds: [],
731
+ now: this._now(),
732
+ ...this._guardianFields(),
733
+ }));
734
+ this._emitProposalOutcome("rejectProposal", "denied", this._minted.context.kind);
735
+ throw Object.assign(new Error(`PRODUCTION_API_REJECT_PROPOSAL_AGENT_DENIED_001: context kind ` +
736
+ `"${this._minted.context.kind}" may not reject proposals; only owner is permitted`), { code: "PRODUCTION_API_REJECT_PROPOSAL_AGENT_DENIED_001" });
737
+ }
738
+ // Phase A: fetch proposal — not found → safe not-found
739
+ const proposal = this._api.getProposal(this._minted.context, proposalId);
740
+ if (proposal === undefined) {
741
+ await this._writeAuditOrFail(buildAuditEvent({
742
+ actor: this._actorLabel(),
743
+ decision: "deny",
744
+ reasonCode: "PROPOSAL_NOT_FOUND",
745
+ selectorFingerprint: `proposal:reject:proposalId=${proposalId}`,
746
+ resultCount: 0,
747
+ resultClaimIds: [],
748
+ now: this._now(),
749
+ ...this._guardianFields(),
750
+ }));
751
+ this._emitProposalOutcome("rejectProposal", "denied", this._minted.context.kind);
752
+ throw Object.assign(new Error("PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001: proposal not found"), { code: "PRODUCTION_API_REJECT_PROPOSAL_SAFE_NOT_FOUND_001" });
753
+ }
754
+ // Phase A: owner-subject ownership check — any failure → safe not-found
755
+ // PRODUCTION_API_REJECT_PROPOSAL_OWNER_SUBJECT_SCOPE_ENFORCED_001
756
+ try {
757
+ assertOwnerOwnsProposalSubjectForReject(proposal, this._minted);
758
+ }
759
+ catch (err) {
760
+ await this._writeAuditOrFail(buildAuditEvent({
761
+ actor: this._actorLabel(),
762
+ decision: "deny",
763
+ reasonCode: "PROPOSAL_CROSS_SUBJECT_DENIED",
764
+ selectorFingerprint: `proposal:reject:proposalId=${proposalId}`,
765
+ resultCount: 0,
766
+ resultClaimIds: [],
767
+ now: this._now(),
768
+ ...this._guardianFields(),
769
+ }));
770
+ this._emitProposalOutcome("rejectProposal", "denied", this._minted.context.kind);
771
+ throw err;
772
+ }
773
+ // Allow path — PRODUCTION_AUDIT_ALLOW_MUTATION_ATOMIC_001 (C2a): audit + mutation atomic.
774
+ const rejectAllowEvent = buildAuditEvent({
775
+ actor: this._actorLabel(),
776
+ decision: "allow",
777
+ reasonCode: "PROPOSAL_ALLOWED",
778
+ selectorFingerprint: `proposal:reject:proposalId=${proposalId}`,
779
+ resultCount: 0,
780
+ resultClaimIds: [],
781
+ now: this._now(),
782
+ ...this._guardianFields(),
783
+ });
784
+ const rejectResult = await this._uow.run(async () => {
785
+ try {
786
+ await this._mutationLog.write(rejectAllowEvent);
787
+ }
788
+ catch {
789
+ safeRecord(this._metricsRecorder, "audit.write.failure", 1, {
790
+ component: "ProductionScopedLifeApiImpl", operation: "rejectProposal", status: "failed",
791
+ });
792
+ this._emitProposalOutcome("rejectProposal", "failed", this._minted.context.kind);
793
+ throw Object.assign(new Error("PRODUCTION_AUDIT_FAIL_CLOSED_001: audit write failed — operation aborted fail-closed"), { code: "PRODUCTION_AUDIT_FAIL_CLOSED_001" });
794
+ }
795
+ // Phase B: ownership confirmed — delegate; status errors may propagate
796
+ return this._api.rejectProposal(this._minted.context, proposalId, options.reason);
797
+ });
798
+ this._emitProposalOutcome("rejectProposal", "success", this._minted.context.kind);
799
+ return rejectResult;
800
+ }
801
+ }
802
+ //# sourceMappingURL=production-scoped-life-api.js.map