@phosra/gatekeeper 0.1.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 (42) hide show
  1. package/README.md +83 -0
  2. package/dist/capabilities-cache.d.ts +25 -0
  3. package/dist/capabilities-cache.d.ts.map +1 -0
  4. package/dist/capabilities-cache.js +45 -0
  5. package/dist/capabilities-cache.js.map +1 -0
  6. package/dist/crosswalk.d.ts +11 -0
  7. package/dist/crosswalk.d.ts.map +1 -0
  8. package/dist/crosswalk.js +32 -0
  9. package/dist/crosswalk.js.map +1 -0
  10. package/dist/fleet-backcompat.test.d.ts +2 -0
  11. package/dist/fleet-backcompat.test.d.ts.map +1 -0
  12. package/dist/fleet-backcompat.test.js +38 -0
  13. package/dist/fleet-backcompat.test.js.map +1 -0
  14. package/dist/gatekeeper-version.test.d.ts +2 -0
  15. package/dist/gatekeeper-version.test.d.ts.map +1 -0
  16. package/dist/gatekeeper-version.test.js +12 -0
  17. package/dist/gatekeeper-version.test.js.map +1 -0
  18. package/dist/gatekeeper.d.ts +38 -0
  19. package/dist/gatekeeper.d.ts.map +1 -0
  20. package/dist/gatekeeper.js +466 -0
  21. package/dist/gatekeeper.js.map +1 -0
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +5 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/ocss-http-client.d.ts +21 -0
  27. package/dist/ocss-http-client.d.ts.map +1 -0
  28. package/dist/ocss-http-client.js +45 -0
  29. package/dist/ocss-http-client.js.map +1 -0
  30. package/dist/protocol.d.ts +10 -0
  31. package/dist/protocol.d.ts.map +1 -0
  32. package/dist/protocol.js +10 -0
  33. package/dist/protocol.js.map +1 -0
  34. package/dist/trust-list-cache.d.ts +23 -0
  35. package/dist/trust-list-cache.d.ts.map +1 -0
  36. package/dist/trust-list-cache.js +35 -0
  37. package/dist/trust-list-cache.js.map +1 -0
  38. package/dist/types.d.ts +216 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +104 -0
  41. package/dist/types.js.map +1 -0
  42. package/package.json +30 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * TrustListCache fetches /.well-known/ocss/trust-list, verifies it to the OCSS ROOT
