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.
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/dist/agent/finance-agent.d.ts +44 -0
- package/dist/agent/finance-agent.js +75 -0
- package/dist/agent/finance-agent.js.map +1 -0
- package/dist/agent/legal-agent.d.ts +44 -0
- package/dist/agent/legal-agent.js +76 -0
- package/dist/agent/legal-agent.js.map +1 -0
- package/dist/agent/medical-agent.d.ts +44 -0
- package/dist/agent/medical-agent.js +75 -0
- package/dist/agent/medical-agent.js.map +1 -0
- package/dist/agent/trusted-agent-registry.d.ts +53 -0
- package/dist/agent/trusted-agent-registry.js +57 -0
- package/dist/agent/trusted-agent-registry.js.map +1 -0
- package/dist/audit/audit-chain-verifier.d.ts +10 -0
- package/dist/audit/audit-chain-verifier.js +67 -0
- package/dist/audit/audit-chain-verifier.js.map +1 -0
- package/dist/audit/audit-log.d.ts +48 -0
- package/dist/audit/audit-log.js +74 -0
- package/dist/audit/audit-log.js.map +1 -0
- package/dist/audit/sqlite-audit-log.d.ts +29 -0
- package/dist/audit/sqlite-audit-log.js +142 -0
- package/dist/audit/sqlite-audit-log.js.map +1 -0
- package/dist/auth/access-context-issuer.d.ts +18 -0
- package/dist/auth/access-context-issuer.js +88 -0
- package/dist/auth/access-context-issuer.js.map +1 -0
- package/dist/auth/assert-authorized-for-subject.d.ts +2 -0
- package/dist/auth/assert-authorized-for-subject.js +20 -0
- package/dist/auth/assert-authorized-for-subject.js.map +1 -0
- package/dist/auth/backend-context-minting-boundary.d.ts +23 -0
- package/dist/auth/backend-context-minting-boundary.js +281 -0
- package/dist/auth/backend-context-minting-boundary.js.map +1 -0
- package/dist/auth/default-authorization-policy.d.ts +7 -0
- package/dist/auth/default-authorization-policy.js +64 -0
- package/dist/auth/default-authorization-policy.js.map +1 -0
- package/dist/auth/fake-auth-adapter.d.ts +7 -0
- package/dist/auth/fake-auth-adapter.js +27 -0
- package/dist/auth/fake-auth-adapter.js.map +1 -0
- package/dist/auth/http-client.d.ts +12 -0
- package/dist/auth/http-client.js +7 -0
- package/dist/auth/http-client.js.map +1 -0
- package/dist/auth/in-memory-grant-resolver.d.ts +21 -0
- package/dist/auth/in-memory-grant-resolver.js +73 -0
- package/dist/auth/in-memory-grant-resolver.js.map +1 -0
- package/dist/auth/in-memory-tenant-boundary.d.ts +16 -0
- package/dist/auth/in-memory-tenant-boundary.js +37 -0
- package/dist/auth/in-memory-tenant-boundary.js.map +1 -0
- package/dist/auth/jwks-key-ring-auth-adapter.d.ts +38 -0
- package/dist/auth/jwks-key-ring-auth-adapter.js +123 -0
- package/dist/auth/jwks-key-ring-auth-adapter.js.map +1 -0
- package/dist/auth/jwks-key-ring.d.ts +25 -0
- package/dist/auth/jwks-key-ring.js +111 -0
- package/dist/auth/jwks-key-ring.js.map +1 -0
- package/dist/auth/jwt-auth-adapter.d.ts +15 -0
- package/dist/auth/jwt-auth-adapter.js +59 -0
- package/dist/auth/jwt-auth-adapter.js.map +1 -0
- package/dist/auth/jwt-core.d.ts +32 -0
- package/dist/auth/jwt-core.js +226 -0
- package/dist/auth/jwt-core.js.map +1 -0
- package/dist/auth/jwt-key-ring-auth-adapter.d.ts +16 -0
- package/dist/auth/jwt-key-ring-auth-adapter.js +58 -0
- package/dist/auth/jwt-key-ring-auth-adapter.js.map +1 -0
- package/dist/auth/oidc-discovery.d.ts +15 -0
- package/dist/auth/oidc-discovery.js +46 -0
- package/dist/auth/oidc-discovery.js.map +1 -0
- package/dist/auth/production-scoped-life-api.d.ts +58 -0
- package/dist/auth/production-scoped-life-api.js +802 -0
- package/dist/auth/production-scoped-life-api.js.map +1 -0
- package/dist/auth/proposal-scope-inspector.d.ts +3 -0
- package/dist/auth/proposal-scope-inspector.js +42 -0
- package/dist/auth/proposal-scope-inspector.js.map +1 -0
- package/dist/auth/proposal-scoped-life-api.d.ts +21 -0
- package/dist/auth/proposal-scoped-life-api.js +168 -0
- package/dist/auth/proposal-scoped-life-api.js.map +1 -0
- package/dist/auth/rate-limited-context-minting-boundary.d.ts +27 -0
- package/dist/auth/rate-limited-context-minting-boundary.js +77 -0
- package/dist/auth/rate-limited-context-minting-boundary.js.map +1 -0
- package/dist/auth/revocation-aware-auth-adapter.d.ts +21 -0
- package/dist/auth/revocation-aware-auth-adapter.js +88 -0
- package/dist/auth/revocation-aware-auth-adapter.js.map +1 -0
- package/dist/auth/sqlite-grant-resolver.d.ts +8 -0
- package/dist/auth/sqlite-grant-resolver.js +78 -0
- package/dist/auth/sqlite-grant-resolver.js.map +1 -0
- package/dist/auth/sqlite-tenant-boundary.d.ts +7 -0
- package/dist/auth/sqlite-tenant-boundary.js +41 -0
- package/dist/auth/sqlite-tenant-boundary.js.map +1 -0
- package/dist/auth/static-jwt-key-ring.d.ts +13 -0
- package/dist/auth/static-jwt-key-ring.js +73 -0
- package/dist/auth/static-jwt-key-ring.js.map +1 -0
- package/dist/auth/subject-scoped-life-api.d.ts +14 -0
- package/dist/auth/subject-scoped-life-api.js +51 -0
- package/dist/auth/subject-scoped-life-api.js.map +1 -0
- package/dist/auth/test-context-minting-boundary.d.ts +13 -0
- package/dist/auth/test-context-minting-boundary.js +74 -0
- package/dist/auth/test-context-minting-boundary.js.map +1 -0
- package/dist/auth/types.d.ts +77 -0
- package/dist/auth/types.js +16 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/compliance/subject-data-report.d.ts +43 -0
- package/dist/compliance/subject-data-report.js +155 -0
- package/dist/compliance/subject-data-report.js.map +1 -0
- package/dist/consent/consent-guard.d.ts +4 -0
- package/dist/consent/consent-guard.js +37 -0
- package/dist/consent/consent-guard.js.map +1 -0
- package/dist/consent/grant-registry.d.ts +7 -0
- package/dist/consent/grant-registry.js +13 -0
- package/dist/consent/grant-registry.js.map +1 -0
- package/dist/consent/intersect-selector.d.ts +3 -0
- package/dist/consent/intersect-selector.js +66 -0
- package/dist/consent/intersect-selector.js.map +1 -0
- package/dist/contract/diagnostics.d.ts +32 -0
- package/dist/contract/diagnostics.js +112 -0
- package/dist/contract/diagnostics.js.map +1 -0
- package/dist/contract/project-contracts.d.ts +5 -0
- package/dist/contract/project-contracts.js +159 -0
- package/dist/contract/project-contracts.js.map +1 -0
- package/dist/contract/types.d.ts +32 -0
- package/dist/contract/types.js +5 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/crypto/aes-gcm.d.ts +11 -0
- package/dist/crypto/aes-gcm.js +78 -0
- package/dist/crypto/aes-gcm.js.map +1 -0
- package/dist/crypto/claim-serializer.d.ts +26 -0
- package/dist/crypto/claim-serializer.js +59 -0
- package/dist/crypto/claim-serializer.js.map +1 -0
- package/dist/crypto/sha256.d.ts +12 -0
- package/dist/crypto/sha256.js +40 -0
- package/dist/crypto/sha256.js.map +1 -0
- package/dist/crypto/static-key.d.ts +11 -0
- package/dist/crypto/static-key.js +60 -0
- package/dist/crypto/static-key.js.map +1 -0
- package/dist/crypto/types.d.ts +20 -0
- package/dist/crypto/types.js +8 -0
- package/dist/crypto/types.js.map +1 -0
- package/dist/datasource/trusted-data-source-registry.d.ts +51 -0
- package/dist/datasource/trusted-data-source-registry.js +57 -0
- package/dist/datasource/trusted-data-source-registry.js.map +1 -0
- package/dist/erasure/tombstone.d.ts +13 -0
- package/dist/erasure/tombstone.js +20 -0
- package/dist/erasure/tombstone.js.map +1 -0
- package/dist/evidence/evidence-backed-claim-creator.d.ts +48 -0
- package/dist/evidence/evidence-backed-claim-creator.js +107 -0
- package/dist/evidence/evidence-backed-claim-creator.js.map +1 -0
- package/dist/evidence/in-memory-raw-document-store.d.ts +26 -0
- package/dist/evidence/in-memory-raw-document-store.js +84 -0
- package/dist/evidence/in-memory-raw-document-store.js.map +1 -0
- package/dist/evidence/raw-document-store.d.ts +12 -0
- package/dist/evidence/raw-document-store.js +8 -0
- package/dist/evidence/raw-document-store.js.map +1 -0
- package/dist/evidence/source-evidence-validator.d.ts +4 -0
- package/dist/evidence/source-evidence-validator.js +14 -0
- package/dist/evidence/source-evidence-validator.js.map +1 -0
- package/dist/evidence/sqlite-raw-document-store.d.ts +19 -0
- package/dist/evidence/sqlite-raw-document-store.js +122 -0
- package/dist/evidence/sqlite-raw-document-store.js.map +1 -0
- package/dist/guardian/guardian-registry.d.ts +96 -0
- package/dist/guardian/guardian-registry.js +95 -0
- package/dist/guardian/guardian-registry.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/health.d.ts +12 -0
- package/dist/observability/health.js +16 -0
- package/dist/observability/health.js.map +1 -0
- package/dist/observability/logger.d.ts +21 -0
- package/dist/observability/logger.js +40 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +15 -0
- package/dist/observability/metrics.js +52 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/persistence/backup.d.ts +24 -0
- package/dist/persistence/backup.js +251 -0
- package/dist/persistence/backup.js.map +1 -0
- package/dist/persistence/decryptability-scan.d.ts +10 -0
- package/dist/persistence/decryptability-scan.js +144 -0
- package/dist/persistence/decryptability-scan.js.map +1 -0
- package/dist/persistence/in-memory-unit-of-work.d.ts +11 -0
- package/dist/persistence/in-memory-unit-of-work.js +32 -0
- package/dist/persistence/in-memory-unit-of-work.js.map +1 -0
- package/dist/persistence/key-availability.d.ts +9 -0
- package/dist/persistence/key-availability.js +125 -0
- package/dist/persistence/key-availability.js.map +1 -0
- package/dist/persistence/sqlite/migrations.d.ts +9 -0
- package/dist/persistence/sqlite/migrations.js +535 -0
- package/dist/persistence/sqlite/migrations.js.map +1 -0
- package/dist/persistence/sqlite/schema.d.ts +10 -0
- package/dist/persistence/sqlite/schema.js +101 -0
- package/dist/persistence/sqlite/schema.js.map +1 -0
- package/dist/persistence/sqlite/sqlite-fact-store.d.ts +24 -0
- package/dist/persistence/sqlite/sqlite-fact-store.js +207 -0
- package/dist/persistence/sqlite/sqlite-fact-store.js.map +1 -0
- package/dist/persistence/sqlite/sqlite-source-registry.d.ts +17 -0
- package/dist/persistence/sqlite/sqlite-source-registry.js +132 -0
- package/dist/persistence/sqlite/sqlite-source-registry.js.map +1 -0
- package/dist/persistence/sqlite/sqlite-unit-of-work.d.ts +8 -0
- package/dist/persistence/sqlite/sqlite-unit-of-work.js +41 -0
- package/dist/persistence/sqlite/sqlite-unit-of-work.js.map +1 -0
- package/dist/persistence/sqlite/types.d.ts +24 -0
- package/dist/persistence/sqlite/types.js +28 -0
- package/dist/persistence/sqlite/types.js.map +1 -0
- package/dist/persistence/unit-of-work.d.ts +6 -0
- package/dist/persistence/unit-of-work.js +15 -0
- package/dist/persistence/unit-of-work.js.map +1 -0
- package/dist/proposal/accepted-claim-appender.d.ts +7 -0
- package/dist/proposal/accepted-claim-appender.js +16 -0
- package/dist/proposal/accepted-claim-appender.js.map +1 -0
- package/dist/proposal/in-memory-proposal-queue.d.ts +21 -0
- package/dist/proposal/in-memory-proposal-queue.js +218 -0
- package/dist/proposal/in-memory-proposal-queue.js.map +1 -0
- package/dist/proposal/policy-aware-proposal-queue.d.ts +23 -0
- package/dist/proposal/policy-aware-proposal-queue.js +162 -0
- package/dist/proposal/policy-aware-proposal-queue.js.map +1 -0
- package/dist/proposal/proposal-policy.d.ts +37 -0
- package/dist/proposal/proposal-policy.js +72 -0
- package/dist/proposal/proposal-policy.js.map +1 -0
- package/dist/proposal/proposal-queue.d.ts +13 -0
- package/dist/proposal/proposal-queue.js +15 -0
- package/dist/proposal/proposal-queue.js.map +1 -0
- package/dist/proposal/source-aware-claim-appender.d.ts +13 -0
- package/dist/proposal/source-aware-claim-appender.js +47 -0
- package/dist/proposal/source-aware-claim-appender.js.map +1 -0
- package/dist/proposal/sqlite-proposal-queue.d.ts +26 -0
- package/dist/proposal/sqlite-proposal-queue.js +282 -0
- package/dist/proposal/sqlite-proposal-queue.js.map +1 -0
- package/dist/resolution/resolve-claims.d.ts +4 -0
- package/dist/resolution/resolve-claims.js +101 -0
- package/dist/resolution/resolve-claims.js.map +1 -0
- package/dist/resolution/trust-profile.d.ts +12 -0
- package/dist/resolution/trust-profile.js +23 -0
- package/dist/resolution/trust-profile.js.map +1 -0
- package/dist/resolution/types.d.ts +15 -0
- package/dist/resolution/types.js +8 -0
- package/dist/resolution/types.js.map +1 -0
- package/dist/sdk/create-backend-integrated-life-api-for-test.d.ts +23 -0
- package/dist/sdk/create-backend-integrated-life-api-for-test.js +240 -0
- package/dist/sdk/create-backend-integrated-life-api-for-test.js.map +1 -0
- package/dist/sdk/create-life-api.d.ts +55 -0
- package/dist/sdk/create-life-api.js +251 -0
- package/dist/sdk/create-life-api.js.map +1 -0
- package/dist/sdk/create-sqlite-life-api.d.ts +33 -0
- package/dist/sdk/create-sqlite-life-api.js +654 -0
- package/dist/sdk/create-sqlite-life-api.js.map +1 -0
- package/dist/sdk/life-api.d.ts +27 -0
- package/dist/sdk/life-api.js +17 -0
- package/dist/sdk/life-api.js.map +1 -0
- package/dist/sdk/validation.d.ts +8 -0
- package/dist/sdk/validation.js +216 -0
- package/dist/sdk/validation.js.map +1 -0
- package/dist/source/in-memory-source-registry.d.ts +15 -0
- package/dist/source/in-memory-source-registry.js +104 -0
- package/dist/source/in-memory-source-registry.js.map +1 -0
- package/dist/source/source-aware-fact-store.d.ts +19 -0
- package/dist/source/source-aware-fact-store.js +40 -0
- package/dist/source/source-aware-fact-store.js.map +1 -0
- package/dist/source/source-kind-predicate-policy.d.ts +4 -0
- package/dist/source/source-kind-predicate-policy.js +62 -0
- package/dist/source/source-kind-predicate-policy.js.map +1 -0
- package/dist/source/source-registry.d.ts +11 -0
- package/dist/source/source-registry.js +12 -0
- package/dist/source/source-registry.js.map +1 -0
- package/dist/store/in-memory-fact-store.d.ts +25 -0
- package/dist/store/in-memory-fact-store.js +192 -0
- package/dist/store/in-memory-fact-store.js.map +1 -0
- package/dist/surface/context-query.d.ts +21 -0
- package/dist/surface/context-query.js +113 -0
- package/dist/surface/context-query.js.map +1 -0
- package/dist/surface/evidence-backed-context-query.d.ts +62 -0
- package/dist/surface/evidence-backed-context-query.js +240 -0
- package/dist/surface/evidence-backed-context-query.js.map +1 -0
- package/dist/types/access-context.d.ts +25 -0
- package/dist/types/access-context.js +6 -0
- package/dist/types/access-context.js.map +1 -0
- package/dist/types/claim.d.ts +48 -0
- package/dist/types/claim.js +5 -0
- package/dist/types/claim.js.map +1 -0
- package/dist/types/consent.d.ts +41 -0
- package/dist/types/consent.js +3 -0
- package/dist/types/consent.js.map +1 -0
- package/dist/types/crypto-boundary.d.ts +12 -0
- package/dist/types/crypto-boundary.js +15 -0
- package/dist/types/crypto-boundary.js.map +1 -0
- package/dist/types/evidence.d.ts +31 -0
- package/dist/types/evidence.js +8 -0
- package/dist/types/evidence.js.map +1 -0
- package/dist/types/fact-store.d.ts +24 -0
- package/dist/types/fact-store.js +12 -0
- package/dist/types/fact-store.js.map +1 -0
- package/dist/types/ids.d.ts +36 -0
- package/dist/types/ids.js +11 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/proposal.d.ts +38 -0
- package/dist/types/proposal.js +7 -0
- package/dist/types/proposal.js.map +1 -0
- package/dist/types/source.d.ts +33 -0
- package/dist/types/source.js +6 -0
- package/dist/types/source.js.map +1 -0
- package/dist/types/trust.d.ts +24 -0
- package/dist/types/trust.js +5 -0
- package/dist/types/trust.js.map +1 -0
- package/dist/utilities/deep-freeze.d.ts +1 -0
- package/dist/utilities/deep-freeze.js +17 -0
- package/dist/utilities/deep-freeze.js.map +1 -0
- 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
|