@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
+ // The composition root: everything a request needs, assembled from the environment once.
2
+ //
3
+ // It also holds the flows that are neither pure policy nor pure transport: enrolling this
4
+ // Gatekeeper as an outbound webhook endpoint, taking delivery of one, and retiring an actor.
5
+ //
6
+ // The POLICY arrives as an argument, never as an import. This package is the base a deployment
7
+ // installs; the policy is the one thing that deployment writes, so a `policy.config.ts` this
8
+ // module reached for would be a file the base owns and the operator cannot replace without
9
+ // forking it.
10
+ import { CatFactoryClient } from '@cat-factory/sdk';
11
+ import { buildCapability } from './capability.js';
12
+ import { requireVar } from './env.js';
13
+ import { GatekeeperError } from './errors.js';
14
+ import { KeyBroker } from './keys.js';
15
+ import { compilePolicy, tierForActor, } from './policy/compile.js';
16
+ import { cardEffectOf, readDelivery, SUBSCRIBED_CARD_TYPES } from './webhook/delivery.js';
17
+ import { verifyDelivery } from './webhook/signature.js';
18
+ /** Identifies this integration in the deployment's logs, beside the SDK's own version. */
19
+ const USER_AGENT = 'cat-factory-gatekeeper';
20
+ export class Gatekeeper {
21
+ #env;
22
+ #policy;
23
+ #state;
24
+ #keys;
25
+ constructor(env, policy) {
26
+ this.#env = env;
27
+ this.#policy = policy;
28
+ const baseUrl = requireVar(env, 'CAT_FACTORY_BASE_URL');
29
+ const clientFor = (apiKey) => new CatFactoryClient({ baseUrl, apiKey, userAgent: USER_AGENT });
30
+ // One durable object per PAIRED DEPLOYMENT, named for the origin it is paired with. The name
31
+ // is the pairing's own identity, so pointing this Worker at a different cat-factory gets a
32
+ // different object rather than inheriting the previous one's cards and minted keys.
33
+ this.#state = env.STATE.get(env.STATE.idFromName(baseUrl));
34
+ this.#keys = new KeyBroker({
35
+ state: this.#state,
36
+ provisioning: clientFor(requireVar(env, 'PROVISIONING_KEY')),
37
+ clientFor,
38
+ now: () => Date.now(),
39
+ });
40
+ }
41
+ /**
42
+ * Assemble against the deployment's own policy, compiling it.
43
+ *
44
+ * Throws a `PolicyError` on a policy an operator has to fix, and a `ConfigError` on a binding
45
+ * they have not set. Both are refusals the request path answers as a 503 naming the cause, which
46
+ * is why the compile happens per assembly rather than once at module load: a Worker whose policy
47
+ * threw while its module evaluated serves nothing at all, not even the refusal that says why.
48
+ */
49
+ static create(env, policy) {
50
+ return new Gatekeeper(env, compilePolicy(policy));
51
+ }
52
+ /** Whether the presented bearer token is this Gatekeeper's own. */
53
+ authorize(header) {
54
+ const expected = requireVar(this.#env, 'OS_SHARED_TOKEN');
55
+ const presented = header?.startsWith('Bearer ') === true ? header.slice('Bearer '.length) : '';
56
+ return timingSafeEqualStrings(presented, expected);
57
+ }
58
+ /**
59
+ * The capability for one actor the OS has authenticated.
60
+ *
61
+ * The tier is resolved from the Gatekeeper's OWN policy, never from anything the caller sent:
62
+ * an agent that could name its tier would be its own authorization.
63
+ */
64
+ capabilityFor(actor) {
65
+ const tier = tierForActor(this.#policy, actor.id);
66
+ if (tier === null) {
67
+ throw new GatekeeperError('unknown_actor', `No tier is granted to '${actor.id}'. Add them to this Gatekeeper's policy under ` +
68
+ 'grants, or set a defaultTier if this deployment means every OS user to have one.');
69
+ }
70
+ return buildCapability({ actor, tier, keys: this.#keys, state: this.#state });
71
+ }
72
+ /**
73
+ * Retire an actor: revoke every key this Gatekeeper minted for them, upstream and here.
74
+ *
75
+ * Exposed on the ADMIN surface rather than on a capability, because it is the OS deployment's
76
+ * offboarding action and not something an agent acting AS someone should be able to do to them.
77
+ */
78
+ async retire(actorId) {
79
+ return await this.#keys.revoke({ id: actorId });
80
+ }
81
+ /**
82
+ * Register this Worker as a named outbound webhook endpoint, idempotently.
83
+ *
84
+ * Safe to call on every cron tick and on demand: the route is keyed on the caller-chosen id, so
85
+ * re-asserting the registration heals a deployment whose endpoint was edited or removed without
86
+ * ever displacing a sibling integration's slot.
87
+ */
88
+ async enroll() {
89
+ const webhookId = requireVar(this.#env, 'WEBHOOK_ID');
90
+ const url = new URL('/webhook', requireVar(this.#env, 'PUBLIC_URL')).toString();
91
+ const provisioning = new CatFactoryClient({
92
+ baseUrl: requireVar(this.#env, 'CAT_FACTORY_BASE_URL'),
93
+ apiKey: requireVar(this.#env, 'PROVISIONING_KEY'),
94
+ userAgent: USER_AGENT,
95
+ });
96
+ await provisioning.webhook.setNamed(webhookId, {
97
+ url,
98
+ name: 'Cloudflare OS gatekeeper',
99
+ secret: requireVar(this.#env, 'WEBHOOK_SECRET'),
100
+ enabled: true,
101
+ // Subscribe to exactly the card types the inbox raises something for, rather than leaving
102
+ // `types` unset for the platform's default tail: the defaults and this Gatekeeper's list are
103
+ // close but not equal, and the gap would arrive as cards that never appear (a type acted on
104
+ // but not subscribed) or cards nobody can do anything with (the reverse). What each type
105
+ // OFFERS is the card's `disposition`, so the list can carry a `notice` without the inbox
106
+ // presenting it as answerable.
107
+ types: [...SUBSCRIBED_CARD_TYPES],
108
+ // The lifecycle family is what lets a status Gadget close a run out without polling: it
109
+ // lands as a `runs_watched()` record, and a terminal event settles the run's open cards. It
110
+ // is opt-in per event on purpose, so name all three: a Gatekeeper that hears only
111
+ // `run.started` shows every run as forever in flight.
112
+ runEvents: ['run.started', 'run.completed', 'run.failed'],
113
+ });
114
+ return { webhookId, url };
115
+ }
116
+ /**
117
+ * Take delivery of one webhook POST.
118
+ *
119
+ * Order matters and is the whole security story of this method: verify the MAC over the RAW
120
+ * bytes FIRST, parse SECOND, and hand the dedupe and the effect to durable state as ONE call.
121
+ * Parsing before verifying would run this deployment's JSON decoder over unauthenticated input.
122
+ * Deduping in a call of its own would be worse than useless: a marker committed before the card
123
+ * write turns a failed write into a `duplicate` on the platform's retry, so the approval never
124
+ * reaches the inbox and nothing anywhere reports that it did not.
125
+ */
126
+ async takeDelivery(request, now) {
127
+ const rawBody = await request.text();
128
+ const verdict = await verifyDelivery(request.headers, rawBody, requireVar(this.#env, 'WEBHOOK_SECRET'), now);
129
+ if (!verdict.ok)
130
+ return { handled: 'rejected', reason: verdict.reason };
131
+ let parsed;
132
+ try {
133
+ parsed = JSON.parse(rawBody);
134
+ }
135
+ catch {
136
+ return { handled: 'unparseable' };
137
+ }
138
+ const delivery = readDelivery(parsed);
139
+ if (delivery === null)
140
+ return { handled: 'unparseable' };
141
+ const applied = await this.#state.applyDelivery(delivery.deliveryId, cardEffectOf(delivery), now);
142
+ if (!applied.applied)
143
+ return { handled: 'duplicate', deliveryId: delivery.deliveryId };
144
+ return { handled: 'accepted', deliveryId: delivery.deliveryId, effect: applied.effect };
145
+ }
146
+ }
147
+ /**
148
+ * Compare two secrets without an early exit on the first differing character.
149
+ *
150
+ * Lengths are compared first and that difference does leak, which is acceptable for a token the
151
+ * caller supplies: the alternative (hashing both to a fixed width) buys nothing here, since the
152
+ * expected token's length is a deployment constant an attacker cannot vary their way into.
153
+ */
154
+ function timingSafeEqualStrings(a, b) {
155
+ if (a.length !== b.length)
156
+ return false;
157
+ let diff = 0;
158
+ for (let i = 0; i < a.length; i++)
159
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
160
+ return diff === 0;
161
+ }
162
+ //# sourceMappingURL=gatekeeper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gatekeeper.js","sourceRoot":"","sources":["../src/gatekeeper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export { createGatekeeperWorker, type GatekeeperWorkerOptions } from './worker.js';
2
+ export { Gatekeeper, type DeliveryOutcome } from './gatekeeper.js';
3
+ export { GatekeeperState, type ApprovalCard, type DeliveryApplication, type DeliveryEffect, type MintClaim, type MintTicket, type RunState, type StoredKey, } from './state.js';
4
+ export { ConfigError, type GatekeeperEnv } from './env.js';
5
+ export { buildCapability, type CardInspection, type SessionDependencies, type TierSummary, } from './capability.js';
6
+ export { KeyBroker, type Actor, type KeyBrokerDependencies, type LeasedClient } from './keys.js';
7
+ export { answerCard, assertAnswerable, describeStale, pendingParks, type AnswerInput, type AnswerOutcome, type BindingInvoker, type DecisionListShape, type PendingPark, } from './approvals.js';
8
+ export { cardEffectOf, dispositionOf, PARKED_DECISION_CARD_TYPES, readDelivery, SUBSCRIBED_CARD_TYPES, type CardDisposition, type Delivery, } from './webhook/delivery.js';
9
+ export { DEFAULT_MAX_SKEW_MS, verifyDelivery, type VerificationResult, } from './webhook/signature.js';
10
+ export * from './policy/index.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,sBAAsB,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAIlE,OAAO,EACL,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,SAAS,GACf,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EACL,eAAe,EACf,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,KAAK,EAAE,KAAK,qBAAqB,EAAE,KAAK,YAAY,EAAE,MAAM,WAAW,CAAA;AAEhG,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,0BAA0B,EAC1B,YAAY,EACZ,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,QAAQ,GACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,KAAK,kBAAkB,GACxB,MAAM,wBAAwB,CAAA;AAI/B,cAAc,mBAAmB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ // `@cat-factory/gatekeeper-worker`: the Gatekeeper machinery a deployment INSTALLS, as opposed to
2
+ // the policy and the wiring it WRITES (`deploy/gatekeeper` is the template for those).
3
+ //
4
+ // The split is what makes an upgrade a dependency bump. Everything here is deployment-neutral: the
5
+ // Cap'n Web capability surface, the per-actor key broker, the delivery receiver and its verifier,
6
+ // the approval inbox and the per-park answerers, the Durable Object all four keep their state in.
7
+ // What is NOT here is the one thing that differs per deployment, the policy, which arrives as an
8
+ // argument to `createGatekeeperWorker`.
9
+ export { createGatekeeperWorker } from './worker.js';
10
+ export { Gatekeeper } from './gatekeeper.js';
11
+ // The Durable Object class. A deployment re-exports it from its own entry module, because
12
+ // wrangler's `class_name` binding resolves against the Worker's exports, not this package's.
13
+ export { GatekeeperState, } from './state.js';
14
+ export { ConfigError } from './env.js';
15
+ export { buildCapability, } from './capability.js';
16
+ export { KeyBroker } from './keys.js';
17
+ export { answerCard, assertAnswerable, describeStale, pendingParks, } from './approvals.js';
18
+ export { cardEffectOf, dispositionOf, PARKED_DECISION_CARD_TYPES, readDelivery, SUBSCRIBED_CARD_TYPES, } from './webhook/delivery.js';
19
+ export { DEFAULT_MAX_SKEW_MS, verifyDelivery, } from './webhook/signature.js';
20
+ // The policy vocabulary, also reachable on its own at `@cat-factory/gatekeeper-worker/policy` for
21
+ // a policy file that should not have to load a Worker runtime to be read or tested.
22
+ export * from './policy/index.js';
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,uFAAuF;AACvF,EAAE;AACF,mGAAmG;AACnG,kGAAkG;AAClG,kGAAkG;AAClG,iGAAiG;AACjG,wCAAwC;AAExC,OAAO,EAAE,sBAAsB,EAAgC,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,UAAU,EAAwB,MAAM,iBAAiB,CAAA;AAElE,0FAA0F;AAC1F,6FAA6F;AAC7F,OAAO,EACL,eAAe,GAQhB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,WAAW,EAAsB,MAAM,UAAU,CAAA;AAC1D,OAAO,EACL,eAAe,GAIhB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,SAAS,EAA6D,MAAM,WAAW,CAAA;AAEhG,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,YAAY,GAMb,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,0BAA0B,EAC1B,YAAY,EACZ,qBAAqB,GAGtB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,mBAAmB,EACnB,cAAc,GAEf,MAAM,wBAAwB,CAAA;AAE/B,kGAAkG;AAClG,oFAAoF;AACpF,cAAc,mBAAmB,CAAA"}
package/dist/keys.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { CatFactoryClient } from '@cat-factory/sdk';
2
+ import type { PublicApiScope } from '@cat-factory/gatekeeper-bindings';
3
+ import type { GatekeeperState } from './state.js';
4
+ /** Who the OS says is calling. The Gatekeeper trusts this and NOTHING else the caller sends. */
5
+ export interface Actor {
6
+ /** The OS's own identity for the person. Rides `externalIdentity` onto every minted key. */
7
+ id: string;
8
+ /** Optional display name, used only to label the key for a human reading the key list. */
9
+ label?: string;
10
+ }
11
+ export interface KeyBrokerDependencies {
12
+ /** The paired workspace's durable state, holding minted keys. */
13
+ state: DurableObjectStub<GatekeeperState>;
14
+ /** A client on the Gatekeeper's own `admin` provisioning key. */
15
+ provisioning: CatFactoryClient;
16
+ /** Build a client on a minted key. */
17
+ clientFor: (apiKey: string) => CatFactoryClient;
18
+ now: () => number;
19
+ /** Wait, injected so the claim-contention path is testable without real time. */
20
+ sleep?: (ms: number) => Promise<void>;
21
+ }
22
+ /**
23
+ * A leased client, paired with the key it is authenticated as.
24
+ *
25
+ * The key id travels with it because the caller needs it on failure, not on success: dropping a
26
+ * rejected credential has to name WHICH one it is dropping, or a concurrent call that has already
27
+ * minted the replacement loses it.
28
+ */
29
+ export interface LeasedClient {
30
+ client: CatFactoryClient;
31
+ keyId: string;
32
+ }
33
+ export declare class KeyBroker {
34
+ #private;
35
+ constructor(deps: KeyBrokerDependencies);
36
+ /**
37
+ * Run one call as `actor`, at `scope`, re-minting once if the credential is rejected.
38
+ *
39
+ * The retry is deliberately narrow: ONE re-mint, on a 401 only, after dropping the row that
40
+ * produced it. Anything wider would turn a genuinely unusable deployment into a mint loop, and
41
+ * anything narrower would make the documented "rotate the provisioning key" kill switch a
42
+ * permanent outage for every actor whose key was cached.
43
+ *
44
+ * The scope is the TIER's, never the caller's request: a caller asking for a better key is
45
+ * asking the wrong side of the boundary.
46
+ */
47
+ run<T>(actor: Actor, scope: PublicApiScope, call: (client: CatFactoryClient) => Promise<T>): Promise<T>;
48
+ /**
49
+ * Revoke every key minted for an actor, for when the OS says they are gone.
50
+ *
51
+ * Revoked UPSTREAM FIRST, then forgotten, and only what was actually revoked is forgotten. The
52
+ * other order can leave a live cat-factory key that this Gatekeeper no longer knows exists,
53
+ * which is a credential nobody can find to revoke; this order can at worst re-revoke an id,
54
+ * which the platform documents as idempotent. A revoke that fails part-way leaves the rest of
55
+ * the rows in place, so a second call finishes the job rather than losing track of them.
56
+ */
57
+ revoke(actor: Actor): Promise<{
58
+ revoked: string[];
59
+ remaining: string[];
60
+ }>;
61
+ }
62
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,gBAAgB,EAA+B,MAAM,kBAAkB,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEtE,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,YAAY,CAAA;AAE5D,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,4FAA4F;IAC5F,EAAE,EAAE,MAAM,CAAA;IACV,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAWD,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,KAAK,EAAE,iBAAiB,CAAC,eAAe,CAAC,CAAA;IACzC,iEAAiE;IACjE,YAAY,EAAE,gBAAgB,CAAA;IAC9B,sCAAsC;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,gBAAgB,CAAA;IAC/C,GAAG,EAAE,MAAM,MAAM,CAAA;IACjB,iFAAiF;IACjF,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,gBAAgB,CAAA;IACxB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,qBAAa,SAAS;;IAIpB,YAAY,IAAI,EAAE,qBAAqB,EAGtC;IAED;;;;;;;;;;OAUG;IACG,GAAG,CAAC,CAAC,EACT,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAwBZ;IAED;;;;;;;;OAQG;IACG,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAgB9E;CAiDF"}
package/dist/keys.js ADDED
@@ -0,0 +1,152 @@
1
+ // Per-actor credentials: the reason an OS user's run is attributable at all.
2
+ //
3
+ // A Gatekeeper could forward every call on one shared workspace key, and every run would then be
4
+ // started by "the integration". That loses the two things the platform builds on the caller's
5
+ // identity: `externalIdentity` mapping a run back to a person without cat-factory holding your
6
+ // directory (slice 3), and role-scoped merge policy, which refuses a landing a shared credential
7
+ // should not make (ADR 0037/0039). So each actor gets their OWN key, minted at the tier's scope
8
+ // and stamped with their identity.
9
+ //
10
+ // Minting is CACHED, not per call. `POST /api/v1/keys` returns the raw secret exactly once, so a
11
+ // Gatekeeper that re-mints per call both spends a key row per operation and leaves a growing set
12
+ // of live credentials it has already forgotten how to revoke.
13
+ //
14
+ // Two things follow from "the mint is a real, non-idempotent side effect that returns its secret
15
+ // once", and both are the difference between a cache and a leak:
16
+ //
17
+ // - IT IS CLAIMED BEFORE IT RUNS. A read-then-mint-then-write has two pipelined calls for one
18
+ // actor both seeing "no key yet"; both mint, the second write wins, and the loser's key stays
19
+ // live upstream with nothing here recording that it exists. The claim is taken atomically in
20
+ // the Durable Object and expires, so a Worker that dies mid-mint does not wedge the actor out.
21
+ // - A REJECTED KEY IS DROPPED, NOT RETRIED FOREVER. The documented kill switch is revoking the
22
+ // provisioning key, which revokes every key it minted; a cache with no invalidation answers
23
+ // every call after that with the same dead secret and a 401 nobody can clear short of wiping
24
+ // the object. A 401 drops the row and re-mints ONCE, so a rotation heals itself and a genuinely
25
+ // unusable credential still fails rather than looping.
26
+ import { CatFactoryClient, CatFactoryUnauthorizedError } from '@cat-factory/sdk';
27
+ import { GatekeeperError } from './errors.js';
28
+ /** The label cap the key contract enforces. */
29
+ const MAX_LABEL_LENGTH = 120;
30
+ /** How many times a caller re-reads a claim another call is holding before giving up. */
31
+ const MINT_CLAIM_POLLS = 20;
32
+ /** How long between those reads. Short: the claim is held across one `POST /api/v1/keys`. */
33
+ const MINT_CLAIM_POLL_MS = 100;
34
+ export class KeyBroker {
35
+ #deps;
36
+ #sleep;
37
+ constructor(deps) {
38
+ this.#deps = deps;
39
+ this.#sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
40
+ }
41
+ /**
42
+ * Run one call as `actor`, at `scope`, re-minting once if the credential is rejected.
43
+ *
44
+ * The retry is deliberately narrow: ONE re-mint, on a 401 only, after dropping the row that
45
+ * produced it. Anything wider would turn a genuinely unusable deployment into a mint loop, and
46
+ * anything narrower would make the documented "rotate the provisioning key" kill switch a
47
+ * permanent outage for every actor whose key was cached.
48
+ *
49
+ * The scope is the TIER's, never the caller's request: a caller asking for a better key is
50
+ * asking the wrong side of the boundary.
51
+ */
52
+ async run(actor, scope, call) {
53
+ const leased = await this.#lease(actor, scope);
54
+ try {
55
+ return await call(leased.client);
56
+ }
57
+ catch (error) {
58
+ if (!(error instanceof CatFactoryUnauthorizedError))
59
+ throw error;
60
+ const dropped = await this.#deps.state.forgetKeyIfCurrent(actor.id, scope, leased.keyId);
61
+ if (!dropped) {
62
+ // Another call already replaced it, so the credential in hand is simply stale. Retrying on
63
+ // the replacement is the same one-shot, not a second one.
64
+ return await call((await this.#lease(actor, scope)).client);
65
+ }
66
+ const fresh = await this.#lease(actor, scope);
67
+ try {
68
+ return await call(fresh.client);
69
+ }
70
+ catch (retryError) {
71
+ if (!(retryError instanceof CatFactoryUnauthorizedError))
72
+ throw retryError;
73
+ throw new GatekeeperError('credential_rejected', `A freshly minted '${scope}' key for '${actor.id}' was refused by the deployment. The ` +
74
+ 'provisioning key is the likely cause: check it is still live and still `admin`-scoped.');
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Revoke every key minted for an actor, for when the OS says they are gone.
80
+ *
81
+ * Revoked UPSTREAM FIRST, then forgotten, and only what was actually revoked is forgotten. The
82
+ * other order can leave a live cat-factory key that this Gatekeeper no longer knows exists,
83
+ * which is a credential nobody can find to revoke; this order can at worst re-revoke an id,
84
+ * which the platform documents as idempotent. A revoke that fails part-way leaves the rest of
85
+ * the rows in place, so a second call finishes the job rather than losing track of them.
86
+ */
87
+ async revoke(actor) {
88
+ const stored = await this.#deps.state.listKeys(actor.id);
89
+ const revoked = [];
90
+ const revokedScopes = [];
91
+ for (const { scope, key } of stored) {
92
+ await this.#deps.provisioning.keys.revoke(key.keyId);
93
+ revoked.push(key.keyId);
94
+ revokedScopes.push(scope);
95
+ }
96
+ await this.#deps.state.forgetKeys(actor.id, revokedScopes);
97
+ return {
98
+ revoked,
99
+ remaining: stored
100
+ .filter(({ scope }) => !revokedScopes.includes(scope))
101
+ .map(({ key }) => key.keyId),
102
+ };
103
+ }
104
+ async #lease(actor, scope) {
105
+ for (let attempt = 0; attempt <= MINT_CLAIM_POLLS; attempt++) {
106
+ const ticket = await this.#deps.state.claimKeyMint(actor.id, scope, this.#deps.now());
107
+ if (ticket.outcome === 'existing') {
108
+ return { client: this.#deps.clientFor(ticket.key.secret), keyId: ticket.key.keyId };
109
+ }
110
+ if (ticket.outcome === 'claimed')
111
+ return await this.#mint(actor, scope);
112
+ await this.#sleep(Math.min(MINT_CLAIM_POLL_MS, ticket.retryAfterMs));
113
+ }
114
+ throw new GatekeeperError('credential_rejected', `Minting a '${scope}' key for '${actor.id}' is still in flight on another request after ` +
115
+ `${MINT_CLAIM_POLLS * MINT_CLAIM_POLL_MS}ms. Retry; if it persists, the deployment's ` +
116
+ '`POST /api/v1/keys` is failing in a way that leaves the claim to expire.');
117
+ }
118
+ /**
119
+ * Mint under a held claim.
120
+ *
121
+ * The claim is RELEASED on failure rather than left to expire: a deployment that is briefly
122
+ * unreachable should not lock the actor out for the claim's whole TTL, and the release is safe
123
+ * precisely because nothing was minted.
124
+ */
125
+ async #mint(actor, scope) {
126
+ let created;
127
+ try {
128
+ created = await this.#deps.provisioning.keys.create({
129
+ label: keyLabel(actor, scope),
130
+ scope: scope,
131
+ externalIdentity: actor.id,
132
+ });
133
+ }
134
+ catch (error) {
135
+ await this.#deps.state.releaseKeyMint(actor.id, scope);
136
+ throw error;
137
+ }
138
+ const key = {
139
+ keyId: created.key.id,
140
+ secret: created.secret,
141
+ mintedAt: this.#deps.now(),
142
+ };
143
+ // Stored BEFORE the client is handed out: the secret came back exactly once, so losing it
144
+ // between here and the caller would leave a live credential nobody can revoke.
145
+ await this.#deps.state.commitKeyMint(actor.id, scope, key);
146
+ return { client: this.#deps.clientFor(key.secret), keyId: key.keyId };
147
+ }
148
+ }
149
+ function keyLabel(actor, scope) {
150
+ return `gatekeeper ${scope}: ${actor.label ?? actor.id}`.slice(0, MAX_LABEL_LENGTH);
151
+ }
152
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /** What a masked leaf becomes. Stated, never removed. */
2
+ export declare const MASKED = "[masked by gatekeeper policy]";
3
+ /**
4
+ * Apply every masked path to one result, returning a copy. The input is never mutated: the same
5
+ * decoded body may be handed to more than one consumer (a card derivation and the caller's
6
+ * result), and a mask applied in place would silently redact the other one too.
7
+ */
8
+ export declare function applyMask(value: unknown, paths: readonly string[]): unknown;
9
+ //# sourceMappingURL=masking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masking.d.ts","sourceRoot":"","sources":["../src/masking.ts"],"names":[],"mappings":"AAYA,yDAAyD;AACzD,eAAO,MAAM,MAAM,kCAAkC,CAAA;AAiBrD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQ3E"}
@@ -0,0 +1,43 @@
1
+ // Field masking: dropping a value from a result before it reaches the caller.
2
+ //
3
+ // The one rule worth stating is that masking REPLACES rather than deletes. A removed key and a
4
+ // key the deployment had no value for read identically to the agent consuming the result, and
5
+ // they are different facts: "your policy hides this" is a question for an operator, while "the
6
+ // platform said null" is an answer about the run. So a masked leaf becomes the sentinel below,
7
+ // which says which of the two happened, in the shape every other value already has.
8
+ //
9
+ // Paths are dotted and traverse arrays element-wise (`steps.status` masks the status of every
10
+ // step). A path that matches nothing is not an error: a result shape legitimately varies by
11
+ // operation, and a policy naming a field some responses do not carry is not a misconfiguration.
12
+ /** What a masked leaf becomes. Stated, never removed. */
13
+ export const MASKED = '[masked by gatekeeper policy]';
14
+ function maskIn(value, segments) {
15
+ const [head, ...rest] = segments;
16
+ if (head === undefined)
17
+ return MASKED;
18
+ if (Array.isArray(value)) {
19
+ return value.map((element) => maskIn(element, segments));
20
+ }
21
+ if (value === null || typeof value !== 'object')
22
+ return value;
23
+ const source = value;
24
+ if (!(head in source))
25
+ return value;
26
+ return { ...source, [head]: maskIn(source[head], rest) };
27
+ }
28
+ /**
29
+ * Apply every masked path to one result, returning a copy. The input is never mutated: the same
30
+ * decoded body may be handed to more than one consumer (a card derivation and the caller's
31
+ * result), and a mask applied in place would silently redact the other one too.
32
+ */
33
+ export function applyMask(value, paths) {
34
+ let masked = value;
35
+ for (const path of paths) {
36
+ const segments = path.split('.').filter((segment) => segment.length > 0);
37
+ if (segments.length === 0)
38
+ continue;
39
+ masked = maskIn(masked, segments);
40
+ }
41
+ return masked;
42
+ }
43
+ //# sourceMappingURL=masking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masking.js","sourceRoot":"","sources":["../src/masking.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,+FAA+F;AAC/F,8FAA8F;AAC9F,+FAA+F;AAC/F,+FAA+F;AAC/F,oFAAoF;AACpF,EAAE;AACF,8FAA8F;AAC9F,4FAA4F;AAC5F,gGAAgG;AAEhG,yDAAyD;AACzD,MAAM,CAAC,MAAM,MAAM,GAAG,+BAA+B,CAAA;AAErD,SAAS,MAAM,CAAC,KAAc,EAAE,QAA2B;IACzD,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAA;IAChC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IAErC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;IAC1D,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE7D,MAAM,MAAM,GAAG,KAAgC,CAAA;IAC/C,IAAI,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAEnC,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAA;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc,EAAE,KAAwB;IAChE,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QACxE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,SAAQ;QACnC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { type GatekeeperBinding, type PublicApiScope } from '@cat-factory/gatekeeper-bindings';
2
+ /**
3
+ * What one tier of caller may do.
4
+ *
5
+ * `keyScope` is the scope of the cat-factory key the Gatekeeper mints for callers at this tier,
6
+ * and it is also the ceiling on what the tier can be granted. The two being ONE value is what
7
+ * keeps the credential and the capability from disagreeing.
8
+ */
9
+ export interface TierPolicy {
10
+ /** Prose an OS deployment shows beside the tier. */
11
+ description: string;
12
+ /** The scope of the per-actor key this tier's calls are made with. */
13
+ keyScope: PublicApiScope;
14
+ /** Binding names to grant, or `'*'` for every binding within `keyScope`. */
15
+ allow: readonly string[] | '*';
16
+ /** Binding names to subtract from `allow`. Applied last, so a deny always wins. */
17
+ deny?: readonly string[];
18
+ /**
19
+ * Dotted paths masked out of every result before it reaches the caller, e.g.
20
+ * `run.pullRequestUrl`. Masking is REDACTION, not omission: see `masking.ts`.
21
+ */
22
+ mask?: readonly string[];
23
+ }
24
+ /** The whole policy: the tiers, and which OS actor gets which. */
25
+ export interface GatekeeperPolicy {
26
+ /** The tier an actor with no explicit grant is given. Name `null` to refuse unknown actors. */
27
+ defaultTier: string | null;
28
+ tiers: Record<string, TierPolicy>;
29
+ /** OS user identity (whatever the OS authenticates) to tier name. */
30
+ grants: Record<string, string>;
31
+ }
32
+ /** Why a binding the deployment serves is not on a capability. */
33
+ export type WithheldReason = 'not_in_policy' | 'denied_by_policy' | 'above_key_scope' | 'not_relayable';
34
+ /** One binding the tier does not carry, and why. */
35
+ export interface WithheldBinding {
36
+ name: string;
37
+ reason: WithheldReason;
38
+ detail: string;
39
+ }
40
+ /** A tier, resolved against the live operation table. */
41
+ export interface CompiledTier {
42
+ name: string;
43
+ description: string;
44
+ keyScope: PublicApiScope;
45
+ granted: readonly GatekeeperBinding[];
46
+ /**
47
+ * Every binding this tier does NOT carry, with the reason. Published on the capability
48
+ * (`withheld()`) because an agent that cannot tell "policy withheld this" from "the deployment
49
+ * does not have it" will report the wrong one to whoever has to fix it.
50
+ */
51
+ withheld: readonly WithheldBinding[];
52
+ mask: readonly string[];
53
+ }
54
+ /** The policy, resolved once and read per request. */
55
+ export interface CompiledPolicy {
56
+ defaultTier: string | null;
57
+ tiers: ReadonlyMap<string, CompiledTier>;
58
+ grants: ReadonlyMap<string, string>;
59
+ }
60
+ /**
61
+ * Compile a policy against the live operation table, or throw a {@link PolicyError} naming the
62
+ * first thing an operator has to fix.
63
+ */
64
+ export declare function compilePolicy(policy: GatekeeperPolicy): CompiledPolicy;
65
+ /**
66
+ * The tier an actor holds, or a refusal.
67
+ *
68
+ * Note what is NOT here: nothing the CALLER sends picks a tier. The OS authenticates the person
69
+ * and the Gatekeeper resolves what that person may do, so a compromised or confused agent cannot
70
+ * ask for a better one.
71
+ */
72
+ export declare function tierForActor(policy: CompiledPolicy, actorId: string): CompiledTier | null;
73
+ /**
74
+ * What the OS needs to run its own approval governance over a call: the consequence the platform
75
+ * annotates, with the cautious default already applied.
76
+ */
77
+ export declare function describeBinding(binding: GatekeeperBinding): {
78
+ name: string;
79
+ summary: string;
80
+ minScope: PublicApiScope;
81
+ readOnly: boolean;
82
+ destructive: boolean;
83
+ idempotent: boolean;
84
+ pathParams: readonly string[];
85
+ queryParams: readonly string[];
86
+ hasBody: boolean;
87
+ };
88
+ //# sourceMappingURL=compile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/policy/compile.ts"],"names":[],"mappings":"AAaA,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,kCAAkC,CAAA;AAGzC;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,QAAQ,EAAE,cAAc,CAAA;IACxB,4EAA4E;IAC5E,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAA;IAC9B,mFAAmF;IACnF,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzB;AAED,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACjC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAED,kEAAkE;AAClE,MAAM,MAAM,cAAc,GACtB,eAAe,GACf,kBAAkB,GAClB,iBAAiB,GACjB,eAAe,CAAA;AAEnB,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,cAAc,CAAA;IACxB,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAA;IACrC;;;;OAIG;IACH,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IACpC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CACxB;AAED,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACxC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AA2HD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAqBtE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAIzF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,OAAO,EAAE,OAAO,CAAA;CACjB,CAaA"}