@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.
- package/README.md +83 -0
- package/dist/capabilities-cache.d.ts +25 -0
- package/dist/capabilities-cache.d.ts.map +1 -0
- package/dist/capabilities-cache.js +45 -0
- package/dist/capabilities-cache.js.map +1 -0
- package/dist/crosswalk.d.ts +11 -0
- package/dist/crosswalk.d.ts.map +1 -0
- package/dist/crosswalk.js +32 -0
- package/dist/crosswalk.js.map +1 -0
- package/dist/fleet-backcompat.test.d.ts +2 -0
- package/dist/fleet-backcompat.test.d.ts.map +1 -0
- package/dist/fleet-backcompat.test.js +38 -0
- package/dist/fleet-backcompat.test.js.map +1 -0
- package/dist/gatekeeper-version.test.d.ts +2 -0
- package/dist/gatekeeper-version.test.d.ts.map +1 -0
- package/dist/gatekeeper-version.test.js +12 -0
- package/dist/gatekeeper-version.test.js.map +1 -0
- package/dist/gatekeeper.d.ts +38 -0
- package/dist/gatekeeper.d.ts.map +1 -0
- package/dist/gatekeeper.js +466 -0
- package/dist/gatekeeper.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/ocss-http-client.d.ts +21 -0
- package/dist/ocss-http-client.d.ts.map +1 -0
- package/dist/ocss-http-client.js +45 -0
- package/dist/ocss-http-client.js.map +1 -0
- package/dist/protocol.d.ts +10 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +10 -0
- package/dist/protocol.js.map +1 -0
- package/dist/trust-list-cache.d.ts +23 -0
- package/dist/trust-list-cache.d.ts.map +1 -0
- package/dist/trust-list-cache.js +35 -0
- package/dist/trust-list-cache.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +104 -0
- package/dist/types.js.map +1 -0
- 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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|