@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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/approvals.d.ts +90 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +193 -0
- package/dist/approvals.js.map +1 -0
- package/dist/capability.d.ts +61 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +125 -0
- package/dist/capability.js.map +1 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +90 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/gatekeeper.d.ts +78 -0
- package/dist/gatekeeper.d.ts.map +1 -0
- package/dist/gatekeeper.js +162 -0
- package/dist/gatekeeper.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +62 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +152 -0
- package/dist/keys.js.map +1 -0
- package/dist/masking.d.ts +9 -0
- package/dist/masking.d.ts.map +1 -0
- package/dist/masking.js +43 -0
- package/dist/masking.js.map +1 -0
- package/dist/policy/compile.d.ts +88 -0
- package/dist/policy/compile.d.ts.map +1 -0
- package/dist/policy/compile.js +170 -0
- package/dist/policy/compile.js.map +1 -0
- package/dist/policy/decisions.d.ts +93 -0
- package/dist/policy/decisions.d.ts.map +1 -0
- package/dist/policy/decisions.js +659 -0
- package/dist/policy/decisions.js.map +1 -0
- package/dist/policy/index.d.ts +6 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +17 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/state.d.ts +150 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +229 -0
- package/dist/state.js.map +1 -0
- package/dist/webhook/delivery.d.ts +102 -0
- package/dist/webhook/delivery.d.ts.map +1 -0
- package/dist/webhook/delivery.js +162 -0
- package/dist/webhook/delivery.js.map +1 -0
- package/dist/webhook/signature.d.ts +17 -0
- package/dist/webhook/signature.d.ts.map +1 -0
- package/dist/webhook/signature.js +73 -0
- package/dist/webhook/signature.js.map +1 -0
- package/dist/worker.d.ts +21 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +155 -0
- package/dist/worker.js.map +1 -0
- 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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"}
|
package/dist/worker.d.ts
ADDED
|
@@ -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
|
+
}
|