@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
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @phosra/gatekeeper
|
|
2
|
+
|
|
3
|
+
Platform-side SDK for the OCSS (Open Child Safety Specification). Verifies signed
|
|
4
|
+
enforcement profiles, runs the local decision engine, sends §8.3.8 confirmation
|
|
5
|
+
receipts, and reports parent-side rating changes back to the Phosra census.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createGatekeeper } from "@phosra/gatekeeper"
|
|
11
|
+
import type { GatekeeperConfig, ReportParentChangeArgs } from "@phosra/gatekeeper"
|
|
12
|
+
|
|
13
|
+
// 1. Construct the gatekeeper with your platform's signing key + census URL.
|
|
14
|
+
const config: GatekeeperConfig = {
|
|
15
|
+
platformDid: "did:ocss:your-platform",
|
|
16
|
+
platformKeyId: "your-platform#key-2026",
|
|
17
|
+
gatekeeperSigningKey: { seed: new Uint8Array(32) /* real Ed25519 seed */, keyID: "did:ocss:your-platform#key-2026" },
|
|
18
|
+
censusBaseUrl: "https://api.sandbox.phosra.com",
|
|
19
|
+
trustRootXB64Url: process.env.OCSS_TRUST_ROOT_X!,
|
|
20
|
+
endpointId: "endpoint-mia-01", // the §9.3(b) bound resolver label
|
|
21
|
+
ratingMappings: [
|
|
22
|
+
{ ocssCategory: "content_rating", myField: "maturity", vocabulary: "mpaa" },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const gk = createGatekeeper(config)
|
|
27
|
+
|
|
28
|
+
// 2. Decide whether to allow a piece of content.
|
|
29
|
+
const verdict = gk.check("content_rating", { nativeValue: "PG-13" })
|
|
30
|
+
if (verdict.decision === "block") {
|
|
31
|
+
// block the content
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. Confirm enforcement outcome (§8.3.8 receipt).
|
|
35
|
+
await verdict.confirm("applied")
|
|
36
|
+
|
|
37
|
+
// 4. Report a parent-side rating change.
|
|
38
|
+
const args: ReportParentChangeArgs = {
|
|
39
|
+
ocssCategory: "content_rating",
|
|
40
|
+
nativeValue: "G",
|
|
41
|
+
changeScope: "family_wide",
|
|
42
|
+
}
|
|
43
|
+
const result = await gk.reportParentChange(args)
|
|
44
|
+
|
|
45
|
+
// 5. Clean up timers when done.
|
|
46
|
+
gk.destroy()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Error classes
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { RuleRefRequired, NoRatingMappingError, UnmappedRatingValueError } from "@phosra/gatekeeper"
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await gk.reportParentChange({ ocssCategory: "unknown_cat", nativeValue: "PG", changeScope: "platform_local" })
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (e instanceof NoRatingMappingError) { /* no mapping declared */ }
|
|
58
|
+
if (e instanceof UnmappedRatingValueError) { /* nativeValue not in crosswalk */ }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Pricing & keys
|
|
63
|
+
|
|
64
|
+
This SDK is **free and open** (MIT). It performs all signing/verification **locally** and calls
|
|
65
|
+
the hosted Phosra census over RFC 9421. Metered census usage requires a **Phosra API key** —
|
|
66
|
+
**billing is enforced server-side**, never in this package. Provision a key with `@phosra/api`
|
|
67
|
+
(the control-plane SDK), then use it here.
|
|
68
|
+
|
|
69
|
+
## The `/protocol` subpath
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { verifyDocument, verifyReceipt } from "@phosra/gatekeeper/protocol"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`@phosra/gatekeeper/protocol` is a verbatim re-export of `@openchildsafety/ocss` — identical object
|
|
76
|
+
identities. It lets you verify signed documents and receipts without a separate `@openchildsafety/ocss`
|
|
77
|
+
install when you already depend on this package.
|
|
78
|
+
|
|
79
|
+
## Stability
|
|
80
|
+
|
|
81
|
+
This package is `0.1.0` — live-proven but still co-evolving with the OCSS spec. Under `0.x`
|
|
82
|
+
semver, minor bumps (`0.1 → 0.2`) may include breaking changes with a one-minor deprecation
|
|
83
|
+
window. Exports marked `@experimental` are excluded from that window.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CapabilitiesDocument } from "@openchildsafety/ocss";
|
|
2
|
+
/**
|
|
3
|
+
* CapabilitiesCache fetches /.well-known/ocss/capabilities, verifies it to the OCSS ROOT
|
|
4
|
+
* (verifyCapabilitiesDocument — the capabilities_v1 twin of the trust-list root path), and
|
|
5
|
+
* admits it ONLY if fresh: not_after > now AND serial > the last admitted serial (anti-rollback).
|
|
6
|
+
* On any rejection the prior admitted doc (if still unexpired) is RETAINED; if none, doc() is
|
|
7
|
+
* undefined and the gatekeeper coercion defaults every unknown verb to block (fail-closed).
|
|
8
|
+
*/
|
|
9
|
+
export declare class CapabilitiesCache {
|
|
10
|
+
private opts;
|
|
11
|
+
private current?;
|
|
12
|
+
private lastSerial;
|
|
13
|
+
constructor(opts: {
|
|
14
|
+
fetchImpl: typeof fetch;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
trustRootXB64Url: string;
|
|
17
|
+
now: () => number;
|
|
18
|
+
});
|
|
19
|
+
refresh(): Promise<void>;
|
|
20
|
+
ensure(): Promise<void>;
|
|
21
|
+
/** The admitted-and-unexpired caps doc, or undefined (→ coercion defaults to block). */
|
|
22
|
+
doc(): CapabilitiesDocument | undefined;
|
|
23
|
+
private expireIfStale;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=capabilities-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities-cache.d.ts","sourceRoot":"","sources":["../src/capabilities-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmD,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAEnH;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,IAAI;IAHd,OAAO,CAAC,OAAO,CAAC,CAAuB;IACvC,OAAO,CAAC,UAAU,CAAM;gBAEd,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;IAYxB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAE7B,wFAAwF;IACxF,GAAG,IAAI,oBAAoB,GAAG,SAAS;IAEvC,OAAO,CAAC,aAAa;CAGtB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { verifyCapabilitiesDocument } from "@openchildsafety/ocss";
|
|
2
|
+
/**
|
|
3
|
+
* CapabilitiesCache fetches /.well-known/ocss/capabilities, verifies it to the OCSS ROOT
|
|
4
|
+
* (verifyCapabilitiesDocument — the capabilities_v1 twin of the trust-list root path), and
|
|
5
|
+
* admits it ONLY if fresh: not_after > now AND serial > the last admitted serial (anti-rollback).
|
|
6
|
+
* On any rejection the prior admitted doc (if still unexpired) is RETAINED; if none, doc() is
|
|
7
|
+
* undefined and the gatekeeper coercion defaults every unknown verb to block (fail-closed).
|
|
8
|
+
*/
|
|
9
|
+
export class CapabilitiesCache {
|
|
10
|
+
opts;
|
|
11
|
+
current;
|
|
12
|
+
lastSerial = -1;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.opts = opts;
|
|
15
|
+
}
|
|
16
|
+
async refresh() {
|
|
17
|
+
const res = await this.opts.fetchImpl(this.opts.baseUrl + "/.well-known/ocss/capabilities", { method: "GET" });
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
this.expireIfStale();
|
|
20
|
+
return;
|
|
21
|
+
} // transient/404 → keep prior-if-fresh, else none
|
|
22
|
+
const signed = (await res.json());
|
|
23
|
+
const doc = verifyCapabilitiesDocument(signed, this.opts.trustRootXB64Url); // throws on bad sig / wrong type
|
|
24
|
+
const naMs = Date.parse(doc.not_after);
|
|
25
|
+
if (!Number.isFinite(naMs) || naMs < this.opts.now()) {
|
|
26
|
+
this.expireIfStale();
|
|
27
|
+
return;
|
|
28
|
+
} // expired → reject
|
|
29
|
+
if (doc.serial <= this.lastSerial) {
|
|
30
|
+
this.expireIfStale();
|
|
31
|
+
return;
|
|
32
|
+
} // rollback → reject
|
|
33
|
+
this.current = doc;
|
|
34
|
+
this.lastSerial = doc.serial;
|
|
35
|
+
}
|
|
36
|
+
async ensure() { if (!this.current)
|
|
37
|
+
await this.refresh(); }
|
|
38
|
+
/** The admitted-and-unexpired caps doc, or undefined (→ coercion defaults to block). */
|
|
39
|
+
doc() { this.expireIfStale(); return this.current; }
|
|
40
|
+
expireIfStale() {
|
|
41
|
+
if (this.current && Date.parse(this.current.not_after) < this.opts.now())
|
|
42
|
+
this.current = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=capabilities-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities-cache.js","sourceRoot":"","sources":["../src/capabilities-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAkD,MAAM,uBAAuB,CAAC;AAEnH;;;;;;GAMG;AACH,MAAM,OAAO,iBAAiB;IAIlB;IAHF,OAAO,CAAwB;IAC/B,UAAU,GAAG,CAAC,CAAC,CAAC;IACxB,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,gCAAgC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/G,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC,CAAY,iDAAiD;QAC3G,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmB,CAAC;QACpD,MAAM,GAAG,GAAG,0BAA0B,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,iCAAiC;QAC7G,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC,CAAC,mBAAmB;QAC3G,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YAAC,OAAO;QAAC,CAAC,CAAqB,oBAAoB;QAC7G,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM,KAAoB,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAE1E,wFAAwF;IACxF,GAAG,KAAuC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAE9E,aAAa;QACnB,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YAAE,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;IACrG,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RatingMapping } from "./types.js";
|
|
2
|
+
export declare const CROSSWALKS: Record<RatingMapping["vocabulary"], Record<string, number>>;
|
|
3
|
+
/**
|
|
4
|
+
* contentAgeFor maps a platform's native rating code to its age equivalent on the
|
|
5
|
+
* `ratings_age` scale. A `codeMap` (declarative, platform-supplied) remaps a
|
|
6
|
+
* proprietary code to a standard board code first. Anything unresolved (unknown
|
|
7
|
+
* code, missing field, non-string) returns MAX_ORDINAL so the downstream
|
|
8
|
+
* compareToCeiling fails closed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function contentAgeFor(vocabulary: RatingMapping["vocabulary"], nativeCode: unknown, codeMap?: Record<string, string>): number;
|
|
11
|
+
//# sourceMappingURL=crosswalk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crosswalk.d.ts","sourceRoot":"","sources":["../src/crosswalk.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMlF,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,aAAa,CAAC,YAAY,CAAC,EACvC,UAAU,EAAE,OAAO,EACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,MAAM,CAMR"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @phosra/gatekeeper PRODUCT DATA — the standard-vocabulary board->age crosswalk.
|
|
2
|
+
// Values are AGES on the `ratings_age` scale (the SAME vocabulary as the census
|
|
3
|
+
// `params.max_allowed`), sourced verbatim from published board definitions. This
|
|
4
|
+
// is NOT OCSS-normative (the comparison primitive `compareToCeiling` is); it ships
|
|
5
|
+
// as product data with a documented promotion path (census-served signed data once
|
|
6
|
+
// a 2nd conformant gatekeeper + an active §8.3.7 lane exist). NO `custom` vocabulary,
|
|
7
|
+
// NO platform-supplied `blockAt`. A wrong mapping is load-bearing safety policy:
|
|
8
|
+
// an unmapped code resolves to MAX_ORDINAL (block, never allow).
|
|
9
|
+
import { MAX_ORDINAL } from "@openchildsafety/ocss";
|
|
10
|
+
export const CROSSWALKS = {
|
|
11
|
+
mpaa: { G: 0, PG: 8, "PG-13": 13, R: 17, "NC-17": 18 },
|
|
12
|
+
tv_parental: { "TV-Y": 0, "TV-G": 0, "TV-PG": 8, "TV-14": 14, "TV-MA": 17 },
|
|
13
|
+
esrb: { EC: 3, E: 6, "E10+": 10, T: 13, M: 17, AO: 18 },
|
|
14
|
+
pegi: { "3": 3, "7": 7, "12": 12, "16": 16, "18": 18 },
|
|
15
|
+
age_band: { under_13: 12, "13_15": 15, "16_17": 17 },
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* contentAgeFor maps a platform's native rating code to its age equivalent on the
|
|
19
|
+
* `ratings_age` scale. A `codeMap` (declarative, platform-supplied) remaps a
|
|
20
|
+
* proprietary code to a standard board code first. Anything unresolved (unknown
|
|
21
|
+
* code, missing field, non-string) returns MAX_ORDINAL so the downstream
|
|
22
|
+
* compareToCeiling fails closed.
|
|
23
|
+
*/
|
|
24
|
+
export function contentAgeFor(vocabulary, nativeCode, codeMap) {
|
|
25
|
+
if (typeof nativeCode !== "string")
|
|
26
|
+
return MAX_ORDINAL;
|
|
27
|
+
const boardCode = codeMap?.[nativeCode] ?? nativeCode;
|
|
28
|
+
const table = CROSSWALKS[vocabulary];
|
|
29
|
+
const age = table?.[boardCode];
|
|
30
|
+
return typeof age === "number" ? age : MAX_ORDINAL;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=crosswalk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crosswalk.js","sourceRoot":"","sources":["../src/crosswalk.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,gFAAgF;AAChF,iFAAiF;AACjF,mFAAmF;AACnF,mFAAmF;AACnF,sFAAsF;AACtF,iFAAiF;AACjF,iEAAiE;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGpD,MAAM,CAAC,MAAM,UAAU,GAAgE;IACrF,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;IACtD,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;IAC3E,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;IACvD,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;IACtD,QAAQ,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;CACrD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,UAAuC,EACvC,UAAmB,EACnB,OAAgC;IAEhC,IAAI,OAAO,UAAU,KAAK,QAAQ;QAAE,OAAO,WAAW,CAAC;IACvD,MAAM,SAAS,GAAG,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC;IACtD,MAAM,KAAK,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC;IAC/B,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-backcompat.test.d.ts","sourceRoot":"","sources":["../src/fleet-backcompat.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// fleet-backcompat.test.ts — P0 Task 9 TS-side integration test.
|
|
2
|
+
//
|
|
3
|
+
// Proves the BACKWARD-COMPATIBLE constraint from the gatekeeper / client side:
|
|
4
|
+
// - an old client advertising only the bare SPEC_VERSION token (no
|
|
5
|
+
// OCSS-Accept-Versions header, single bare -pre) negotiates -pre;
|
|
6
|
+
// - the gatekeeper accepting an echoed -pre profile version still accepts it;
|
|
7
|
+
// - fail-closed: no common version → null (rejected, never default-allow).
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { negotiate, SUPPORTED_VERSIONS, SPEC_VERSION } from "@openchildsafety/ocss";
|
|
10
|
+
describe("fleet back-compat", () => {
|
|
11
|
+
it("old gatekeeper accepts -pre echo; census picks -pre for an old client", () => {
|
|
12
|
+
// Census-side pick for a legacy client (single bare token — no range header):
|
|
13
|
+
// the census calls negotiate([SPEC_VERSION], SUPPORTED_VERSIONS) and resolves
|
|
14
|
+
// OCSS-v1.0-pre — no forced upgrade.
|
|
15
|
+
expect(negotiate([SPEC_VERSION], SUPPORTED_VERSIONS)).toBe("OCSS-v1.0-pre");
|
|
16
|
+
// Gatekeeper-side accept of the echoed -pre profile version: the gatekeeper
|
|
17
|
+
// calls negotiate([echoed], SUPPORTED_VERSIONS); echoed == "OCSS-v1.0-pre"
|
|
18
|
+
// → accepted.
|
|
19
|
+
expect(negotiate(["OCSS-v1.0-pre"], SUPPORTED_VERSIONS)).toBe("OCSS-v1.0-pre");
|
|
20
|
+
});
|
|
21
|
+
it("fail-closed: no common version → null (never default-allow)", () => {
|
|
22
|
+
expect(negotiate(["OCSS-v2.0"], SUPPORTED_VERSIONS)).toBeNull();
|
|
23
|
+
expect(negotiate(["OCSS-v9.9-GREASE-ff"], SUPPORTED_VERSIONS)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it("GREASE tolerated: real common version wins beside a GREASE token", () => {
|
|
26
|
+
// GREASE sentinel is skipped; the real -pre is returned.
|
|
27
|
+
expect(negotiate(["OCSS-v9.9-GREASE-ff", SPEC_VERSION], SUPPORTED_VERSIONS)).toBe("OCSS-v1.0-pre");
|
|
28
|
+
});
|
|
29
|
+
it("forward path: higher common version wins when both endpoints share it", () => {
|
|
30
|
+
// Simulate a future dual-version census locally (no global mutation).
|
|
31
|
+
const futureServerVers = ["OCSS-v1.0-pre", "OCSS-v1.1"];
|
|
32
|
+
// Forward-capable client gets the higher common version.
|
|
33
|
+
expect(negotiate(["OCSS-v1.0-pre", "OCSS-v1.1"], futureServerVers)).toBe("OCSS-v1.1");
|
|
34
|
+
// Old client gets -pre — ZERO forced upgrade.
|
|
35
|
+
expect(negotiate(["OCSS-v1.0-pre"], futureServerVers)).toBe("OCSS-v1.0-pre");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=fleet-backcompat.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-backcompat.test.js","sourceRoot":"","sources":["../src/fleet-backcompat.test.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,+EAA+E;AAC/E,qEAAqE;AACrE,sEAAsE;AACtE,gFAAgF;AAChF,6EAA6E;AAE7E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEpF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,8EAA8E;QAC9E,8EAA8E;QAC9E,qCAAqC;QACrC,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAE5E,4EAA4E;QAC5E,2EAA2E;QAC3E,cAAc;QACd,MAAM,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAChE,MAAM,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,yDAAyD;QACzD,MAAM,CAAC,SAAS,CAAC,CAAC,qBAAqB,EAAE,YAAY,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAC/E,eAAe,CAChB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,sEAAsE;QACtE,MAAM,gBAAgB,GAAG,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;QAExD,yDAAyD;QACzD,MAAM,CAAC,SAAS,CAAC,CAAC,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAEtF,8CAA8C;QAC9C,MAAM,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gatekeeper-version.test.d.ts","sourceRoot":"","sources":["../src/gatekeeper-version.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { negotiate, SUPPORTED_VERSIONS } from "@openchildsafety/ocss";
|
|
3
|
+
describe("gatekeeper version gate", () => {
|
|
4
|
+
it("accepts the echoed -pre version (backward-compat)", () => {
|
|
5
|
+
expect(negotiate(["OCSS-v1.0-pre"], SUPPORTED_VERSIONS)).not.toBeNull();
|
|
6
|
+
});
|
|
7
|
+
it("BLOCKS a GREASE / unknown echoed version", () => {
|
|
8
|
+
expect(negotiate(["OCSS-v9.9-GREASE-ff"], SUPPORTED_VERSIONS)).toBeNull(); // → throw at :173
|
|
9
|
+
expect(negotiate(["OCSS-v2.0"], SUPPORTED_VERSIONS)).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
//# sourceMappingURL=gatekeeper-version.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gatekeeper-version.test.js","sourceRoot":"","sources":["../src/gatekeeper-version.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEtE,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,kBAAkB;QAC7F,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type SigningKey } from "@openchildsafety/ocss";
|
|
2
|
+
import { OcssHttpClient } from "./ocss-http-client.js";
|
|
3
|
+
import { TrustListCache } from "./trust-list-cache.js";
|
|
4
|
+
import { CapabilitiesCache } from "./capabilities-cache.js";
|
|
5
|
+
import { type GatekeeperConfig, type Gatekeeper, type VerifiedProfile } from "./types.js";
|
|
6
|
+
/** Internal in-memory stores. */
|
|
7
|
+
declare class EnforcementProfileStore {
|
|
8
|
+
private m;
|
|
9
|
+
get(endpointId: string): VerifiedProfile | undefined;
|
|
10
|
+
put(endpointId: string, p: VerifiedProfile): void;
|
|
11
|
+
}
|
|
12
|
+
declare class ConsentStore {
|
|
13
|
+
private active;
|
|
14
|
+
private revoked;
|
|
15
|
+
markActive(endpointId: string): void;
|
|
16
|
+
markRevoked(endpointId: string): void;
|
|
17
|
+
isActive(endpointId: string): boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface GatekeeperRuntime {
|
|
20
|
+
cfg: GatekeeperConfig;
|
|
21
|
+
http: OcssHttpClient;
|
|
22
|
+
trustList: TrustListCache;
|
|
23
|
+
caps: CapabilitiesCache;
|
|
24
|
+
profileStore: EnforcementProfileStore;
|
|
25
|
+
consentStore: ConsentStore;
|
|
26
|
+
signingKey: SigningKey;
|
|
27
|
+
now: () => number;
|
|
28
|
+
/** P3: the endpoint_id_label delivered by gk.handleConnect; overrides cfg.endpointId
|
|
29
|
+
* for the no-arg accessors once a connect ceremony completes (Gap-7). */
|
|
30
|
+
connectedEndpointId?: string;
|
|
31
|
+
/** Idempotency guard: labels that have already been successfully connected.
|
|
32
|
+
* A re-delivered connect callback for an already-seen label returns 200 immediately
|
|
33
|
+
* without re-running refreshProfile. */
|
|
34
|
+
processedConnectLabels: Set<string>;
|
|
35
|
+
}
|
|
36
|
+
export declare function createGatekeeper(config: GatekeeperConfig): Gatekeeper;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=gatekeeper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gatekeeper.d.ts","sourceRoot":"","sources":["../src/gatekeeper.ts"],"names":[],"mappings":"AACA,OAAO,EAAuJ,KAAK,UAAU,EAAgC,MAAM,uBAAuB,CAAC;AAG3O,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAEf,KAAK,eAAe,EAKrB,MAAM,YAAY,CAAC;AAsBpB,iCAAiC;AACjC,cAAM,uBAAuB;IAC3B,OAAO,CAAC,CAAC,CAAsC;IAC/C,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAGpD,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,GAAG,IAAI;CAGlD;AAED,cAAM,YAAY;IAChB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAqB;IACpC,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIpC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAGrC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;CAGtC;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,gBAAgB,CAAC;IACtB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,cAAc,CAAC;IAC1B,IAAI,EAAE,iBAAiB,CAAC;IACxB,YAAY,EAAE,uBAAuB,CAAC;IACtC,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB;8EAC0E;IAC1E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;6CAEyC;IACzC,sBAAsB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrC;AAOD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA4ErE"}
|