3
+ * via @openchildsafety/ocss verifyDocument (the ONLY verifyDocument call in the gatekeeper — it is
4
+ * the Trust-List-to-root path), and builds a Resolver. signingKey(keyId) resolves the
5
+ * router role key the enforcement profile was signed with. The profile itself is
6
+ * verified by the caller via a DIRECT Ed25519 verify with this resolved key — NEVER
7
+ * by passing the profile to verifyDocument.
8
+ */
9
+ export declare class TrustListCache {
10
+ private opts;
11
+ private resolver?;
12
+ constructor(opts: {
13
+ fetchImpl: typeof fetch;
14
+ baseUrl: string;
15
+ trustRootXB64Url: string;
16
+ now: () => number;
17
+ });
18
+ refresh(): Promise<void>;
19
+ ensure(): Promise<void>;
20
+ /** Resolve "did:ocss:<slug>#<kid>" to a 32-byte Ed25519 public key from the verified list. */
21
+ signingKey(keyId: string): Uint8Array;
22
+ }
23
+ //# sourceMappingURL=trust-list-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust-list-cache.d.ts","sourceRoot":"","sources":["../src/trust-list-cache.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,qBAAa,cAAc;IAGvB,OAAO,CAAC,IAAI;IAFd,OAAO,CAAC,QAAQ,CAAC,CAAW;gBAElB,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,KAAK,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,MAAM,CAAA;KAAE;IAGnG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQxB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,8FAA8F;IAC9F,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;CAItC"}
@@ -0,0 +1,35 @@
1
+ import { verifyDocument, fromVerifiedDocument } from "@openchildsafety/ocss";
2
+ /**
3
+ * TrustListCache fetches /.well-known/ocss/trust-list, verifies it to the OCSS ROOT
4
+ * via @openchildsafety/ocss verifyDocument (the ONLY verifyDocument call in the gatekeeper — it is
5
+ * the Trust-List-to-root path), and builds a Resolver. signingKey(keyId) resolves the
6
+ * router role key the enforcement profile was signed with. The profile itself is
7
+ * verified by the caller via a DIRECT Ed25519 verify with this resolved key — NEVER
8
+ * by passing the profile to verifyDocument.
9
+ */
10
+ export class TrustListCache {
11
+ opts;
12
+ resolver;
13
+ constructor(opts) {
14
+ this.opts = opts;
15
+ }
16
+ async refresh() {
17
+ const res = await this.opts.fetchImpl(this.opts.baseUrl + "/.well-known/ocss/trust-list", { method: "GET" });
18
+ if (!res.ok)
19
+ throw new Error(`@phosra/gatekeeper: trust-list fetch failed (${res.status})`);
20
+ const signed = (await res.json());
21
+ const doc = verifyDocument(signed, this.opts.trustRootXB64Url); // verify to ROOT
22
+ this.resolver = fromVerifiedDocument(doc, this.opts.now);
23
+ }
24
+ async ensure() {
25
+ if (!this.resolver)
26
+ await this.refresh();
27
+ }
28
+ /** Resolve "did:ocss:<slug>#<kid>" to a 32-byte Ed25519 public key from the verified list. */
29
+ signingKey(keyId) {
30
+ if (!this.resolver)
31
+ throw new Error("@phosra/gatekeeper: trust list not loaded");
32
+ return this.resolver.signingKey(keyId);
33
+ }
34
+ }
35
+ //# sourceMappingURL=trust-list-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust-list-cache.js","sourceRoot":"","sources":["../src/trust-list-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAiC,MAAM,uBAAuB,CAAC;AAE5G;;;;;;;GAOG;AACH,MAAM,OAAO,cAAc;IAGf;IAFF,QAAQ,CAAY;IAC5B,YACU,IAA+F;QAA/F,SAAI,GAAJ,IAAI,CAA2F;IACtG,CAAC;IAEJ,KAAK,CAAC,OAAO;QACX,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,8BAA8B,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7G,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5F,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmB,CAAC;QACpD,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,iBAAiB;QACjF,IAAI,CAAC,QAAQ,GAAG,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IAC3C,CAAC;IAED,8FAA8F;IAC9F,UAAU,CAAC,KAAa;QACtB,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QACjF,OAAO,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC;CACF"}
@@ -0,0 +1,216 @@
1
+ import type { SenderKey, JWK, Receipt } from "@openchildsafety/ocss";
2
+ export interface RatingMapping {
3
+ ocssCategory: string;
4
+ myField: string;
5
+ vocabulary: "mpaa" | "tv_parental" | "esrb" | "pegi" | "age_band";
6
+ /** Optional declarative remap from a proprietary code to a standard board code. */
7
+ codeMap?: Record<string, string>;
8
+ }
9
+ export interface GatekeeperConfig {
10
+ platformDid: string;
11
+ platformKeyId: string;
12
+ /** Ed25519 SenderKey; signs RFC 9421 census requests and §8.3.8 receipts. */
13
+ gatekeeperSigningKey: SenderKey;
14
+ /** EC P-256 PRIVATE JWK — OPTIONAL/RESERVED for future sealed content flows.
15
+ * NEVER used in the enforcement-profile read/verify path (profiles are
16
+ * router-signed, not sealed to this key). NEVER shared. */
17
+ platformPayloadKeyJwk?: JWK;
18
+ censusBaseUrl: string;
19
+ /** Ed25519 root X (base64url-raw); verifies the Trust List to root. */
20
+ trustRootXB64Url: string;
21
+ /** High-entropy §9.3(b) bound-resolver label; presenting it IS authentication. */
22
+ endpointId: string;
23
+ ratingMappings: RatingMapping[];
24
+ tlRefreshIntervalMs?: number;
25
+ pollIntervalMs?: number;
26
+ /** Injectable for tests/non-default runtimes. */
27
+ fetchImpl?: typeof fetch;
28
+ now?: () => number;
29
+ /**
30
+ * HMAC-SHA256 shared secret used to verify the `X-Phosra-Signature` header on
31
+ * inbound connect-leg deliveries (the server-to-server `POST { endpoint_id_label }`
32
+ * that the writer-BFF sends after the connect ceremony completes).
33
+ *
34
+ * **Algorithm:** `HMAC-SHA256(secret, rawRequestBody)` → lowercase hex.
35
+ * Matches the server-side signing in `internal/service/webhook.go` lines 155-157.
36
+ *
37
+ * **Required in production.** When set, `handleConnect` rejects any request whose
38
+ * `X-Phosra-Signature` header is absent or does not match (HTTP 401, fail-closed).
39
+ * When omitted, no signature check is performed — acceptable in a trusted-network
40
+ * deployment only; SHOULD NOT be omitted in internet-facing environments.
41
+ *
42
+ * The secret is returned once at endpoint-mint time; treat it like an API key.
43
+ */
44
+ connectSecret?: string;
45
+ }
46
+ export interface Verdict {
47
+ decision: "allow" | "warn" | "block";
48
+ failMode: "block" | "allow";
49
+ ruleSlug: string;
50
+ ruleRef: string | null;
51
+ /**
52
+ * No-op (resolves undefined) when ruleRef === null (fail-closed).
53
+ *
54
+ * INTERVENTION-FIRED GUARDRAIL (§8.3.8 spec-faithfulness):
55
+ * - "applied" — the enforcement intervention actually fired (content removed,
56
+ * connection blocked, feature disabled, etc.). MUST NOT be used when the
57
+ * platform received the signal but did not perform the intervention.
58
+ * A signature proves *provenance*, not enforcement truth. REQUIRES a
59
+ * non-empty `method_class` in the receipt body naming the enforcement
60
+ * mechanism (e.g. "dns_block", "platform_gate", "content_removal") — the
61
+ * census rejects an "applied" receipt with a blank method_class (§8.3.8
62
+ * intervention-fired requirement).
63
+ * - "degraded" — the platform attempted enforcement but the result was partial
64
+ * (e.g. 48-hour NCII deadline missed, feature only partially disabled), or
65
+ * enforcement was attempted after the fact. May carry an empty method_class.
66
+ * - "refused" — the platform explicitly declined to enforce (with reason).
67
+ * May carry an empty method_class.
68
+ *
69
+ * Do NOT pass "applied" unless the intervention visibly and verifiably fired.
70
+ * The `method_class` in the confirmation body is the mechanism-of-enforcement
71
+ * attestation; `gk.confirm()` defaults it to "platform_gate" when not specified.
72
+ */
73
+ confirm: (state: "applied" | "degraded" | "refused") => Promise<Receipt | undefined>;
74
+ }
75
+ /**
76
+ * §7.3 per-rule minimum-assurance floors mirrored from registry/ocss-rules.json
77
+ * and internal/ocss/profile/floors.go. Used by the gatekeeper's assurance-level
78
+ * guardrail: a signal whose assurance_level falls below a rule's floor MUST be
79
+ * treated as insufficient and produce a block verdict, regardless of the compiled
80
+ * profile decision.
81
+ *
82
+ * CANONICAL SOURCE: registry/ocss-rules.json "floor" fields.
83
+ * Widening / narrowing this set is a §12.4 governance action, never an ad-hoc edit.
84
+ * Assurance rank: self_declared(0) < parental_declared(1) < estimated(2)
85
+ * < document_verified(3) < institution_record(4)
86
+ *
87
+ * DRIFT GUARD: HARD_AV_FLOOR_RULES (below) is DERIVED from this map — it
88
+ * automatically covers any new document_verified-floor entry added here.
89
+ * Never maintain a separate hardcoded set of "hard AV rules".
90
+ */
91
+ export declare const RULE_ASSURANCE_FLOORS: Readonly<Record<string, number>>;
92
+ /**
93
+ * Derived set of rule slugs whose §7.3 assurance floor is document_verified
94
+ * (rank 3) or higher. MUST NOT be maintained by hand — derived from
95
+ * RULE_ASSURANCE_FLOORS so that any new document_verified-floor rule is
96
+ * automatically covered by the confirm-stage hard-AV guardrail in gatekeeper.ts.
97
+ *
98
+ * Exported for testing: tests import from this module (not from gatekeeper.ts)
99
+ * to verify the derivation invariant.
100
+ */
101
+ export declare const HARD_AV_FLOOR_RULES: ReadonlySet<string>;
102
+ /**
103
+ * Map from the wire `assurance_level` string to its §7.2 assurance rank.
104
+ * Unknown strings rank -1 and satisfy nothing (fail-closed).
105
+ */
106
+ export declare const ASSURANCE_RANK: Readonly<Record<string, number>>;
107
+ /** The product-side extended category — reads `params`/`rule_ref` from the verified
108
+ * document. The @openchildsafety/ocss normative ProfileCategory is NOT extended with these. */
109
+ export interface GatekeeperCategory {
110
+ category: string;
111
+ decision: "allow" | "warn" | "block";
112
+ fail_mode: "open" | "closed";
113
+ rule_slug: string;
114
+ statute_mapping_ref?: string;
115
+ /** json.RawMessage from the census: an object on the wire, but tolerate a string. */
116
+ params?: string | {
117
+ family?: string;
118
+ scale?: string;
119
+ max_allowed?: number;
120
+ };
121
+ /** The opaque per-child rule_ref the §8.3.8 confirm submits. Absent on family-less compile. */
122
+ rule_ref?: string;
123
+ }
124
+ export interface VerifiedProfile {
125
+ endpointId: string;
126
+ document_type: string;
127
+ ocss_version: string;
128
+ profile_ref?: string;
129
+ window: {
130
+ not_before: string;
131
+ not_after: string;
132
+ };
133
+ categories: GatekeeperCategory[];
134
+ }
135
+ export interface Gatekeeper {
136
+ refreshTrustList(): Promise<void>;
137
+ refreshProfile(endpointId?: string): Promise<void>;
138
+ getCachedProfile(endpointId?: string): VerifiedProfile | undefined;
139
+ isAllowed(args: {
140
+ endpointId?: string;
141
+ category: string;
142
+ signal?: Record<string, unknown>;
143
+ }): Verdict;
144
+ check(category: string, ctx?: Record<string, unknown>): Verdict;
145
+ /**
146
+ * Low-level confirm: POST a signed §8.3.8 enforcement-confirmation directly.
147
+ *
148
+ * PREFER `verdict.confirm()` returned by `isAllowed()` — it carries the category
149
+ * in scope and applies the §7.3 assurance guardrail (throwing `AssuranceLevelError`
150
+ * for HARD-AV rules when state === "applied" without document-verified assurance).
151
+ *
152
+ * This method is NOT category-aware: calling `confirm("applied")` directly for
153
+ * `adult_site_av_required` or `hard_id_verification_escalation` will NOT throw
154
+ * AssuranceLevelError client-side. Census-side enforcement is the backstop, but
155
+ * the defense-in-depth client check is bypassed. Use this method only when you
156
+ * hold the ruleRef + envelopeRef from a prior isAllowed() call and need to confirm
157
+ * in a different callsite (e.g. a background task after enforcement fires async).
158
+ */
159
+ confirm(envelopeRef: string, ruleRef: string | null, state: "applied" | "degraded" | "refused", methodClass?: string): Promise<Receipt | undefined>;
160
+ /** P3 connect callback: receives the writer-BFF-delivered endpoint_id_label
161
+ * (S2S POST { endpoint_id_label, state }), persists it as the connected
162
+ * endpoint, and activates the PULL path (refreshProfile). NOT the provisioner
163
+ * (GC1) and does NOT decrypt (GC3). gk.webhook() PUSH path is deferred (GC7). */
164
+ handleConnect(req: Request): Promise<Response>;
165
+ /** P4 report-back (§3.2): normalize nativeValue via the SAME crosswalk isAllowed
166
+ * uses, echo-suppress against the cached profile ceiling (EXACT equality), then
167
+ * sign a self-contained phosra_platform_proposal and POST /api/v1/proposals.
168
+ * Phosra-PRODUCT — NOT vocab.ts, NOT enforcement_result. MUST NOT write a rule. */
169
+ reportParentChange(change: ReportParentChangeArgs): Promise<ReportParentChangeResult>;
170
+ destroy(): void;
171
+ }
172
+ export declare class RuleRefRequired extends Error {
173
+ constructor();
174
+ }
175
+ export interface ReportParentChangeArgs {
176
+ /** Defaults to connectedEndpointId / cfg.endpointId. */
177
+ endpointId?: string;
178
+ ocssCategory: string;
179
+ nativeValue: string;
180
+ /** Explicit — no silent default (§5.10). */
181
+ changeScope: "platform_local" | "family_wide";
182
+ }
183
+ export type ReportParentChangeResult = {
184
+ proposalId: string;
185
+ delta_kind?: string;
186
+ } | {
187
+ suppressed: true;
188
+ };
189
+ /** Thrown when reportParentChange is called for a category with no RatingMapping (C8). */
190
+ export declare class NoRatingMappingError extends Error {
191
+ constructor(category: string);
192
+ }
193
+ /** Thrown when reportParentChange receives a nativeValue that does not resolve to a
194
+ * known crosswalk entry (contentAgeFor returns MAX_ORDINAL). MAX_ORDINAL is safe as a
195
+ * CONTENT-AGE in isAllowed (→ block), but MUST NOT be emitted as a proposed ceiling
196
+ * (desired_max_allowed === MAX_ORDINAL means "allow everything"). */
197
+ export declare class UnmappedRatingValueError extends Error {
198
+ constructor(vocabulary: string, nativeValue: string);
199
+ }
200
+ /**
201
+ * Thrown by the §7.3 assurance-level guardrail when a platform calls
202
+ * verdict.confirm("applied") for a rule whose registered floor is
203
+ * `document_verified` (or higher) but the enforcement profile was compiled
204
+ * from a signal that only carried `parental_declared` assurance.
205
+ *
206
+ * Spec-faithfulness invariant: a signature proves PROVENANCE, not age TRUTH.
207
+ * `parental_declared` proves a parent made a statement; it does NOT prove the
208
+ * hard age-verification a statute like UT/TX app-store AV requires. The
209
+ * platform MUST confirm "degraded" in this case, not "applied".
210
+ *
211
+ * Affected rules: `adult_site_av_required`, `hard_id_verification_escalation`.
212
+ */
213
+ export declare class AssuranceLevelError extends Error {
214
+ constructor(category: string);
215
+ }
216
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * §7.3 per-rule minimum-assurance floors mirrored from registry/ocss-rules.json
3
+ * and internal/ocss/profile/floors.go. Used by the gatekeeper's assurance-level
4
+ * guardrail: a signal whose assurance_level falls below a rule's floor MUST be
5
+ * treated as insufficient and produce a block verdict, regardless of the compiled
6
+ * profile decision.
7
+ *
8
+ * CANONICAL SOURCE: registry/ocss-rules.json "floor" fields.
9
+ * Widening / narrowing this set is a §12.4 governance action, never an ad-hoc edit.
10
+ * Assurance rank: self_declared(0) < parental_declared(1) < estimated(2)
11
+ * < document_verified(3) < institution_record(4)
12
+ *
13
+ * DRIFT GUARD: HARD_AV_FLOOR_RULES (below) is DERIVED from this map — it
14
+ * automatically covers any new document_verified-floor entry added here.
15
+ * Never maintain a separate hardcoded set of "hard AV rules".
16
+ */
17
+ export const RULE_ASSURANCE_FLOORS = {
18
+ // parental_declared floor (rank 1) — the minimum for most age rules
19
+ rating_age_gate: 1,
20
+ age_gate: 1,
21
+ app_store_age_attestation: 1,
22
+ os_age_signal_ingest: 1,
23
+ age_signal_broadcast: 1,
24
+ social_media_min_age: 1,
25
+ ai_chatbot_age_assertion: 1,
26
+ teen_minimum_age_16_gate: 1,
27
+ age_appropriate_profile_mode: 1,
28
+ // document_verified floor (rank 3) — hard age-verification statutes
29
+ // A signature proves provenance, not age truth. @phosra/gatekeeper MUST
30
+ // refuse to represent a parental_declared or estimated signal as satisfying
31
+ // these rules (UT HB 311, TX HB 1181, SCREEN Act, UK OSA Part 5).
32
+ adult_site_av_required: 3,
33
+ hard_id_verification_escalation: 3,
34
+ };
35
+ /**
36
+ * Derived set of rule slugs whose §7.3 assurance floor is document_verified
37
+ * (rank 3) or higher. MUST NOT be maintained by hand — derived from
38
+ * RULE_ASSURANCE_FLOORS so that any new document_verified-floor rule is
39
+ * automatically covered by the confirm-stage hard-AV guardrail in gatekeeper.ts.
40
+ *
41
+ * Exported for testing: tests import from this module (not from gatekeeper.ts)
42
+ * to verify the derivation invariant.
43
+ */
44
+ export const HARD_AV_FLOOR_RULES = new Set(Object.entries(RULE_ASSURANCE_FLOORS)
45
+ .filter(([, rank]) => rank >= 3 /* document_verified is rank 3 — see the rank map below */)
46
+ .map(([rule]) => rule));
47
+ /**
48
+ * Map from the wire `assurance_level` string to its §7.2 assurance rank.
49
+ * Unknown strings rank -1 and satisfy nothing (fail-closed).
50
+ */
51
+ export const ASSURANCE_RANK = {
52
+ self_declared: 0,
53
+ parental_declared: 1,
54
+ estimated: 2,
55
+ document_verified: 3,
56
+ institution_record: 4,
57
+ };
58
+ export class RuleRefRequired extends Error {
59
+ constructor() {
60
+ super("§8.3.8: rule_ref is required — refusing to confirm a fail-closed verdict (no rule was enforced)");
61
+ this.name = "RuleRefRequired";
62
+ }
63
+ }
64
+ /** Thrown when reportParentChange is called for a category with no RatingMapping (C8). */
65
+ export class NoRatingMappingError extends Error {
66
+ constructor(category) {
67
+ super(`reportParentChange: no rating mapping for "${category}" — only declared total-ordered rating fields are reportable (§5.7)`);
68
+ this.name = "NoRatingMappingError";
69
+ }
70
+ }
71
+ /** Thrown when reportParentChange receives a nativeValue that does not resolve to a
72
+ * known crosswalk entry (contentAgeFor returns MAX_ORDINAL). MAX_ORDINAL is safe as a
73
+ * CONTENT-AGE in isAllowed (→ block), but MUST NOT be emitted as a proposed ceiling
74
+ * (desired_max_allowed === MAX_ORDINAL means "allow everything"). */
75
+ export class UnmappedRatingValueError extends Error {
76
+ constructor(vocabulary, nativeValue) {
77
+ super(`reportParentChange: "${nativeValue}" does not map to a known age in vocabulary "${vocabulary}" — rejecting to avoid emitting a maximally-permissive proposal`);
78
+ this.name = "UnmappedRatingValueError";
79
+ }
80
+ }
81
+ /**
82
+ * Thrown by the §7.3 assurance-level guardrail when a platform calls
83
+ * verdict.confirm("applied") for a rule whose registered floor is
84
+ * `document_verified` (or higher) but the enforcement profile was compiled
85
+ * from a signal that only carried `parental_declared` assurance.
86
+ *
87
+ * Spec-faithfulness invariant: a signature proves PROVENANCE, not age TRUTH.
88
+ * `parental_declared` proves a parent made a statement; it does NOT prove the
89
+ * hard age-verification a statute like UT/TX app-store AV requires. The
90
+ * platform MUST confirm "degraded" in this case, not "applied".
91
+ *
92
+ * Affected rules: `adult_site_av_required`, `hard_id_verification_escalation`.
93
+ */
94
+ export class AssuranceLevelError extends Error {
95
+ constructor(category) {
96
+ super(`§7.3 assurance guardrail: "${category}" has a document_verified floor — ` +
97
+ `confirm("applied") is refused because a parental_declared signal proves provenance, ` +
98
+ `not the hard age-verification this statute requires. ` +
99
+ `Confirm "degraded" to honestly attest that enforcement was attempted but the ` +
100
+ `required assurance level was not met.`);
101
+ this.name = "AssuranceLevelError";
102
+ }
103
+ }
104
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA8EA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAqC;IACrE,oEAAoE;IACpE,eAAe,EAAgB,CAAC;IAChC,QAAQ,EAAuB,CAAC;IAChC,yBAAyB,EAAM,CAAC;IAChC,oBAAoB,EAAW,CAAC;IAChC,oBAAoB,EAAW,CAAC;IAChC,oBAAoB,EAAW,CAAC;IAChC,wBAAwB,EAAO,CAAC;IAChC,wBAAwB,EAAO,CAAC;IAChC,4BAA4B,EAAG,CAAC;IAChC,oEAAoE;IACpE,yEAAyE;IACzE,4EAA4E;IAC5E,kEAAkE;IAClE,sBAAsB,EAAW,CAAC;IAClC,+BAA+B,EAAE,CAAC;CAC1B,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAC7D,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC;KAClC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,0DAA0D,CAAC;KAC1F,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CACzB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAqC;IAC9D,aAAa,EAAO,CAAC;IACrB,iBAAiB,EAAG,CAAC;IACrB,SAAS,EAAW,CAAC;IACrB,iBAAiB,EAAG,CAAC;IACrB,kBAAkB,EAAE,CAAC;CACb,CAAC;AAgEX,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC;QACE,KAAK,CAAC,iGAAiG,CAAC,CAAC;QACzG,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAeD,0FAA0F;AAC1F,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,QAAgB;QAC1B,KAAK,CAAC,8CAA8C,QAAQ,qEAAqE,CAAC,CAAC;QACnI,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED;;;sEAGsE;AACtE,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,YAAY,UAAkB,EAAE,WAAmB;QACjD,KAAK,CAAC,wBAAwB,WAAW,gDAAgD,UAAU,iEAAiE,CAAC,CAAC;QACtK,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,QAAgB;QAC1B,KAAK,CACH,8BAA8B,QAAQ,oCAAoC;YAC1E,sFAAsF;YACtF,uDAAuD;YACvD,+EAA+E;YAC/E,uCAAuC,CACxC,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@phosra/gatekeeper",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
10
+ "./protocol": { "import": "./dist/protocol.js", "types": "./dist/protocol.d.ts" }
11
+ },
12
+ "files": ["dist", "README.md"],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "@openchildsafety/ocss": "^0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.7.0",
26
+ "@types/node": "^22.0.0",
27
+ "vitest": "^1.6.1",
28
+ "@phosra/link": "^0.1.0"
29
+ }
30
+ }