@cat-factory/gatekeeper-worker 0.3.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/dist/approvals.d.ts +90 -0
  4. package/dist/approvals.d.ts.map +1 -0
  5. package/dist/approvals.js +193 -0
  6. package/dist/approvals.js.map +1 -0
  7. package/dist/capability.d.ts +61 -0
  8. package/dist/capability.d.ts.map +1 -0
  9. package/dist/capability.js +125 -0
  10. package/dist/capability.js.map +1 -0
  11. package/dist/env.d.ts +45 -0
  12. package/dist/env.d.ts.map +1 -0
  13. package/dist/env.js +90 -0
  14. package/dist/env.js.map +1 -0
  15. package/dist/errors.d.ts +22 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +30 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/gatekeeper.d.ts +78 -0
  20. package/dist/gatekeeper.d.ts.map +1 -0
  21. package/dist/gatekeeper.js +162 -0
  22. package/dist/gatekeeper.js.map +1 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/keys.d.ts +62 -0
  28. package/dist/keys.d.ts.map +1 -0
  29. package/dist/keys.js +152 -0
  30. package/dist/keys.js.map +1 -0
  31. package/dist/masking.d.ts +9 -0
  32. package/dist/masking.d.ts.map +1 -0
  33. package/dist/masking.js +43 -0
  34. package/dist/masking.js.map +1 -0
  35. package/dist/policy/compile.d.ts +88 -0
  36. package/dist/policy/compile.d.ts.map +1 -0
  37. package/dist/policy/compile.js +170 -0
  38. package/dist/policy/compile.js.map +1 -0
  39. package/dist/policy/decisions.d.ts +93 -0
  40. package/dist/policy/decisions.d.ts.map +1 -0
  41. package/dist/policy/decisions.js +659 -0
  42. package/dist/policy/decisions.js.map +1 -0
  43. package/dist/policy/index.d.ts +6 -0
  44. package/dist/policy/index.d.ts.map +1 -0
  45. package/dist/policy/index.js +17 -0
  46. package/dist/policy/index.js.map +1 -0
  47. package/dist/state.d.ts +150 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +229 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/webhook/delivery.d.ts +102 -0
  52. package/dist/webhook/delivery.d.ts.map +1 -0
  53. package/dist/webhook/delivery.js +162 -0
  54. package/dist/webhook/delivery.js.map +1 -0
  55. package/dist/webhook/signature.d.ts +17 -0
  56. package/dist/webhook/signature.d.ts.map +1 -0
  57. package/dist/webhook/signature.js +73 -0
  58. package/dist/webhook/signature.js.map +1 -0
  59. package/dist/worker.d.ts +21 -0
  60. package/dist/worker.d.ts.map +1 -0
  61. package/dist/worker.js +155 -0
  62. package/dist/worker.js.map +1 -0
  63. package/package.json +62 -0
@@ -0,0 +1,162 @@
1
+ // Reading one delivery, and deciding what (if anything) it means for the approval inbox.
2
+ //
3
+ // The three families share an endpoint and are told apart by SHAPE (`notification` / `run` /
4
+ // `alert`), which is what this module does first. It deliberately validates only what it reads:
5
+ // the wire contract grows additively, so a delivery carrying a family or a notification type this
6
+ // package has never heard of is a thing to pass through as unrecognised, never a parse failure.
7
+ // A Gatekeeper that 400s an unknown shape would page an operator for a platform upgrade.
8
+ //
9
+ // The card derivation is the second half, and its rule is the one from the docs: the webhook is a
10
+ // TRIGGER and the API is the truth. So a card carries only what routes an answer (the run) and
11
+ // what a human reads (the title and body). The parked decision itself, with its ids and its quorum
12
+ // tally, is re-read from `/runs/:runId/decisions` at answer time, because between the delivery and
13
+ // the answer the run may have moved on entirely.
14
+ /**
15
+ * The card types that mean "a run is parked and a person has to answer", with what answering one
16
+ * would take.
17
+ *
18
+ * Deliberately a SMALL map rather than "anything with a runId": `pipeline_complete` and
19
+ * `ci_failed` also carry one and are reports, not questions. Raising an approval card for a report
20
+ * trains people to dismiss cards, which is how a real one gets missed. A deployment that registers
21
+ * its own gate and wants its card here adds the type; that is an edit to this constant, in review,
22
+ * rather than a behaviour that widens on its own.
23
+ *
24
+ * It is keyed by the SDK's own `NotificationType`, so a type this deployment retires fails the
25
+ * build here rather than becoming a card that silently stops arriving. Its keys are also the list
26
+ * `enroll()` SUBSCRIBES to, so what a Gatekeeper asks for and what it can act on are one value.
27
+ */
28
+ export const PARKED_DECISION_CARD_TYPES = {
29
+ requirement_review: 'decision',
30
+ clarity_review: 'decision',
31
+ decision_required: 'decision',
32
+ fork_decision_pending: 'decision',
33
+ judge_review: 'decision',
34
+ human_test_ready: 'decision',
35
+ visual_confirmation_ready: 'decision',
36
+ pr_review_ready: 'decision',
37
+ followup_pending: 'decision',
38
+ // The one notice. Settling it is a real merge through `notifications_act`, which no tier here
39
+ // grants: a shared credential is not one of the people a merge policy named. The card says a
40
+ // human has to finish the run in the cat-factory app.
41
+ merge_review: 'notice',
42
+ };
43
+ /** Every type this Gatekeeper subscribes to, answerable or not. */
44
+ export const SUBSCRIBED_CARD_TYPES = Object.keys(PARKED_DECISION_CARD_TYPES);
45
+ /**
46
+ * How a delivered card type is dispositioned, or `null` for one this Gatekeeper raises nothing for.
47
+ *
48
+ * Takes a plain `string` because the wire vocabulary grows additively: a deployment one release
49
+ * ahead of this package delivers a type the SDK's union has never heard of, and narrowing the
50
+ * ARGUMENT would make that a type error at the one place that must instead answer "no, not one of
51
+ * mine" and pass the delivery through.
52
+ */
53
+ export function dispositionOf(type) {
54
+ return PARKED_DECISION_CARD_TYPES[type] ?? null;
55
+ }
56
+ /** The lifecycle events a `run` delivery can carry, and whether each ends the run. */
57
+ const TERMINAL_RUN_EVENTS = new Set(['run.completed', 'run.failed']);
58
+ function asRecord(value) {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
60
+ ? value
61
+ : null;
62
+ }
63
+ function asString(value) {
64
+ return typeof value === 'string' && value.length > 0 ? value : null;
65
+ }
66
+ /**
67
+ * Classify a parsed delivery body.
68
+ *
69
+ * Returns `null` only when the envelope itself is unusable (no `deliveryId`), which is the one
70
+ * case where nothing can be done with it at all: the id is what dedupe keys on, so a delivery
71
+ * without one cannot be admitted safely even as unrecognised.
72
+ */
73
+ export function readDelivery(body) {
74
+ const envelope = asRecord(body);
75
+ if (envelope === null)
76
+ return null;
77
+ const deliveryId = asString(envelope.deliveryId);
78
+ if (deliveryId === null)
79
+ return null;
80
+ const sentAt = typeof envelope.sentAt === 'number' ? envelope.sentAt : 0;
81
+ const notification = asRecord(envelope.notification);
82
+ if (notification !== null) {
83
+ return {
84
+ family: 'notification',
85
+ deliveryId,
86
+ sentAt,
87
+ runId: asString(envelope.runId),
88
+ taskId: asString(envelope.taskId),
89
+ notification: {
90
+ id: asString(notification.id) ?? deliveryId,
91
+ type: asString(notification.type) ?? 'unknown',
92
+ status: asString(notification.status) ?? 'open',
93
+ title: asString(notification.title) ?? '',
94
+ body: asString(notification.body) ?? '',
95
+ },
96
+ };
97
+ }
98
+ const event = asString(envelope.event);
99
+ const run = asRecord(envelope.run);
100
+ if (event !== null && run !== null)
101
+ return { family: 'run', deliveryId, sentAt, event, run };
102
+ const alert = asRecord(envelope.alert);
103
+ if (event !== null && alert !== null)
104
+ return { family: 'alert', deliveryId, sentAt, event, alert };
105
+ return { family: 'unrecognised', deliveryId, sentAt };
106
+ }
107
+ /**
108
+ * Decide what a delivery asks durable state to do.
109
+ *
110
+ * A card is keyed on the NOTIFICATION id rather than the delivery id, because the platform
111
+ * re-delivers the same card as it resolves (`ntf_123-open` then `ntf_123-acted`) and those two
112
+ * deliveries are one question with an answer, not two questions. Dedupe still runs on the delivery
113
+ * id upstream of this: the two ids do different jobs and collapsing them would either drop the
114
+ * resolution or re-raise the card.
115
+ *
116
+ * A `run` delivery lands as a lifecycle record rather than nothing. That leg is why the
117
+ * subscription asks for all three run events: a status Gadget reads `runs_watched()` instead of
118
+ * polling, and a terminal event settles the run's open cards, so an inbox never holds a question
119
+ * about a run that has ended.
120
+ */
121
+ export function cardEffectOf(delivery) {
122
+ if (delivery.family === 'run') {
123
+ const runId = asString(delivery.run.id) ?? asString(delivery.run.runId);
124
+ if (runId === null)
125
+ return { kind: 'none' };
126
+ return {
127
+ kind: 'run-event',
128
+ state: {
129
+ runId,
130
+ event: delivery.event,
131
+ terminal: TERMINAL_RUN_EVENTS.has(delivery.event),
132
+ run: delivery.run,
133
+ },
134
+ };
135
+ }
136
+ if (delivery.family !== 'notification')
137
+ return { kind: 'none' };
138
+ const { notification, runId } = delivery;
139
+ const disposition = dispositionOf(notification.type);
140
+ if (disposition === null)
141
+ return { kind: 'none' };
142
+ if (notification.status !== 'open')
143
+ return { kind: 'supersede', cardId: notification.id };
144
+ if (runId === null)
145
+ return { kind: 'none' };
146
+ return {
147
+ kind: 'open',
148
+ card: {
149
+ cardId: notification.id,
150
+ runId,
151
+ taskId: delivery.taskId,
152
+ type: notification.type,
153
+ disposition,
154
+ title: notification.title,
155
+ body: notification.body,
156
+ raisedAt: delivery.sentAt,
157
+ resolvedAt: null,
158
+ resolution: null,
159
+ },
160
+ };
161
+ }
162
+ //# sourceMappingURL=delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.js","sourceRoot":"","sources":["../../src/webhook/delivery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Why a delivery was refused, or that it was accepted. */
2
+ export type VerificationResult = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ reason: 'missing_signature' | 'malformed_signature' | 'missing_timestamp' | 'stale_timestamp' | 'bad_signature';
7
+ };
8
+ /** Default replay window: the platform's own documented example. */
9
+ export declare const DEFAULT_MAX_SKEW_MS: number;
10
+ /**
11
+ * Verify one delivery.
12
+ *
13
+ * `rawBody` must be the exact text the request carried. `now` is injected so the skew check is
14
+ * testable without waiting; production passes `Date.now()`.
15
+ */
16
+ export declare function verifyDelivery(headers: Headers, rawBody: string, secret: string, now: number, maxSkewMs?: number): Promise<VerificationResult>;
17
+ //# sourceMappingURL=signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/webhook/signature.ts"],"names":[],"mappings":"AAgBA,2DAA2D;AAC3D,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IACE,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EACF,mBAAmB,GACnB,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,eAAe,CAAA;CACpB,CAAA;AAEL,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,QAAgB,CAAA;AAIhD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,MAA4B,GACtC,OAAO,CAAC,kBAAkB,CAAC,CAyB7B"}
@@ -0,0 +1,73 @@
1
+ // Verifying an outbound-webhook delivery, over the RAW request bytes.
2
+ //
3
+ // The platform signs `"<timestamp>.<raw body>"` with the endpoint's own secret and sends the MAC
4
+ // as `x-cat-factory-signature: v1=<hex>` beside `x-cat-factory-timestamp`. Two properties of that
5
+ // contract decide how this is written:
6
+ //
7
+ // - The timestamp is bound INTO the MAC, so it can be trusted, which is what makes a skew
8
+ // window a real replay defence rather than a field an attacker sets.
9
+ // - The MAC covers bytes, not a parsed value. So the body is read once, as text, and verified
10
+ // BEFORE any JSON parse. Re-serializing a parsed body to check it would compare a
11
+ // reconstruction against a signature over the original.
12
+ //
13
+ // The verdict is a discriminated result rather than a boolean because a receiver that cannot tell
14
+ // "no signature was sent" from "the signature was wrong" cannot tell an unconfigured endpoint
15
+ // from an attacker, and those need opposite reactions.
16
+ /** Default replay window: the platform's own documented example. */
17
+ export const DEFAULT_MAX_SKEW_MS = 5 * 60 * 1000;
18
+ const encoder = new TextEncoder();
19
+ /**
20
+ * Verify one delivery.
21
+ *
22
+ * `rawBody` must be the exact text the request carried. `now` is injected so the skew check is
23
+ * testable without waiting; production passes `Date.now()`.
24
+ */
25
+ export async function verifyDelivery(headers, rawBody, secret, now, maxSkewMs = DEFAULT_MAX_SKEW_MS) {
26
+ const signature = headers.get('x-cat-factory-signature');
27
+ if (signature === null || signature.length === 0)
28
+ return { ok: false, reason: 'missing_signature' };
29
+ if (!signature.startsWith('v1='))
30
+ return { ok: false, reason: 'malformed_signature' };
31
+ const provided = hexToBytes(signature.slice('v1='.length));
32
+ if (provided === null)
33
+ return { ok: false, reason: 'malformed_signature' };
34
+ const timestampHeader = headers.get('x-cat-factory-timestamp');
35
+ const timestamp = timestampHeader === null ? Number.NaN : Number(timestampHeader);
36
+ if (!Number.isFinite(timestamp))
37
+ return { ok: false, reason: 'missing_timestamp' };
38
+ if (Math.abs(now - timestamp) > maxSkewMs)
39
+ return { ok: false, reason: 'stale_timestamp' };
40
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
41
+ const expected = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(`${timestampHeader}.${rawBody}`)));
42
+ return equalBytes(expected, provided) ? { ok: true } : { ok: false, reason: 'bad_signature' };
43
+ }
44
+ /**
45
+ * Constant-time byte comparison.
46
+ *
47
+ * Hand-written rather than reached for from a library because workerd exposes no
48
+ * `timingSafeEqual` on the WebCrypto surface. The length check leaks only the LENGTH of the
49
+ * supplied signature, which the attacker already chose; the bytes themselves are compared with no
50
+ * early exit.
51
+ */
52
+ function equalBytes(a, b) {
53
+ if (a.length !== b.length)
54
+ return false;
55
+ let diff = 0;
56
+ for (let i = 0; i < a.length; i++)
57
+ diff |= a[i] ^ b[i];
58
+ return diff === 0;
59
+ }
60
+ /** Decode a lowercase-or-uppercase hex string, or `null` if it is not one. */
61
+ function hexToBytes(hex) {
62
+ if (hex.length === 0 || hex.length % 2 !== 0)
63
+ return null;
64
+ const bytes = new Uint8Array(hex.length / 2);
65
+ for (let i = 0; i < bytes.length; i++) {
66
+ const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
67
+ if (!Number.isInteger(byte))
68
+ return null;
69
+ bytes[i] = byte;
70
+ }
71
+ return /^[0-9a-fA-F]+$/.test(hex) ? bytes : null;
72
+ }
73
+ //# sourceMappingURL=signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature.js","sourceRoot":"","sources":["../../src/webhook/signature.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type GatekeeperEnv } from './env.js';
2
+ import type { GatekeeperPolicy } from './policy/compile.js';
3
+ /** What a deployment supplies to get a Worker. */
4
+ export interface GatekeeperWorkerOptions {
5
+ /**
6
+ * The tiers, the grants and the default, as the deployment wrote them.
7
+ *
8
+ * Compiled against the LIVE operation table on every assembly, so a policy naming a retired or
9
+ * above-scope operation refuses to serve rather than serving methods that 403.
10
+ */
11
+ policy: GatekeeperPolicy;
12
+ }
13
+ /**
14
+ * Build the Worker a deployment exports as its default.
15
+ *
16
+ * Everything it needs beyond the policy comes from the environment, so the returned handler is the
17
+ * same one this package tests: an operator's Worker and the suite in `test/` differ only in which
18
+ * policy they were given.
19
+ */
20
+ export declare function createGatekeeperWorker(options: GatekeeperWorkerOptions): ExportedHandler<GatekeeperEnv>;
21
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAyD,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAIpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,MAAM,EAAE,gBAAgB,CAAA;CACzB;AAwDD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,eAAe,CAAC,aAAa,CAAC,CAqGhC"}
package/dist/worker.js ADDED
@@ -0,0 +1,155 @@
1
+ // The Worker handler: five routes and a cron, and no logic of its own beyond translating between
2
+ // HTTP and the Gatekeeper.
3
+ //
4
+ // It is a FACTORY over the deployment's policy rather than a module-level `export default`, which
5
+ // is the whole shape of the base/template split. A deployment's entry point is then three lines
6
+ // (its policy, this factory, and the re-exported Durable Object class), so upgrading the machinery
7
+ // is a dependency bump instead of a re-merge against a file the operator has edited.
8
+ //
9
+ // POST /webhook the platform's outbound delivery receiver
10
+ // ALL /rpc the Cap'n Web endpoint the paired Cloudflare OS talks to
11
+ // POST /admin/enroll re-assert this Worker's webhook registration on demand
12
+ // POST /admin/retire revoke every key minted for one OS user, for offboarding
13
+ // GET /health liveness plus whether this deployment could serve at all
14
+ //
15
+ // Two conventions are worth stating because they look like sloppiness and are not.
16
+ //
17
+ // A REFUSED DELIVERY STILL ANSWERS 2xx-ADJACENT ONLY WHERE IT SHOULD. A delivery that fails its
18
+ // signature check is a 401: it is not from the platform, and telling the sender otherwise would
19
+ // make an attacker's probe indistinguishable from a real endpoint. Everything the platform DID
20
+ // sign is a 202, duplicates and unrecognised families included, because the platform's delivery
21
+ // contract retries a 5xx and gives up on a 4xx: reporting "I already have this one" as an error
22
+ // would spend a receiver's retry budget arguing about a message it has already handled.
23
+ //
24
+ // THE RPC ROUTE IS BEARER-GATED even though the intended path is a Worker service binding, which
25
+ // never traverses the internet. A Worker with a route attached is reachable by anyone who finds
26
+ // it, and a capability surface whose only defence is obscurity is not one.
27
+ import { newWorkersRpcResponse, RpcTarget } from 'capnweb';
28
+ import { ConfigError, describeMissingBindings, missingBindings } from './env.js';
29
+ import { GatekeeperError, PolicyError } from './errors.js';
30
+ import { Gatekeeper } from './gatekeeper.js';
31
+ /** The root capability: the only thing reachable before an actor is named. */
32
+ class GatekeeperApi extends RpcTarget {
33
+ #gatekeeper;
34
+ constructor(gatekeeper) {
35
+ super();
36
+ this.#gatekeeper = gatekeeper;
37
+ }
38
+ connect(request) {
39
+ const actorId = typeof request?.actorId === 'string' ? request.actorId : '';
40
+ if (actorId.length === 0) {
41
+ throw new GatekeeperError('unknown_actor', 'connect() needs the OS user identity as `actorId`. It is the value every minted key is ' +
42
+ 'stamped with, so a run can be traced back to a person.');
43
+ }
44
+ const actor = {
45
+ id: actorId,
46
+ ...(typeof request.label === 'string' ? { label: request.label } : {}),
47
+ };
48
+ return this.#gatekeeper.capabilityFor(actor);
49
+ }
50
+ }
51
+ function problem(status, reason, message) {
52
+ return Response.json({ error: { reason, message } }, { status });
53
+ }
54
+ /**
55
+ * Turn a configuration or policy failure into the refusal an operator can act on.
56
+ *
57
+ * Both are 503 rather than 500 because neither is a fault in the request: the deployment is not
58
+ * wired, which is exactly what the status class means, and the `reason` is what a monitor keys on.
59
+ */
60
+ function refuseSetup(error) {
61
+ if (error instanceof ConfigError)
62
+ return problem(503, 'not_configured', error.message);
63
+ if (error instanceof PolicyError)
64
+ return problem(503, 'policy_invalid', error.message);
65
+ return null;
66
+ }
67
+ /**
68
+ * Build the Worker a deployment exports as its default.
69
+ *
70
+ * Everything it needs beyond the policy comes from the environment, so the returned handler is the
71
+ * same one this package tests: an operator's Worker and the suite in `test/` differ only in which
72
+ * policy they were given.
73
+ */
74
+ export function createGatekeeperWorker(options) {
75
+ const { policy } = options;
76
+ return {
77
+ async fetch(request, env) {
78
+ const url = new URL(request.url);
79
+ try {
80
+ // Health runs BEFORE the assembly below, and asks the whole configuration rather than the
81
+ // two bindings that assembly happens to read. A monitor keyed on this route is asking
82
+ // "could a call served right now get past setup", and a Gatekeeper missing only its
83
+ // WEBHOOK_SECRET or OS_SHARED_TOKEN answers every /rpc call with a 503 while its liveness
84
+ // is perfect: green here and refusing everything there is the one answer this route must
85
+ // never give.
86
+ if (url.pathname === '/health') {
87
+ const missing = missingBindings(env);
88
+ if (missing.length > 0) {
89
+ return problem(503, 'not_configured', describeMissingBindings(missing));
90
+ }
91
+ // Assembling IS the rest of the check: it compiles the policy against the live operation
92
+ // table, and a policy that does not compile is a Gatekeeper that serves nothing.
93
+ Gatekeeper.create(env, policy);
94
+ return Response.json({ ok: true });
95
+ }
96
+ const gatekeeper = Gatekeeper.create(env, policy);
97
+ if (url.pathname === '/webhook') {
98
+ if (request.method !== 'POST')
99
+ return problem(405, 'method_not_allowed', 'POST only.');
100
+ const outcome = await gatekeeper.takeDelivery(request, Date.now());
101
+ if (outcome.handled === 'rejected') {
102
+ return problem(401, outcome.reason, 'This delivery did not verify against the endpoint secret.');
103
+ }
104
+ if (outcome.handled === 'unparseable') {
105
+ return problem(400, 'unparseable_delivery', 'The signed body is not a delivery envelope.');
106
+ }
107
+ return Response.json(outcome, { status: 202 });
108
+ }
109
+ if (!gatekeeper.authorize(request.headers.get('authorization'))) {
110
+ return problem(401, 'unauthorized', 'Present the paired deployment’s shared token.');
111
+ }
112
+ if (url.pathname === '/admin/enroll') {
113
+ if (request.method !== 'POST')
114
+ return problem(405, 'method_not_allowed', 'POST only.');
115
+ return Response.json(await gatekeeper.enroll());
116
+ }
117
+ // Offboarding. It sits on the admin surface rather than on a capability because it is a
118
+ // decision the OS deployment makes ABOUT a person, and an agent acting as one of them must
119
+ // not be able to make it for the others.
120
+ if (url.pathname === '/admin/retire') {
121
+ if (request.method !== 'POST')
122
+ return problem(405, 'method_not_allowed', 'POST only.');
123
+ const actorId = url.searchParams.get('actorId') ?? '';
124
+ if (actorId.length === 0) {
125
+ return problem(400, 'unknown_actor', 'Name the OS user identity to retire as `?actorId=`. It is the same value connect() ' +
126
+ 'takes, and the value every key minted for them is stamped with.');
127
+ }
128
+ return Response.json(await gatekeeper.retire(actorId));
129
+ }
130
+ if (url.pathname === '/rpc') {
131
+ return await newWorkersRpcResponse(request, new GatekeeperApi(gatekeeper));
132
+ }
133
+ return problem(404, 'no_such_route', `Nothing is served at ${url.pathname}.`);
134
+ }
135
+ catch (error) {
136
+ const refusal = refuseSetup(error);
137
+ if (refusal !== null)
138
+ return refusal;
139
+ throw error;
140
+ }
141
+ },
142
+ /**
143
+ * Re-assert the webhook registration.
144
+ *
145
+ * A cron rather than a one-shot at first boot: the registration lives on the cat-factory side,
146
+ * where it can be edited, disabled or dropped by someone tidying a workspace, and a Gatekeeper
147
+ * that enrolled once would then go quiet with nothing failing. Re-asserting is idempotent by the
148
+ * caller-chosen id, so it can never displace another integration's endpoint.
149
+ */
150
+ async scheduled(_controller, env, ctx) {
151
+ ctx.waitUntil(Gatekeeper.create(env, policy).enroll());
152
+ },
153
+ };
154
+ }
155
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@cat-factory/gatekeeper-worker",
3
+ "version": "0.3.1",
4
+ "description": "The Cloudflare OS Gatekeeper machinery for cat-factory as an installable library: policy-compiled Cap'n Web capabilities over the /api/v1 operation table, per-actor API-key minting, a verified outbound-webhook receiver and the approval inbox that answers every park a run stops on. A deployment supplies only its policy; deploy/gatekeeper is the template it copies.",
5
+ "keywords": [
6
+ "capnweb",
7
+ "cat-factory",
8
+ "cloudflare-os",
9
+ "cloudflare-workers",
10
+ "gatekeeper"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/kibertoad/cat-factory.git",
16
+ "directory": "sdk/gatekeeper-worker"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./policy": {
30
+ "types": "./dist/policy/index.d.ts",
31
+ "default": "./dist/policy/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "capnweb": "^0.10.0",
40
+ "@cat-factory/gatekeeper-bindings": "0.6.0",
41
+ "@cat-factory/sdk": "0.25.0"
42
+ },
43
+ "devDependencies": {
44
+ "@cloudflare/vitest-pool-workers": "^0.20.1",
45
+ "@cloudflare/workers-types": "^5.20260804.1",
46
+ "typescript": "7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "peerDependencies": {
50
+ "@cloudflare/workers-types": ">=4"
51
+ },
52
+ "engines": {
53
+ "node": ">=20"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc -b tsconfig.build.json",
57
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.dev.json --noEmit",
58
+ "test": "vitest",
59
+ "test:run": "vitest run",
60
+ "test:live": "vitest run --config vitest.live.config.ts"
61
+ }
62
+ }