@titan-design/hitl 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -10
- package/dist/{base-store-BvwOJL6M.d.ts → base-store-BrVibYRJ.d.ts} +23 -6
- package/dist/{chunk-4GAZKT6Z.js → chunk-PMEJTMSE.js} +42 -8
- package/dist/chunk-PMEJTMSE.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/sqlite.d.ts +9 -8
- package/dist/sqlite.js +86 -32
- package/dist/sqlite.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-4GAZKT6Z.js.map +0 -1
package/README.md
CHANGED
|
@@ -51,7 +51,11 @@ Somewhere else entirely, in a CLI, an MCP tool, or a dashboard route:
|
|
|
51
51
|
```ts
|
|
52
52
|
import { resolveGate, cancelGate } from "@titan-design/hitl";
|
|
53
53
|
|
|
54
|
-
resolveGate(store, "deploy-approval", { approved: true }
|
|
54
|
+
resolveGate(store, "deploy-approval", { approved: true }, {
|
|
55
|
+
class: "owner-terminal",
|
|
56
|
+
id: "owner",
|
|
57
|
+
channel: "cli",
|
|
58
|
+
});
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
After a restart, re-attach by id instead of re-opening:
|
|
@@ -89,7 +93,7 @@ both pass the same behaviour suite.
|
|
|
89
93
|
|
|
90
94
|
`SqliteGateStore` installs a `hitl_gate` table through store-sqlite's
|
|
91
95
|
`runMigrations` on construction. Pass `migrate: false` and put `gateMigration(n)`
|
|
92
|
-
in the product's own migration list when hitl shares a database with domain
|
|
96
|
+
and `gateResolverMigration(m)` in the product's own migration list when hitl shares a database with domain
|
|
93
97
|
tables. `table` renames the table so one database can host several gate spaces.
|
|
94
98
|
Timestamps are ISO-8601 strings, the shape store-sqlite writes and any surface
|
|
95
99
|
can send on as-is.
|
|
@@ -99,7 +103,7 @@ tell its waiter why.
|
|
|
99
103
|
|
|
100
104
|
## Who resolved it
|
|
101
105
|
|
|
102
|
-
`resolve` and `resolveGate`
|
|
106
|
+
`resolve` and `resolveGate` require a third argument, a `GateResolver`:
|
|
103
107
|
`{ class, id, channel, confirmEvent? }`. `class` is an actor class from
|
|
104
108
|
`@titan-design/authority`. The store records it as `resolvedBy`.
|
|
105
109
|
|
|
@@ -116,23 +120,88 @@ agent or automation never answers a gate. It throws `GateResolverRefused` and th
|
|
|
116
120
|
pending. The store reads each declared resolver field once into a frozen copy, and checks and stores
|
|
117
121
|
only that copy. A store's `authorize` option runs after the class check and can refuse more,
|
|
118
122
|
never fewer. It must return `{ allowed }` synchronously, or the store throws
|
|
119
|
-
`GateAuthorizeInvalid`.
|
|
123
|
+
`GateAuthorizeInvalid`.
|
|
120
124
|
Refusals name the gate id and the actor class, never the resolver's other fields.
|
|
121
125
|
|
|
122
126
|
hitl records a claim about the resolver; it cannot prove one. Any process that can write
|
|
123
127
|
the database can claim any class.
|
|
124
128
|
|
|
125
129
|
On SQLite the resolver lives in a `resolved_by` column that `gateResolverMigration(n)` adds,
|
|
126
|
-
along with a trigger that refuses any resolve naming no resolver.
|
|
127
|
-
|
|
128
|
-
before it read back with `resolvedBy`
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
along with a trigger that refuses any resolve naming no resolver. `migrate: true` runs it as
|
|
131
|
+
version 2; with `migrate: false`, add it to your own migration list after `gateMigration`. It
|
|
132
|
+
is idempotent and does not backfill: gates resolved before it read back with `resolvedBy`
|
|
133
|
+
undefined. `SqliteGateStore` checks for the column when it is constructed and throws
|
|
134
|
+
`GateStoreSchemaOutdated` naming `gateResolverMigration` when it is missing, or naming
|
|
135
|
+
`gateMigration` when the table does not exist. That error carries an empty `gateId`,
|
|
136
|
+
because no gate is involved yet.
|
|
131
137
|
|
|
132
138
|
After the migration, a writer built on hitl 0.2.x fails when it resolves: SQLite aborts the
|
|
133
139
|
statement with a raw error whose message is `hitl: resolvedBy required`. The same trigger
|
|
134
140
|
refuses a direct insert of a resolved row with no resolver. Cancels from an old writer still
|
|
135
|
-
work. The fix is to upgrade that writer so it passes a resolver.
|
|
141
|
+
work. The fix is to upgrade that writer so it passes a resolver. A caller on this release
|
|
142
|
+
never reaches the trigger: every store refuses a resolve with no resolver first.
|
|
143
|
+
|
|
144
|
+
## Upgrading to 0.4
|
|
145
|
+
|
|
146
|
+
0.4 makes the resolver required. It is a breaking release; a `^0.3` range does not pick it up.
|
|
147
|
+
|
|
148
|
+
1. Pass a `GateResolver` to every `store.resolve`, `resolveGate` and workflow
|
|
149
|
+
`runtime.signal` call. The compiler finds each one. Name the class honestly: a human at
|
|
150
|
+
a terminal is `owner-terminal`, a human on a phone is `owner-remote`. An agent or an
|
|
151
|
+
automation cannot resolve a gate, by design.
|
|
152
|
+
2. Add `gateResolverMigration(n)` to your migration list, after `gateMigration`, with the
|
|
153
|
+
next free version in your own list. `migrate: true` stores run it for you as version 2.
|
|
154
|
+
3. Expect `GateStoreSchemaOutdated` naming `gateResolverMigration` from the
|
|
155
|
+
`SqliteGateStore` constructor if step 2 is missing. The store no longer opens over such a
|
|
156
|
+
table.
|
|
157
|
+
4. Upgrade every process that writes the gate table at once. A writer still on 0.2 fails
|
|
158
|
+
each resolve with SQLite's `hitl: resolvedBy required` once the migration has run.
|
|
159
|
+
|
|
160
|
+
A caller that bypasses the type and resolves with no resolver gets `GateResolverRefused`
|
|
161
|
+
with the reason `a resolver is required` from every store, memory or SQLite, and the gate
|
|
162
|
+
stays pending.
|
|
163
|
+
|
|
164
|
+
## Rule-bound gates
|
|
165
|
+
|
|
166
|
+
A gate can carry the authority rule that opened it. Pass `rule` to `create`:
|
|
167
|
+
`{ table, version, ruleId, resolvers }`, where `resolvers` lists the resolver classes the
|
|
168
|
+
rule admits, as `evaluate` from `@titan-design/authority` returns them.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
store.create({
|
|
172
|
+
id: "release-approval",
|
|
173
|
+
prompt: "Publish the release?",
|
|
174
|
+
rule: { table: "F5", version: "1.0.0", ruleId: "REL-CO", resolvers: ["owner-terminal"] },
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The store reads the rule once into a frozen copy, so the caller cannot widen it after
|
|
179
|
+
`create`. A rule that is not a non-empty list of resolver classes throws `GateRuleInvalid`
|
|
180
|
+
and creates nothing. A rule-bound gate refuses a resolver whose class the rule does not
|
|
181
|
+
name, and refuses a resolve that names no resolver. Both throw `GateResolverRefused` and
|
|
182
|
+
leave the gate pending. The check runs after the default class check and before
|
|
183
|
+
`authorize`, so it only narrows. A gate without a rule behaves as before.
|
|
184
|
+
|
|
185
|
+
On SQLite the rule lives in a `rule` column that `gateRuleMigration(n)` adds, along with a
|
|
186
|
+
trigger that aborts any update resolving a rule-bound row by a class outside its rule, or
|
|
187
|
+
changing the rule, with `hitl: resolver outside the gate rule`. That stops a writer built on
|
|
188
|
+
hitl 0.3.x, which knows nothing of rules, from widening who may answer. The migration is
|
|
189
|
+
idempotent and does not backfill. `migrate: true` runs it as version 3. A store whose table
|
|
190
|
+
lacks the column throws `GateStoreSchemaOutdated` naming `gateRuleMigration` when handed a
|
|
191
|
+
rule, rather than dropping it.
|
|
192
|
+
|
|
193
|
+
The trigger needs `resolved_by` to know who answered. On a table that has run
|
|
194
|
+
`gateRuleMigration` but not `gateResolverMigration`, the trigger aborts every raw resolve of
|
|
195
|
+
a rule-bound row, and no store opens over the table. Run both migrations in either order;
|
|
196
|
+
the second one installs the class-aware trigger.
|
|
197
|
+
|
|
198
|
+
The insert guard fires before SQLite's conflict handling. A raw insert with no rule onto a
|
|
199
|
+
pending rule-bound id therefore aborts with `hitl: a pending rule-bound gate cannot be
|
|
200
|
+
replaced`, whether it is a plain duplicate, `ON CONFLICT DO UPDATE`, `ON CONFLICT DO NOTHING`
|
|
201
|
+
or `INSERT OR IGNORE`; a duplicate plain insert reports that message, not `UNIQUE`. The guard
|
|
202
|
+
covers only pending rows. A raw `REPLACE` of a cancelled or resolved rule-bound row succeeds
|
|
203
|
+
and can leave a pending rule-less row. That is outside the store API: `create` is a plain
|
|
204
|
+
insert and throws on an existing id, and no store method replaces a row.
|
|
136
205
|
|
|
137
206
|
## Settling
|
|
138
207
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActorClass } from '@titan-design/authority';
|
|
1
|
+
import { ResolverClass, ActorClass } from '@titan-design/authority';
|
|
2
2
|
|
|
3
3
|
/** A gate is a row, never a promise: pending work survives the process that opened it. */
|
|
4
4
|
type GateStatus = "pending" | "resolved" | "cancelled" | "expired";
|
|
@@ -18,6 +18,15 @@ interface GateRecord {
|
|
|
18
18
|
expiresAt: string | undefined;
|
|
19
19
|
/** Who resolved the gate, as claimed by the caller; unset for gates resolved before stores recorded it. */
|
|
20
20
|
resolvedBy: GateResolver | undefined;
|
|
21
|
+
/** The authority rule that opened the gate; unset for a gate no rule governs. */
|
|
22
|
+
rule: GateRule | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Binds a gate to the authority rule that opened it, so only that rule's resolver classes may answer. */
|
|
25
|
+
interface GateRule {
|
|
26
|
+
table: string;
|
|
27
|
+
version: string;
|
|
28
|
+
ruleId: string;
|
|
29
|
+
resolvers: ResolverClass[];
|
|
21
30
|
}
|
|
22
31
|
/** A claim about who answered a gate. hitl records it and checks its class; it cannot prove it. */
|
|
23
32
|
interface GateResolver {
|
|
@@ -44,6 +53,7 @@ interface GateInput {
|
|
|
44
53
|
prompt: string;
|
|
45
54
|
schema?: JsonSchema;
|
|
46
55
|
expiresAt?: Date | string;
|
|
56
|
+
rule?: GateRule;
|
|
47
57
|
}
|
|
48
58
|
/**
|
|
49
59
|
* Synchronous on purpose: both implementations are local (a Map, and
|
|
@@ -52,7 +62,7 @@ interface GateInput {
|
|
|
52
62
|
interface GateStore {
|
|
53
63
|
create(input: GateInput): GateRecord;
|
|
54
64
|
get(id: string): GateRecord | undefined;
|
|
55
|
-
resolve(id: string, payload: unknown, resolvedBy
|
|
65
|
+
resolve(id: string, payload: unknown, resolvedBy: GateResolver): GateRecord;
|
|
56
66
|
cancel(id: string, reason: string): GateRecord;
|
|
57
67
|
listPending(): GateRecord[];
|
|
58
68
|
}
|
|
@@ -89,12 +99,19 @@ declare class GateResolverRefused extends GateError {
|
|
|
89
99
|
readonly reason: string;
|
|
90
100
|
constructor(gateId: string, actorClass: string | undefined, reason: string);
|
|
91
101
|
}
|
|
92
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* The store cannot record a resolver or a rule because its table predates the migration that adds the column.
|
|
104
|
+
* `gateId` is empty when the store refuses at construction, before any gate is named.
|
|
105
|
+
*/
|
|
93
106
|
declare class GateStoreSchemaOutdated extends GateError {
|
|
94
107
|
readonly table: string;
|
|
95
108
|
readonly migration: string;
|
|
96
109
|
constructor(gateId: string, table: string, migration: string);
|
|
97
110
|
}
|
|
111
|
+
declare class GateRuleInvalid extends GateError {
|
|
112
|
+
readonly reason: string;
|
|
113
|
+
constructor(gateId: string, reason: string);
|
|
114
|
+
}
|
|
98
115
|
declare class GateAuthorizeInvalid extends GateError {
|
|
99
116
|
constructor(gateId: string);
|
|
100
117
|
}
|
|
@@ -113,15 +130,15 @@ declare abstract class BaseGateStore implements GateStore {
|
|
|
113
130
|
protected abstract readByStatus(status: GateRecord["status"]): GateRecord[];
|
|
114
131
|
create(input: GateInput): GateRecord;
|
|
115
132
|
get(id: string): GateRecord | undefined;
|
|
116
|
-
resolve(id: string, payload: unknown, resolvedBy
|
|
133
|
+
resolve(id: string, payload: unknown, resolvedBy: GateResolver): GateRecord;
|
|
117
134
|
cancel(id: string, reason: string): GateRecord;
|
|
118
135
|
listPending(): GateRecord[];
|
|
119
136
|
private requirePending;
|
|
120
|
-
/** The default check runs first and `authorize`
|
|
137
|
+
/** The default check runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */
|
|
121
138
|
private requireAuthorized;
|
|
122
139
|
/** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
|
|
123
140
|
private lapseIfExpired;
|
|
124
141
|
protected nowIso(): string;
|
|
125
142
|
}
|
|
126
143
|
|
|
127
|
-
export { BaseGateStore as B, type GateRecord as G, type JsonSchema as J, type GateStore as a, type GateResolver as b, type GateAuthorize as c, GateAborted as d, GateAlreadyExists as e, GateAlreadySettled as f, type GateAuthorization as g, GateAuthorizeInvalid as h, GateCancelled as i, GateError as j, GateExpired as k, type GateInput as l, GateNotFound as m, GatePayloadInvalid as n, GateResolverRefused as o, type
|
|
144
|
+
export { BaseGateStore as B, type GateRecord as G, type JsonSchema as J, type GateStore as a, type GateResolver as b, type GateAuthorize as c, GateAborted as d, GateAlreadyExists as e, GateAlreadySettled as f, type GateAuthorization as g, GateAuthorizeInvalid as h, GateCancelled as i, GateError as j, GateExpired as k, type GateInput as l, GateNotFound as m, GatePayloadInvalid as n, GateResolverRefused as o, type GateRule as p, GateRuleInvalid as q, type GateStatus as r, GateStoreSchemaOutdated as s };
|
|
@@ -59,13 +59,21 @@ var GateResolverRefused = class extends GateError {
|
|
|
59
59
|
};
|
|
60
60
|
var GateStoreSchemaOutdated = class extends GateError {
|
|
61
61
|
constructor(gateId, table, migration) {
|
|
62
|
-
|
|
62
|
+
const message = gateId === "" ? `cannot open a gate store on table ${table}: it needs ${migration}` : `gate ${gateId} cannot be recorded: table ${table} needs ${migration}`;
|
|
63
|
+
super(message, gateId);
|
|
63
64
|
this.table = table;
|
|
64
65
|
this.migration = migration;
|
|
65
66
|
}
|
|
66
67
|
table;
|
|
67
68
|
migration;
|
|
68
69
|
};
|
|
70
|
+
var GateRuleInvalid = class extends GateError {
|
|
71
|
+
constructor(gateId, reason) {
|
|
72
|
+
super(`gate ${gateId} has an invalid rule: ${reason}`, gateId);
|
|
73
|
+
this.reason = reason;
|
|
74
|
+
}
|
|
75
|
+
reason;
|
|
76
|
+
};
|
|
69
77
|
var GateAuthorizeInvalid = class extends GateError {
|
|
70
78
|
constructor(gateId) {
|
|
71
79
|
super(`authorize for gate ${gateId} must return a decision { allowed: boolean } synchronously`, gateId);
|
|
@@ -149,6 +157,23 @@ function defaultResolverRefusal(resolver) {
|
|
|
149
157
|
if (RESOLVER_CLASSES.includes(resolver.class)) return void 0;
|
|
150
158
|
return `actor class ${resolver.class} may not resolve a gate`;
|
|
151
159
|
}
|
|
160
|
+
function ruleResolverRefusal(record, resolver) {
|
|
161
|
+
if (!record.rule) return void 0;
|
|
162
|
+
if (record.rule.resolvers.includes(resolver.class)) return void 0;
|
|
163
|
+
return `rule ${record.rule.ruleId} does not let ${resolver.class} resolve this gate`;
|
|
164
|
+
}
|
|
165
|
+
function snapshotRule(gateId, raw) {
|
|
166
|
+
if (typeof raw !== "object" || raw === null) throw new GateRuleInvalid(gateId, "the rule is not an object");
|
|
167
|
+
const { table, version, ruleId, resolvers } = raw;
|
|
168
|
+
if (typeof table !== "string" || typeof version !== "string" || typeof ruleId !== "string") {
|
|
169
|
+
throw new GateRuleInvalid(gateId, "the rule's table, version and ruleId must be strings");
|
|
170
|
+
}
|
|
171
|
+
const classes = Array.isArray(resolvers) ? [...resolvers] : [];
|
|
172
|
+
if (classes.length === 0 || !classes.every(isResolverClass)) {
|
|
173
|
+
throw new GateRuleInvalid(gateId, "the rule's resolvers must be a non-empty list of resolver classes");
|
|
174
|
+
}
|
|
175
|
+
return Object.freeze({ table, version, ruleId, resolvers: Object.freeze(classes) });
|
|
176
|
+
}
|
|
152
177
|
function snapshotResolver(gateId, raw) {
|
|
153
178
|
if (typeof raw !== "object" || raw === null) throw new GateResolverRefused(gateId, void 0, "the resolver is not an object");
|
|
154
179
|
const { class: actorClass, id, channel, confirmEvent } = raw;
|
|
@@ -176,6 +201,9 @@ function readDecision(gateId, decision) {
|
|
|
176
201
|
if (allowed) return { allowed: true };
|
|
177
202
|
return { allowed: false, reason: typeof reason === "string" ? reason : "authorize refused the resolver" };
|
|
178
203
|
}
|
|
204
|
+
function isResolverClass(value) {
|
|
205
|
+
return typeof value === "string" && RESOLVER_CLASSES.includes(value);
|
|
206
|
+
}
|
|
179
207
|
function isActorClass(value) {
|
|
180
208
|
return typeof value === "string" && ACTOR_CLASSES.includes(value);
|
|
181
209
|
}
|
|
@@ -190,6 +218,7 @@ var BaseGateStore = class {
|
|
|
190
218
|
authorize;
|
|
191
219
|
create(input) {
|
|
192
220
|
const id = input.id ?? globalThis.crypto.randomUUID();
|
|
221
|
+
const rule = input.rule === void 0 ? void 0 : snapshotRule(id, input.rule);
|
|
193
222
|
if (this.read(id)) throw new GateAlreadyExists(id);
|
|
194
223
|
const record = {
|
|
195
224
|
id,
|
|
@@ -201,7 +230,8 @@ var BaseGateStore = class {
|
|
|
201
230
|
createdAt: this.nowIso(),
|
|
202
231
|
resolvedAt: void 0,
|
|
203
232
|
expiresAt: toIso(input.expiresAt),
|
|
204
|
-
resolvedBy: void 0
|
|
233
|
+
resolvedBy: void 0,
|
|
234
|
+
rule
|
|
205
235
|
};
|
|
206
236
|
this.insert(record);
|
|
207
237
|
return record;
|
|
@@ -211,7 +241,8 @@ var BaseGateStore = class {
|
|
|
211
241
|
return record ? this.lapseIfExpired(record) : void 0;
|
|
212
242
|
}
|
|
213
243
|
resolve(id, payload, resolvedBy) {
|
|
214
|
-
|
|
244
|
+
if (!resolvedBy) throw new GateResolverRefused(id, void 0, "a resolver is required");
|
|
245
|
+
const resolver = snapshotResolver(id, resolvedBy);
|
|
215
246
|
const record = this.requirePending(id);
|
|
216
247
|
this.requireAuthorized(record, resolver);
|
|
217
248
|
if (record.schema) {
|
|
@@ -238,12 +269,13 @@ var BaseGateStore = class {
|
|
|
238
269
|
if (record.status !== "pending") throw new GateAlreadySettled(id, record.status);
|
|
239
270
|
return record;
|
|
240
271
|
}
|
|
241
|
-
/** The default check runs first and `authorize`
|
|
272
|
+
/** The default check runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */
|
|
242
273
|
requireAuthorized(record, resolver) {
|
|
243
|
-
const refusal =
|
|
244
|
-
if (
|
|
274
|
+
const refusal = defaultResolverRefusal(resolver);
|
|
275
|
+
if (refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);
|
|
276
|
+
const ruleRefusal = ruleResolverRefusal(record, resolver);
|
|
277
|
+
if (ruleRefusal) throw new GateResolverRefused(record.id, resolver.class, ruleRefusal);
|
|
245
278
|
if (!this.authorize) return;
|
|
246
|
-
if (!resolver) throw new GateResolverRefused(record.id, void 0, "a resolver is required when authorize is installed");
|
|
247
279
|
const decision = readDecision(record.id, this.authorize(Object.freeze({ ...record }), resolver));
|
|
248
280
|
if (!decision.allowed) throw new GateResolverRefused(record.id, resolver.class, decision.reason);
|
|
249
281
|
}
|
|
@@ -275,10 +307,12 @@ export {
|
|
|
275
307
|
GateAborted,
|
|
276
308
|
GateResolverRefused,
|
|
277
309
|
GateStoreSchemaOutdated,
|
|
310
|
+
GateRuleInvalid,
|
|
278
311
|
GateAuthorizeInvalid,
|
|
279
312
|
checkAgainstJsonSchema,
|
|
280
313
|
defaultResolverRefusal,
|
|
314
|
+
ruleResolverRefusal,
|
|
281
315
|
snapshotResolver,
|
|
282
316
|
BaseGateStore
|
|
283
317
|
};
|
|
284
|
-
//# sourceMappingURL=chunk-
|
|
318
|
+
//# sourceMappingURL=chunk-PMEJTMSE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/json-schema.ts","../src/resolver-policy.ts","../src/base-store.ts"],"sourcesContent":["import type { ActorClass, ResolverClass } from \"@titan-design/authority\";\n\n/** A gate is a row, never a promise: pending work survives the process that opened it. */\nexport type GateStatus = \"pending\" | \"resolved\" | \"cancelled\" | \"expired\";\n\n/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */\nexport type JsonSchema = Record<string, unknown>;\n\nexport interface GateRecord {\n id: string;\n prompt: string;\n schema: JsonSchema | undefined;\n status: GateStatus;\n payload: unknown;\n /** Why the gate was cancelled; unset for every other status. */\n reason: string | undefined;\n /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */\n createdAt: string;\n resolvedAt: string | undefined;\n expiresAt: string | undefined;\n /** Who resolved the gate, as claimed by the caller; unset for gates resolved before stores recorded it. */\n resolvedBy: GateResolver | undefined;\n /** The authority rule that opened the gate; unset for a gate no rule governs. */\n rule: GateRule | undefined;\n}\n\n/** Binds a gate to the authority rule that opened it, so only that rule's resolver classes may answer. */\nexport interface GateRule {\n table: string;\n version: string;\n ruleId: string;\n resolvers: ResolverClass[];\n}\n\n/** A claim about who answered a gate. hitl records it and checks its class; it cannot prove it. */\nexport interface GateResolver {\n class: ActorClass;\n id: string;\n channel: string;\n /** The channel's own id for the confirming event, such as a chat reaction. */\n confirmEvent?: string;\n}\n\nexport type GateAuthorization = { allowed: true } | { allowed: false; reason: string };\n\n/**\n * Runs after the default resolver-class check, so it can refuse a resolver but\n * never admit one the default refused. Must return synchronously.\n */\nexport type GateAuthorize = (gate: Readonly<GateRecord>, resolver: Readonly<GateResolver>) => GateAuthorization;\n\nexport interface GateInput {\n /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */\n id?: string;\n prompt: string;\n schema?: JsonSchema;\n expiresAt?: Date | string;\n rule?: GateRule;\n}\n\n/**\n * Synchronous on purpose: both implementations are local (a Map, and\n * better-sqlite3), and `wait` is the only thing that needs to be async.\n */\nexport interface GateStore {\n create(input: GateInput): GateRecord;\n get(id: string): GateRecord | undefined;\n resolve(id: string, payload: unknown, resolvedBy: GateResolver): GateRecord;\n cancel(id: string, reason: string): GateRecord;\n listPending(): GateRecord[];\n}\n\nexport class GateError extends Error {\n constructor(\n message: string,\n readonly gateId: string,\n ) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class GateNotFound extends GateError {\n constructor(gateId: string) {\n super(`no gate with id ${gateId}`, gateId);\n }\n}\n\nexport class GateAlreadyExists extends GateError {\n constructor(gateId: string) {\n super(`a gate with id ${gateId} already exists`, gateId);\n }\n}\n\nexport class GateAlreadySettled extends GateError {\n constructor(\n gateId: string,\n readonly status: GateStatus,\n ) {\n super(`gate ${gateId} is already ${status}`, gateId);\n }\n}\n\nexport class GateCancelled extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} was cancelled: ${reason}`, gateId);\n }\n}\n\nexport class GateExpired extends GateError {\n constructor(gateId: string) {\n super(`gate ${gateId} expired before anyone resolved it`, gateId);\n }\n}\n\nexport class GatePayloadInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`payload for gate ${gateId} does not match its schema: ${issues.join(\"; \")}`, gateId);\n }\n}\n\nexport class GateAborted extends GateError {\n constructor(gateId: string, reason: string) {\n super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);\n }\n}\n\nexport class GateResolverRefused extends GateError {\n constructor(\n gateId: string,\n readonly actorClass: string | undefined,\n readonly reason: string,\n ) {\n super(`gate ${gateId} refused a resolution by ${actorClass ?? \"an unnamed resolver\"}: ${reason}`, gateId);\n }\n}\n\n/**\n * The store cannot record a resolver or a rule because its table predates the migration that adds the column.\n * `gateId` is empty when the store refuses at construction, before any gate is named.\n */\nexport class GateStoreSchemaOutdated extends GateError {\n constructor(\n gateId: string,\n readonly table: string,\n readonly migration: string,\n ) {\n const message =\n gateId === \"\"\n ? `cannot open a gate store on table ${table}: it needs ${migration}`\n : `gate ${gateId} cannot be recorded: table ${table} needs ${migration}`;\n super(message, gateId);\n }\n}\n\nexport class GateRuleInvalid extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} has an invalid rule: ${reason}`, gateId);\n }\n}\n\nexport class GateAuthorizeInvalid extends GateError {\n constructor(gateId: string) {\n super(`authorize for gate ${gateId} must return a decision { allowed: boolean } synchronously`, gateId);\n }\n}\n","import type { JsonSchema } from \"./types.js\";\n\n/**\n * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`\n * emits. It exists so the process that resolves a gate can reject a bad payload\n * without holding the original zod schema, which by definition lives in another\n * process. The waiter still re-validates with the real zod schema, so this is the\n * boundary check, not the type guarantee.\n */\nexport function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path = \"$\"): string[] {\n const issues: string[] = [];\n collect(schema, value, path, issues);\n return issues;\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, issues: string[]): void {\n if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {\n checkUnion((schema.anyOf ?? schema.oneOf) as JsonSchema[], value, path, issues);\n return;\n }\n if (\"const\" in schema && !Object.is(schema.const, value)) {\n issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);\n return;\n }\n if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {\n issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);\n return;\n }\n if (typeof schema.type !== \"string\") return;\n if (!matchesType(schema.type, value)) {\n issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);\n return;\n }\n if (schema.type === \"object\") checkObject(schema, value as Record<string, unknown>, path, issues);\n if (schema.type === \"array\") checkArray(schema, value as unknown[], path, issues);\n}\n\nfunction checkUnion(branches: JsonSchema[], value: unknown, path: string, issues: string[]): void {\n const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);\n if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);\n}\n\nfunction checkObject(schema: JsonSchema, value: Record<string, unknown>, path: string, issues: string[]): void {\n const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;\n for (const key of (schema.required ?? []) as string[]) {\n if (!(key in value)) issues.push(`${path}.${key}: required`);\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);\n }\n }\n for (const [key, sub] of Object.entries(properties)) {\n if (key in value) collect(sub, value[key], `${path}.${key}`, issues);\n }\n}\n\nfunction checkArray(schema: JsonSchema, value: unknown[], path: string, issues: string[]): void {\n const items = schema.items as JsonSchema | undefined;\n if (!items) return;\n value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));\n}\n\nfunction matchesType(type: string, value: unknown): boolean {\n switch (type) {\n case \"object\":\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n case \"array\":\n return Array.isArray(value);\n case \"integer\":\n return Number.isInteger(value);\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"null\":\n return value === null;\n default:\n return typeof value === type;\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n return Array.isArray(value) ? \"array\" : typeof value;\n}\n","import { ACTOR_CLASSES, RESOLVER_CLASSES, type ActorClass, type ResolverClass } from \"@titan-design/authority\";\nimport {\n GateAuthorizeInvalid,\n GateResolverRefused,\n GateRuleInvalid,\n type GateAuthorization,\n type GateRecord,\n type GateResolver,\n type GateRule,\n} from \"./types.js\";\n\n/**\n * The default refusal: only an owner class may resolve a gate, so an agent or\n * automation never answers its own gate. Every store applies it before `authorize`.\n */\nexport function defaultResolverRefusal(resolver: GateResolver): string | undefined {\n if ((RESOLVER_CLASSES as readonly string[]).includes(resolver.class)) return undefined;\n return `actor class ${resolver.class} may not resolve a gate`;\n}\n\n/** A rule-bound gate admits only its rule's resolver classes. Runs after the default refusal, so it only narrows. */\nexport function ruleResolverRefusal(record: GateRecord, resolver: GateResolver): string | undefined {\n if (!record.rule) return undefined;\n if ((record.rule.resolvers as readonly string[]).includes(resolver.class)) return undefined;\n return `rule ${record.rule.ruleId} does not let ${resolver.class} resolve this gate`;\n}\n\n/** Reads each declared rule field once into a frozen copy, so a caller cannot widen the rule after `create`. */\nexport function snapshotRule(gateId: string, raw: unknown): Readonly<GateRule> {\n if (typeof raw !== \"object\" || raw === null) throw new GateRuleInvalid(gateId, \"the rule is not an object\");\n const { table, version, ruleId, resolvers } = raw as Record<string, unknown>;\n if (typeof table !== \"string\" || typeof version !== \"string\" || typeof ruleId !== \"string\") {\n throw new GateRuleInvalid(gateId, \"the rule's table, version and ruleId must be strings\");\n }\n const classes = Array.isArray(resolvers) ? [...(resolvers as unknown[])] : [];\n if (classes.length === 0 || !classes.every(isResolverClass)) {\n throw new GateRuleInvalid(gateId, \"the rule's resolvers must be a non-empty list of resolver classes\");\n }\n return Object.freeze({ table, version, ruleId, resolvers: Object.freeze(classes) as ResolverClass[] });\n}\n\n/**\n * Reads each declared field exactly once into a frozen plain object, so the\n * checks and the stored row see the same resolver and nothing undeclared is kept.\n */\nexport function snapshotResolver(gateId: string, raw: unknown): Readonly<GateResolver> {\n if (typeof raw !== \"object\" || raw === null) throw new GateResolverRefused(gateId, undefined, \"the resolver is not an object\");\n const { class: actorClass, id, channel, confirmEvent } = raw as Record<string, unknown>;\n if (!isActorClass(actorClass)) {\n throw new GateResolverRefused(gateId, undefined, \"the resolver's class is not a known actor class\");\n }\n if (typeof id !== \"string\" || typeof channel !== \"string\") {\n throw new GateResolverRefused(gateId, actorClass, \"the resolver's id and channel must be strings\");\n }\n if (confirmEvent === undefined) return Object.freeze({ class: actorClass, id, channel });\n if (typeof confirmEvent !== \"string\") {\n throw new GateResolverRefused(gateId, actorClass, \"the resolver's confirmEvent must be a string\");\n }\n return Object.freeze({ class: actorClass, id, channel, confirmEvent });\n}\n\n/** Accepts only a synchronous `{ allowed: boolean }`; a promise is refused because the row cannot wait on it. */\nexport function readDecision(gateId: string, decision: unknown): GateAuthorization {\n if (typeof decision !== \"object\" || decision === null) throw new GateAuthorizeInvalid(gateId);\n const { then, allowed, reason } = decision as Record<string, unknown>;\n if (typeof then === \"function\") {\n (then as (onFulfilled: undefined, onRejected: () => void) => unknown).call(decision, undefined, () => {});\n throw new GateAuthorizeInvalid(gateId);\n }\n if (typeof allowed !== \"boolean\") throw new GateAuthorizeInvalid(gateId);\n if (allowed) return { allowed: true };\n return { allowed: false, reason: typeof reason === \"string\" ? reason : \"authorize refused the resolver\" };\n}\n\nfunction isResolverClass(value: unknown): value is ResolverClass {\n return typeof value === \"string\" && (RESOLVER_CLASSES as readonly string[]).includes(value);\n}\n\nfunction isActorClass(value: unknown): value is ActorClass {\n return typeof value === \"string\" && (ACTOR_CLASSES as readonly string[]).includes(value);\n}\n","import { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport { defaultResolverRefusal, readDecision, ruleResolverRefusal, snapshotResolver, snapshotRule } from \"./resolver-policy.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n GateResolverRefused,\n type GateAuthorize,\n type GateInput,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\n/**\n * Every settle rule lives here so the memory and SQLite stores cannot drift\n * apart. Subclasses only move rows.\n */\nexport abstract class BaseGateStore implements GateStore {\n protected constructor(\n protected readonly clock: () => number,\n private readonly authorize?: GateAuthorize,\n ) {}\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n protected abstract update(record: GateRecord): void;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? globalThis.crypto.randomUUID();\n const rule = input.rule === undefined ? undefined : snapshotRule(id, input.rule);\n if (this.read(id)) throw new GateAlreadyExists(id);\n const record: GateRecord = {\n id,\n prompt: input.prompt,\n schema: input.schema,\n status: \"pending\",\n payload: undefined,\n reason: undefined,\n createdAt: this.nowIso(),\n resolvedAt: undefined,\n expiresAt: toIso(input.expiresAt),\n resolvedBy: undefined,\n rule,\n };\n this.insert(record);\n return record;\n }\n\n get(id: string): GateRecord | undefined {\n const record = this.read(id);\n return record ? this.lapseIfExpired(record) : undefined;\n }\n\n resolve(id: string, payload: unknown, resolvedBy: GateResolver): GateRecord {\n if (!resolvedBy) throw new GateResolverRefused(id, undefined, \"a resolver is required\");\n const resolver = snapshotResolver(id, resolvedBy);\n const record = this.requirePending(id);\n this.requireAuthorized(record, resolver);\n if (record.schema) {\n const issues = checkAgainstJsonSchema(record.schema, payload);\n if (issues.length > 0) throw new GatePayloadInvalid(id, issues);\n }\n const resolved: GateRecord = { ...record, status: \"resolved\", payload, resolvedAt: this.nowIso(), resolvedBy: resolver };\n this.update(resolved);\n return resolved;\n }\n\n cancel(id: string, reason: string): GateRecord {\n const record = this.requirePending(id);\n const cancelled: GateRecord = { ...record, status: \"cancelled\", reason, resolvedAt: this.nowIso() };\n this.update(cancelled);\n return cancelled;\n }\n\n listPending(): GateRecord[] {\n return this.readByStatus(\"pending\")\n .map((record) => this.lapseIfExpired(record))\n .filter((record) => record.status === \"pending\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n private requirePending(id: string): GateRecord {\n const record = this.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"expired\") throw new GateExpired(id);\n if (record.status !== \"pending\") throw new GateAlreadySettled(id, record.status);\n return record;\n }\n\n /** The default check runs first, the gate's rule second and `authorize` last, so each can only narrow who may resolve. */\n private requireAuthorized(record: GateRecord, resolver: Readonly<GateResolver>): void {\n const refusal = defaultResolverRefusal(resolver);\n if (refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);\n const ruleRefusal = ruleResolverRefusal(record, resolver);\n if (ruleRefusal) throw new GateResolverRefused(record.id, resolver.class, ruleRefusal);\n if (!this.authorize) return;\n const decision = readDecision(record.id, this.authorize(Object.freeze({ ...record }), resolver));\n if (!decision.allowed) throw new GateResolverRefused(record.id, resolver.class, decision.reason);\n }\n\n /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */\n private lapseIfExpired(record: GateRecord): GateRecord {\n if (record.status !== \"pending\" || !record.expiresAt) return record;\n if (Date.parse(record.expiresAt) > this.clock()) return record;\n const expired: GateRecord = { ...record, status: \"expired\", resolvedAt: this.nowIso() };\n this.update(expired);\n return expired;\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction toIso(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === \"string\" ? value : value.toISOString();\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodType } from 'zod';
|
|
2
|
-
import { G as GateRecord, a as GateStore, b as GateResolver, B as BaseGateStore, c as GateAuthorize, J as JsonSchema } from './base-store-
|
|
3
|
-
export { d as GateAborted, e as GateAlreadyExists, f as GateAlreadySettled, g as GateAuthorization, h as GateAuthorizeInvalid, i as GateCancelled, j as GateError, k as GateExpired, l as GateInput, m as GateNotFound, n as GatePayloadInvalid, o as GateResolverRefused, p as
|
|
2
|
+
import { G as GateRecord, a as GateStore, b as GateResolver, B as BaseGateStore, c as GateAuthorize, J as JsonSchema } from './base-store-BrVibYRJ.js';
|
|
3
|
+
export { d as GateAborted, e as GateAlreadyExists, f as GateAlreadySettled, g as GateAuthorization, h as GateAuthorizeInvalid, i as GateCancelled, j as GateError, k as GateExpired, l as GateInput, m as GateNotFound, n as GatePayloadInvalid, o as GateResolverRefused, p as GateRule, q as GateRuleInvalid, r as GateStatus, s as GateStoreSchemaOutdated } from './base-store-BrVibYRJ.js';
|
|
4
4
|
import '@titan-design/authority';
|
|
5
5
|
|
|
6
6
|
declare const DEFAULT_POLL_MS = 250;
|
|
@@ -34,7 +34,7 @@ declare function openGate<T = unknown>(store: GateStore, options: OpenGateOption
|
|
|
34
34
|
*/
|
|
35
35
|
declare function waitForGate<T = unknown>(store: GateStore, id: string, options?: WaitOptions<T>): Promise<T>;
|
|
36
36
|
/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */
|
|
37
|
-
declare function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy
|
|
37
|
+
declare function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy: GateResolver): GateRecord;
|
|
38
38
|
declare function cancelGate(store: GateStore, id: string, reason: string): GateRecord;
|
|
39
39
|
|
|
40
40
|
interface MemoryGateStoreOptions {
|
|
@@ -62,6 +62,8 @@ declare class MemoryGateStore extends BaseGateStore {
|
|
|
62
62
|
* automation never answers its own gate. Every store applies it before `authorize`.
|
|
63
63
|
*/
|
|
64
64
|
declare function defaultResolverRefusal(resolver: GateResolver): string | undefined;
|
|
65
|
+
/** A rule-bound gate admits only its rule's resolver classes. Runs after the default refusal, so it only narrows. */
|
|
66
|
+
declare function ruleResolverRefusal(record: GateRecord, resolver: GateResolver): string | undefined;
|
|
65
67
|
/**
|
|
66
68
|
* Reads each declared field exactly once into a frozen plain object, so the
|
|
67
69
|
* checks and the stored row see the same resolver and nothing undeclared is kept.
|
|
@@ -77,4 +79,4 @@ declare function snapshotResolver(gateId: string, raw: unknown): Readonly<GateRe
|
|
|
77
79
|
*/
|
|
78
80
|
declare function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path?: string): string[];
|
|
79
81
|
|
|
80
|
-
export { BaseGateStore, DEFAULT_POLL_MS, GateAuthorize, type GateHandle, GateRecord, GateResolver, GateStore, JsonSchema, MemoryGateStore, type MemoryGateStoreOptions, type OpenGateOptions, type WaitOptions, cancelGate, checkAgainstJsonSchema, defaultResolverRefusal, openGate, resolveGate, snapshotResolver, waitForGate };
|
|
82
|
+
export { BaseGateStore, DEFAULT_POLL_MS, GateAuthorize, type GateHandle, GateRecord, GateResolver, GateStore, JsonSchema, MemoryGateStore, type MemoryGateStoreOptions, type OpenGateOptions, type WaitOptions, cancelGate, checkAgainstJsonSchema, defaultResolverRefusal, openGate, resolveGate, ruleResolverRefusal, snapshotResolver, waitForGate };
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
GateNotFound,
|
|
11
11
|
GatePayloadInvalid,
|
|
12
12
|
GateResolverRefused,
|
|
13
|
+
GateRuleInvalid,
|
|
13
14
|
GateStoreSchemaOutdated,
|
|
14
15
|
checkAgainstJsonSchema,
|
|
15
16
|
defaultResolverRefusal,
|
|
17
|
+
ruleResolverRefusal,
|
|
16
18
|
snapshotResolver
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-PMEJTMSE.js";
|
|
18
20
|
|
|
19
21
|
// src/gate.ts
|
|
20
22
|
import { toJSONSchema } from "zod";
|
|
@@ -98,7 +100,11 @@ var MemoryGateStore = class extends BaseGateStore {
|
|
|
98
100
|
}
|
|
99
101
|
};
|
|
100
102
|
function copy(record) {
|
|
101
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
...record,
|
|
105
|
+
resolvedBy: record.resolvedBy && { ...record.resolvedBy },
|
|
106
|
+
rule: record.rule && { ...record.rule, resolvers: [...record.rule.resolvers] }
|
|
107
|
+
};
|
|
102
108
|
}
|
|
103
109
|
export {
|
|
104
110
|
BaseGateStore,
|
|
@@ -113,6 +119,7 @@ export {
|
|
|
113
119
|
GateNotFound,
|
|
114
120
|
GatePayloadInvalid,
|
|
115
121
|
GateResolverRefused,
|
|
122
|
+
GateRuleInvalid,
|
|
116
123
|
GateStoreSchemaOutdated,
|
|
117
124
|
MemoryGateStore,
|
|
118
125
|
cancelGate,
|
|
@@ -120,6 +127,7 @@ export {
|
|
|
120
127
|
defaultResolverRefusal,
|
|
121
128
|
openGate,
|
|
122
129
|
resolveGate,
|
|
130
|
+
ruleResolverRefusal,
|
|
123
131
|
snapshotResolver,
|
|
124
132
|
waitForGate
|
|
125
133
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/gate.ts","../src/memory-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy
|
|
1
|
+
{"version":3,"sources":["../src/gate.ts","../src/memory-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy: GateResolver): GateRecord {\n return store.resolve(id, payload, resolvedBy);\n}\n\nexport function cancelGate(store: GateStore, id: string, reason: string): GateRecord {\n return store.cancel(id, reason);\n}\n\nfunction validate<T>(record: GateRecord, schema: ZodType<T> | undefined): T {\n if (!schema) return record.payload as T;\n const parsed = schema.safeParse(record.payload);\n if (parsed.success) return parsed.data;\n throw new GatePayloadInvalid(\n record.id,\n parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"$\"}: ${issue.message}`),\n );\n}\n\nfunction throwIfAborted(id: string, signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? \"aborted\"));\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms);\n signal?.addEventListener(\"abort\", finish, { once: true });\n function finish(): void {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", finish);\n resolve();\n }\n });\n}\n","import { BaseGateStore } from \"./base-store.js\";\nimport type { GateAuthorize, GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\n now?: () => number;\n /** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */\n authorize?: GateAuthorize;\n}\n\n/**\n * In-process gates. Right for tests and for a single-process product that only\n * needs the pause, not the durability; anything that must survive a restart\n * wants `SqliteGateStore`.\n */\nexport class MemoryGateStore extends BaseGateStore {\n private readonly rows = new Map<string, GateRecord>();\n\n constructor(options: MemoryGateStoreOptions = {}) {\n super(options.now ?? Date.now, options.authorize);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id, copy(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? copy(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.rows.set(record.id, copy(record));\n }\n\n protected readByStatus(status: GateRecord[\"status\"]): GateRecord[] {\n return [...this.rows.values()].filter((row) => row.status === status).map(copy);\n }\n}\n\n/** `resolvedBy` and `rule` are nested, so a shallow spread would let a caller rewrite what the store holds. */\nfunction copy(record: GateRecord): GateRecord {\n return {\n ...record,\n resolvedBy: record.resolvedBy && { ...record.resolvedBy },\n rule: record.rule && { ...record.rule, resolvers: [...record.rule.resolvers] },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Db, Migration } from '@titan-design/store-sqlite';
|
|
2
|
-
import { B as BaseGateStore, c as GateAuthorize, G as GateRecord,
|
|
2
|
+
import { B as BaseGateStore, c as GateAuthorize, G as GateRecord, r as GateStatus } from './base-store-BrVibYRJ.js';
|
|
3
3
|
import '@titan-design/authority';
|
|
4
4
|
|
|
5
5
|
declare const DEFAULT_GATE_TABLE = "hitl_gate";
|
|
@@ -16,9 +16,11 @@ declare function resolverRequiredTriggerDdl(name?: string): string;
|
|
|
16
16
|
* gates resolved before it keep an unknown resolver rather than a guessed one.
|
|
17
17
|
*/
|
|
18
18
|
declare function gateResolverMigration(version: number, name?: string): Migration;
|
|
19
|
+
/** Adds `rule` and its trigger. Idempotent and backfill-free: gates opened before it carry no rule. */
|
|
20
|
+
declare function gateRuleMigration(version: number, name?: string): Migration;
|
|
19
21
|
interface SqliteGateStoreOptions {
|
|
20
22
|
table?: string;
|
|
21
|
-
/** Run `gateMigration` on construction. Off when the product owns its migration list. */
|
|
23
|
+
/** Run `gateMigration`, `gateResolverMigration` and `gateRuleMigration` on construction. Off when the product owns its migration list. */
|
|
22
24
|
migrate?: boolean;
|
|
23
25
|
now?: () => number;
|
|
24
26
|
/** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */
|
|
@@ -32,16 +34,15 @@ interface SqliteGateStoreOptions {
|
|
|
32
34
|
declare class SqliteGateStore extends BaseGateStore {
|
|
33
35
|
private readonly db;
|
|
34
36
|
private readonly table;
|
|
35
|
-
private
|
|
37
|
+
private ruleColumnSeen;
|
|
36
38
|
constructor(db: Db, options?: SqliteGateStoreOptions);
|
|
37
39
|
protected insert(record: GateRecord): void;
|
|
40
|
+
/** Refuses rather than dropping the rule when the table has no column to hold it. */
|
|
41
|
+
private insertWithRule;
|
|
38
42
|
protected read(id: string): GateRecord | undefined;
|
|
39
43
|
protected update(record: GateRecord): void;
|
|
40
|
-
|
|
41
|
-
private updateWithResolver;
|
|
42
|
-
/** Only a positive probe is cached, so a migration run after construction is still noticed. */
|
|
43
|
-
private resolverColumnPresent;
|
|
44
|
+
private ruleColumnPresent;
|
|
44
45
|
protected readByStatus(status: GateStatus): GateRecord[];
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export { DEFAULT_GATE_TABLE, SqliteGateStore, type SqliteGateStoreOptions, gateMigration, gateResolverMigration, gateTableDdl, resolverRequiredTriggerDdl };
|
|
48
|
+
export { DEFAULT_GATE_TABLE, SqliteGateStore, type SqliteGateStoreOptions, gateMigration, gateResolverMigration, gateRuleMigration, gateTableDdl, resolverRequiredTriggerDdl };
|
package/dist/sqlite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BaseGateStore,
|
|
3
3
|
GateStoreSchemaOutdated
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PMEJTMSE.js";
|
|
5
5
|
|
|
6
6
|
// src/sqlite-store.ts
|
|
7
7
|
import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
|
|
@@ -48,72 +48,124 @@ function gateResolverMigration(version, name = DEFAULT_GATE_TABLE) {
|
|
|
48
48
|
up: (db) => {
|
|
49
49
|
if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);
|
|
50
50
|
db.exec(resolverRequiredTriggerDdl(name));
|
|
51
|
+
if (hasColumn(db, name, "rule")) db.exec(ruleTriggerDdl(db, name));
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
function hasResolverColumn(db, table) {
|
|
55
|
-
return db
|
|
56
|
+
return hasColumn(db, table, "resolved_by");
|
|
57
|
+
}
|
|
58
|
+
function hasColumn(db, table, column) {
|
|
59
|
+
return db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = ?").get(table, column) !== void 0;
|
|
60
|
+
}
|
|
61
|
+
var CANONICAL_STATUSES = "'pending', 'resolved', 'cancelled', 'expired'";
|
|
62
|
+
function ruleTriggerDdl(db, name) {
|
|
63
|
+
const hasResolver = hasResolverColumn(db, name);
|
|
64
|
+
const update = ruleTrigger(name, "UPDATE", "OLD.rule", hasResolver, "NEW.rule IS NOT OLD.rule");
|
|
65
|
+
const insert = ruleTrigger(name, "INSERT", "NEW.rule", hasResolver, "0");
|
|
66
|
+
return `${update}
|
|
67
|
+
${insert}
|
|
68
|
+
${replaceGuard(name)}`;
|
|
69
|
+
}
|
|
70
|
+
function replaceGuard(name) {
|
|
71
|
+
const trigger = quoteIdent(`${name}_rule_replace`);
|
|
72
|
+
return `
|
|
73
|
+
DROP TRIGGER IF EXISTS ${trigger};
|
|
74
|
+
CREATE TRIGGER ${trigger}
|
|
75
|
+
BEFORE INSERT ON ${quoteIdent(name)}
|
|
76
|
+
FOR EACH ROW WHEN EXISTS (
|
|
77
|
+
SELECT 1 FROM ${quoteIdent(name)} WHERE id = NEW.id AND status = 'pending' AND rule IS NOT NULL AND rule IS NOT NEW.rule
|
|
78
|
+
)
|
|
79
|
+
BEGIN
|
|
80
|
+
SELECT RAISE(ABORT, 'hitl: a pending rule-bound gate cannot be replaced');
|
|
81
|
+
END;
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
function ruleTrigger(name, event, rule, hasResolver, ruleChanged) {
|
|
85
|
+
const trigger = quoteIdent(event === "UPDATE" ? `${name}_rule_resolver` : `${name}_rule_resolver_insert`);
|
|
86
|
+
const outsideRule = hasResolver ? `COALESCE(json_extract(NEW.resolved_by, '$.class'), '') NOT IN (SELECT value FROM json_each(${rule}, '$.resolvers'))` : "1";
|
|
87
|
+
return `
|
|
88
|
+
DROP TRIGGER IF EXISTS ${trigger};
|
|
89
|
+
CREATE TRIGGER ${trigger}
|
|
90
|
+
BEFORE ${event} ON ${quoteIdent(name)}
|
|
91
|
+
FOR EACH ROW WHEN ${rule} IS NOT NULL OR ${ruleChanged}
|
|
92
|
+
BEGIN
|
|
93
|
+
SELECT RAISE(ABORT, 'hitl: status outside the canonical set') WHERE ${rule} IS NOT NULL AND NEW.status NOT IN (${CANONICAL_STATUSES});
|
|
94
|
+
SELECT RAISE(ABORT, 'hitl: resolver outside the gate rule')
|
|
95
|
+
WHERE ${ruleChanged} OR (NEW.status = 'resolved' AND ${outsideRule});
|
|
96
|
+
END;
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
function gateRuleMigration(version, name = DEFAULT_GATE_TABLE) {
|
|
100
|
+
return {
|
|
101
|
+
version,
|
|
102
|
+
name: `hitl:rule:${name}`,
|
|
103
|
+
up: (db) => {
|
|
104
|
+
if (!hasColumn(db, name, "rule")) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN rule TEXT`);
|
|
105
|
+
db.exec(ruleTriggerDdl(db, name));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
56
108
|
}
|
|
57
109
|
var SqliteGateStore = class extends BaseGateStore {
|
|
58
110
|
constructor(db, options = {}) {
|
|
59
111
|
super(options.now ?? Date.now, options.authorize);
|
|
60
112
|
this.db = db;
|
|
61
113
|
this.table = options.table ?? DEFAULT_GATE_TABLE;
|
|
62
|
-
if (options.migrate ?? true) runMigrations(db,
|
|
114
|
+
if (options.migrate ?? true) runMigrations(db, defaultMigrations(this.table));
|
|
115
|
+
const missing = missingMigration(db, this.table);
|
|
116
|
+
if (missing) throw new GateStoreSchemaOutdated("", this.table, missing);
|
|
63
117
|
}
|
|
64
118
|
db;
|
|
65
119
|
table;
|
|
66
|
-
|
|
120
|
+
ruleColumnSeen = false;
|
|
67
121
|
insert(record) {
|
|
122
|
+
if (record.rule) {
|
|
123
|
+
this.insertWithRule(record, record.rule);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
68
126
|
this.db.prepare(
|
|
69
127
|
`INSERT INTO ${quoteIdent(this.table)}
|
|
70
128
|
(id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)
|
|
71
129
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
72
130
|
).run(...columns(record));
|
|
73
131
|
}
|
|
132
|
+
/** Refuses rather than dropping the rule when the table has no column to hold it. */
|
|
133
|
+
insertWithRule(record, rule) {
|
|
134
|
+
if (!this.ruleColumnPresent()) throw new GateStoreSchemaOutdated(record.id, this.table, "gateRuleMigration");
|
|
135
|
+
this.db.prepare(
|
|
136
|
+
`INSERT INTO ${quoteIdent(this.table)}
|
|
137
|
+
(id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at, rule)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
139
|
+
).run(...columns(record), JSON.stringify(rule));
|
|
140
|
+
}
|
|
74
141
|
read(id) {
|
|
75
142
|
const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id);
|
|
76
143
|
return row ? toRecord(row) : void 0;
|
|
77
144
|
}
|
|
78
145
|
update(record) {
|
|
79
|
-
|
|
80
|
-
this.updateWithResolver(record, record.resolvedBy);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
this.db.prepare(
|
|
84
|
-
`UPDATE ${quoteIdent(this.table)}
|
|
85
|
-
SET status = ?, payload = ?, reason = ?, resolved_at = ?
|
|
86
|
-
WHERE id = ?`
|
|
87
|
-
).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);
|
|
88
|
-
}
|
|
89
|
-
/** Refuses rather than dropping the resolver when the table has no column to hold it. */
|
|
90
|
-
updateWithResolver(record, resolvedBy) {
|
|
91
|
-
if (!this.resolverColumnPresent()) {
|
|
92
|
-
throw new GateStoreSchemaOutdated(record.id, this.table, "gateResolverMigration");
|
|
93
|
-
}
|
|
146
|
+
const resolvedBy = record.resolvedBy ? JSON.stringify(record.resolvedBy) : null;
|
|
94
147
|
this.db.prepare(
|
|
95
148
|
`UPDATE ${quoteIdent(this.table)}
|
|
96
149
|
SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?
|
|
97
150
|
WHERE id = ?`
|
|
98
|
-
).run(
|
|
99
|
-
record.status,
|
|
100
|
-
toJson(record.payload),
|
|
101
|
-
record.reason ?? null,
|
|
102
|
-
record.resolvedAt ?? null,
|
|
103
|
-
JSON.stringify(resolvedBy),
|
|
104
|
-
record.id
|
|
105
|
-
);
|
|
151
|
+
).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, resolvedBy, record.id);
|
|
106
152
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return this.resolverColumnSeen;
|
|
153
|
+
ruleColumnPresent() {
|
|
154
|
+
if (!this.ruleColumnSeen) this.ruleColumnSeen = hasColumn(this.db, this.table, "rule");
|
|
155
|
+
return this.ruleColumnSeen;
|
|
111
156
|
}
|
|
112
157
|
readByStatus(status) {
|
|
113
158
|
const rows = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`).all(status);
|
|
114
159
|
return rows.map(toRecord);
|
|
115
160
|
}
|
|
116
161
|
};
|
|
162
|
+
function missingMigration(db, table) {
|
|
163
|
+
if (!hasColumn(db, table, "id")) return "gateMigration";
|
|
164
|
+
return hasResolverColumn(db, table) ? void 0 : "gateResolverMigration";
|
|
165
|
+
}
|
|
166
|
+
function defaultMigrations(table) {
|
|
167
|
+
return [gateMigration(1, table), gateResolverMigration(2, table), gateRuleMigration(3, table)];
|
|
168
|
+
}
|
|
117
169
|
function columns(record) {
|
|
118
170
|
return [
|
|
119
171
|
record.id,
|
|
@@ -141,7 +193,8 @@ function toRecord(row) {
|
|
|
141
193
|
createdAt: row.created_at,
|
|
142
194
|
resolvedAt: row.resolved_at ?? void 0,
|
|
143
195
|
expiresAt: row.expires_at ?? void 0,
|
|
144
|
-
resolvedBy: row.resolved_by ? JSON.parse(row.resolved_by) : void 0
|
|
196
|
+
resolvedBy: row.resolved_by ? JSON.parse(row.resolved_by) : void 0,
|
|
197
|
+
rule: row.rule ? JSON.parse(row.rule) : void 0
|
|
145
198
|
};
|
|
146
199
|
}
|
|
147
200
|
export {
|
|
@@ -149,6 +202,7 @@ export {
|
|
|
149
202
|
SqliteGateStore,
|
|
150
203
|
gateMigration,
|
|
151
204
|
gateResolverMigration,
|
|
205
|
+
gateRuleMigration,
|
|
152
206
|
gateTableDdl,
|
|
153
207
|
resolverRequiredTriggerDdl
|
|
154
208
|
};
|
package/dist/sqlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sqlite-store.ts"],"sourcesContent":["import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport {\n GateStoreSchemaOutdated,\n type GateAuthorize,\n type GateRecord,\n type GateResolver,\n type GateStatus,\n type JsonSchema,\n} from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\n/**\n * The triggers that refuse a resolved row naming no resolver, on update and on\n * insert, so a writer that predates `resolved_by` fails loudly instead of resolving anonymously.\n */\nexport function resolverRequiredTriggerDdl(name: string = DEFAULT_GATE_TABLE): string {\n return ([\"UPDATE\", \"INSERT\"] as const).map((event) => resolverTrigger(name, event)).join(\"\\n\");\n}\n\nfunction resolverTrigger(name: string, event: \"UPDATE\" | \"INSERT\"): string {\n const suffix = event === \"UPDATE\" ? \"resolver_required\" : \"resolver_required_insert\";\n return `\n CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${name}_${suffix}`)}\n BEFORE ${event} ON ${quoteIdent(name)}\n FOR EACH ROW WHEN NEW.status = 'resolved' AND NEW.resolved_by IS NULL\n BEGIN\n SELECT RAISE(ABORT, 'hitl: resolvedBy required');\n END;\n `;\n}\n\n/**\n * Adds `resolved_by` and the resolver trigger. Idempotent and backfill-free:\n * gates resolved before it keep an unknown resolver rather than a guessed one.\n */\nexport function gateResolverMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:resolver:${name}`,\n up: (db) => {\n if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);\n db.exec(resolverRequiredTriggerDdl(name));\n },\n };\n}\n\nfunction hasResolverColumn(db: Db, table: string): boolean {\n return db.prepare(\"SELECT 1 FROM pragma_table_info(?) WHERE name = 'resolved_by'\").get(table) !== undefined;\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n /** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */\n authorize?: GateAuthorize;\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n /** Absent entirely on a table that has not run `gateResolverMigration`. */\n resolved_by?: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n private resolverColumnSeen = false;\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now, options.authorize);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);\n }\n\n protected insert(record: GateRecord): void {\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n if (record.resolvedBy) {\n this.updateWithResolver(record, record.resolvedBy);\n return;\n }\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);\n }\n\n /** Refuses rather than dropping the resolver when the table has no column to hold it. */\n private updateWithResolver(record: GateRecord, resolvedBy: GateResolver): void {\n if (!this.resolverColumnPresent()) {\n throw new GateStoreSchemaOutdated(record.id, this.table, \"gateResolverMigration\");\n }\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?\n WHERE id = ?`,\n )\n .run(\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.resolvedAt ?? null,\n JSON.stringify(resolvedBy),\n record.id,\n );\n }\n\n /** Only a positive probe is cached, so a migration run after construction is still noticed. */\n private resolverColumnPresent(): boolean {\n if (!this.resolverColumnSeen) this.resolverColumnSeen = hasResolverColumn(this.db, this.table);\n return this.resolverColumnSeen;\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n resolvedBy: row.resolved_by ? (JSON.parse(row.resolved_by) as GateResolver) : undefined,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/sqlite-store.ts"],"sourcesContent":["import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport {\n GateStoreSchemaOutdated,\n type GateAuthorize,\n type GateRecord,\n type GateResolver,\n type GateRule,\n type GateStatus,\n type JsonSchema,\n} from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\n/**\n * The triggers that refuse a resolved row naming no resolver, on update and on\n * insert, so a writer that predates `resolved_by` fails loudly instead of resolving anonymously.\n */\nexport function resolverRequiredTriggerDdl(name: string = DEFAULT_GATE_TABLE): string {\n return ([\"UPDATE\", \"INSERT\"] as const).map((event) => resolverTrigger(name, event)).join(\"\\n\");\n}\n\nfunction resolverTrigger(name: string, event: \"UPDATE\" | \"INSERT\"): string {\n const suffix = event === \"UPDATE\" ? \"resolver_required\" : \"resolver_required_insert\";\n return `\n CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${name}_${suffix}`)}\n BEFORE ${event} ON ${quoteIdent(name)}\n FOR EACH ROW WHEN NEW.status = 'resolved' AND NEW.resolved_by IS NULL\n BEGIN\n SELECT RAISE(ABORT, 'hitl: resolvedBy required');\n END;\n `;\n}\n\n/**\n * Adds `resolved_by` and the resolver trigger. Idempotent and backfill-free:\n * gates resolved before it keep an unknown resolver rather than a guessed one.\n */\nexport function gateResolverMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:resolver:${name}`,\n up: (db) => {\n if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);\n db.exec(resolverRequiredTriggerDdl(name));\n if (hasColumn(db, name, \"rule\")) db.exec(ruleTriggerDdl(db, name));\n },\n };\n}\n\nfunction hasResolverColumn(db: Db, table: string): boolean {\n return hasColumn(db, table, \"resolved_by\");\n}\n\nfunction hasColumn(db: Db, table: string, column: string): boolean {\n return db.prepare(\"SELECT 1 FROM pragma_table_info(?) WHERE name = ?\").get(table, column) !== undefined;\n}\n\nconst CANONICAL_STATUSES = \"'pending', 'resolved', 'cancelled', 'expired'\";\n\n/**\n * The triggers that refuse a resolve by a class outside the row's rule, and any\n * change to the rule, so a writer that predates `rule` cannot widen it. The\n * INSERT twin closes REPLACE INTO and DELETE then INSERT, which an UPDATE trigger never sees.\n * A rule-bound row also refuses a status outside the canonical set, so `RESOLVED` cannot slip past the `resolved` test.\n * Without `resolved_by` no resolver can be recorded, so they refuse every resolve\n * of a rule-bound row; whichever migration runs second installs the class-aware form.\n */\nfunction ruleTriggerDdl(db: Db, name: string): string {\n const hasResolver = hasResolverColumn(db, name);\n const update = ruleTrigger(name, \"UPDATE\", \"OLD.rule\", hasResolver, \"NEW.rule IS NOT OLD.rule\");\n const insert = ruleTrigger(name, \"INSERT\", \"NEW.rule\", hasResolver, \"0\");\n return `${update}\\n${insert}\\n${replaceGuard(name)}`;\n}\n\n/** REPLACE deletes the old row before any delete trigger fires without recursive_triggers, so refuse it up front. */\nfunction replaceGuard(name: string): string {\n const trigger = quoteIdent(`${name}_rule_replace`);\n return `\n DROP TRIGGER IF EXISTS ${trigger};\n CREATE TRIGGER ${trigger}\n BEFORE INSERT ON ${quoteIdent(name)}\n FOR EACH ROW WHEN EXISTS (\n SELECT 1 FROM ${quoteIdent(name)} WHERE id = NEW.id AND status = 'pending' AND rule IS NOT NULL AND rule IS NOT NEW.rule\n )\n BEGIN\n SELECT RAISE(ABORT, 'hitl: a pending rule-bound gate cannot be replaced');\n END;\n `;\n}\n\nfunction ruleTrigger(name: string, event: \"UPDATE\" | \"INSERT\", rule: string, hasResolver: boolean, ruleChanged: string): string {\n const trigger = quoteIdent(event === \"UPDATE\" ? `${name}_rule_resolver` : `${name}_rule_resolver_insert`);\n const outsideRule = hasResolver\n ? `COALESCE(json_extract(NEW.resolved_by, '$.class'), '') NOT IN (SELECT value FROM json_each(${rule}, '$.resolvers'))`\n : \"1\";\n return `\n DROP TRIGGER IF EXISTS ${trigger};\n CREATE TRIGGER ${trigger}\n BEFORE ${event} ON ${quoteIdent(name)}\n FOR EACH ROW WHEN ${rule} IS NOT NULL OR ${ruleChanged}\n BEGIN\n SELECT RAISE(ABORT, 'hitl: status outside the canonical set') WHERE ${rule} IS NOT NULL AND NEW.status NOT IN (${CANONICAL_STATUSES});\n SELECT RAISE(ABORT, 'hitl: resolver outside the gate rule')\n WHERE ${ruleChanged} OR (NEW.status = 'resolved' AND ${outsideRule});\n END;\n `;\n}\n\n/** Adds `rule` and its trigger. Idempotent and backfill-free: gates opened before it carry no rule. */\nexport function gateRuleMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return {\n version,\n name: `hitl:rule:${name}`,\n up: (db) => {\n if (!hasColumn(db, name, \"rule\")) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN rule TEXT`);\n db.exec(ruleTriggerDdl(db, name));\n },\n };\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration`, `gateResolverMigration` and `gateRuleMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n /** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */\n authorize?: GateAuthorize;\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n resolved_by: string | null;\n /** Absent entirely on a table that has not run `gateRuleMigration`. */\n rule?: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n private ruleColumnSeen = false;\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now, options.authorize);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, defaultMigrations(this.table));\n const missing = missingMigration(db, this.table);\n if (missing) throw new GateStoreSchemaOutdated(\"\", this.table, missing);\n }\n\n protected insert(record: GateRecord): void {\n if (record.rule) {\n this.insertWithRule(record, record.rule);\n return;\n }\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record));\n }\n\n /** Refuses rather than dropping the rule when the table has no column to hold it. */\n private insertWithRule(record: GateRecord, rule: GateRule): void {\n if (!this.ruleColumnPresent()) throw new GateStoreSchemaOutdated(record.id, this.table, \"gateRuleMigration\");\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at, rule)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record), JSON.stringify(rule));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n const resolvedBy = record.resolvedBy ? JSON.stringify(record.resolvedBy) : null;\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, resolvedBy, record.id);\n }\n\n private ruleColumnPresent(): boolean {\n if (!this.ruleColumnSeen) this.ruleColumnSeen = hasColumn(this.db, this.table, \"rule\");\n return this.ruleColumnSeen;\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\n/** Names the earliest migration the table lacks, so the error points at the step to add. */\nfunction missingMigration(db: Db, table: string): string | undefined {\n if (!hasColumn(db, table, \"id\")) return \"gateMigration\";\n return hasResolverColumn(db, table) ? undefined : \"gateResolverMigration\";\n}\n\nfunction defaultMigrations(table: string): Migration[] {\n return [gateMigration(1, table), gateResolverMigration(2, table), gateRuleMigration(3, table)];\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n resolvedBy: row.resolved_by ? (JSON.parse(row.resolved_by) as GateResolver) : undefined,\n rule: row.rule ? (JSON.parse(row.rule) as GateRule) : undefined,\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/hitl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Human-in-the-loop gate()/resolve() primitive",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@titan-design/authority": "^0.2.
|
|
38
|
+
"@titan-design/authority": "^0.2.1",
|
|
39
39
|
"@titan-design/store-sqlite": "0.3.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/json-schema.ts","../src/resolver-policy.ts","../src/base-store.ts"],"sourcesContent":["import type { ActorClass } from \"@titan-design/authority\";\n\n/** A gate is a row, never a promise: pending work survives the process that opened it. */\nexport type GateStatus = \"pending\" | \"resolved\" | \"cancelled\" | \"expired\";\n\n/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */\nexport type JsonSchema = Record<string, unknown>;\n\nexport interface GateRecord {\n id: string;\n prompt: string;\n schema: JsonSchema | undefined;\n status: GateStatus;\n payload: unknown;\n /** Why the gate was cancelled; unset for every other status. */\n reason: string | undefined;\n /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */\n createdAt: string;\n resolvedAt: string | undefined;\n expiresAt: string | undefined;\n /** Who resolved the gate, as claimed by the caller; unset for gates resolved before stores recorded it. */\n resolvedBy: GateResolver | undefined;\n}\n\n/** A claim about who answered a gate. hitl records it and checks its class; it cannot prove it. */\nexport interface GateResolver {\n class: ActorClass;\n id: string;\n channel: string;\n /** The channel's own id for the confirming event, such as a chat reaction. */\n confirmEvent?: string;\n}\n\nexport type GateAuthorization = { allowed: true } | { allowed: false; reason: string };\n\n/**\n * Runs after the default resolver-class check, so it can refuse a resolver but\n * never admit one the default refused. Must return synchronously.\n */\nexport type GateAuthorize = (gate: Readonly<GateRecord>, resolver: Readonly<GateResolver>) => GateAuthorization;\n\nexport interface GateInput {\n /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */\n id?: string;\n prompt: string;\n schema?: JsonSchema;\n expiresAt?: Date | string;\n}\n\n/**\n * Synchronous on purpose: both implementations are local (a Map, and\n * better-sqlite3), and `wait` is the only thing that needs to be async.\n */\nexport interface GateStore {\n create(input: GateInput): GateRecord;\n get(id: string): GateRecord | undefined;\n resolve(id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord;\n cancel(id: string, reason: string): GateRecord;\n listPending(): GateRecord[];\n}\n\nexport class GateError extends Error {\n constructor(\n message: string,\n readonly gateId: string,\n ) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class GateNotFound extends GateError {\n constructor(gateId: string) {\n super(`no gate with id ${gateId}`, gateId);\n }\n}\n\nexport class GateAlreadyExists extends GateError {\n constructor(gateId: string) {\n super(`a gate with id ${gateId} already exists`, gateId);\n }\n}\n\nexport class GateAlreadySettled extends GateError {\n constructor(\n gateId: string,\n readonly status: GateStatus,\n ) {\n super(`gate ${gateId} is already ${status}`, gateId);\n }\n}\n\nexport class GateCancelled extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} was cancelled: ${reason}`, gateId);\n }\n}\n\nexport class GateExpired extends GateError {\n constructor(gateId: string) {\n super(`gate ${gateId} expired before anyone resolved it`, gateId);\n }\n}\n\nexport class GatePayloadInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`payload for gate ${gateId} does not match its schema: ${issues.join(\"; \")}`, gateId);\n }\n}\n\nexport class GateAborted extends GateError {\n constructor(gateId: string, reason: string) {\n super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);\n }\n}\n\nexport class GateResolverRefused extends GateError {\n constructor(\n gateId: string,\n readonly actorClass: string | undefined,\n readonly reason: string,\n ) {\n super(`gate ${gateId} refused a resolution by ${actorClass ?? \"an unnamed resolver\"}: ${reason}`, gateId);\n }\n}\n\n/** The store cannot record a resolver because its table predates the resolver migration. */\nexport class GateStoreSchemaOutdated extends GateError {\n constructor(\n gateId: string,\n readonly table: string,\n readonly migration: string,\n ) {\n super(`gate ${gateId} cannot record its resolver: table ${table} needs ${migration}`, gateId);\n }\n}\n\nexport class GateAuthorizeInvalid extends GateError {\n constructor(gateId: string) {\n super(`authorize for gate ${gateId} must return a decision { allowed: boolean } synchronously`, gateId);\n }\n}\n","import type { JsonSchema } from \"./types.js\";\n\n/**\n * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`\n * emits. It exists so the process that resolves a gate can reject a bad payload\n * without holding the original zod schema, which by definition lives in another\n * process. The waiter still re-validates with the real zod schema, so this is the\n * boundary check, not the type guarantee.\n */\nexport function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path = \"$\"): string[] {\n const issues: string[] = [];\n collect(schema, value, path, issues);\n return issues;\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, issues: string[]): void {\n if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {\n checkUnion((schema.anyOf ?? schema.oneOf) as JsonSchema[], value, path, issues);\n return;\n }\n if (\"const\" in schema && !Object.is(schema.const, value)) {\n issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);\n return;\n }\n if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {\n issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);\n return;\n }\n if (typeof schema.type !== \"string\") return;\n if (!matchesType(schema.type, value)) {\n issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);\n return;\n }\n if (schema.type === \"object\") checkObject(schema, value as Record<string, unknown>, path, issues);\n if (schema.type === \"array\") checkArray(schema, value as unknown[], path, issues);\n}\n\nfunction checkUnion(branches: JsonSchema[], value: unknown, path: string, issues: string[]): void {\n const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);\n if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);\n}\n\nfunction checkObject(schema: JsonSchema, value: Record<string, unknown>, path: string, issues: string[]): void {\n const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;\n for (const key of (schema.required ?? []) as string[]) {\n if (!(key in value)) issues.push(`${path}.${key}: required`);\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);\n }\n }\n for (const [key, sub] of Object.entries(properties)) {\n if (key in value) collect(sub, value[key], `${path}.${key}`, issues);\n }\n}\n\nfunction checkArray(schema: JsonSchema, value: unknown[], path: string, issues: string[]): void {\n const items = schema.items as JsonSchema | undefined;\n if (!items) return;\n value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));\n}\n\nfunction matchesType(type: string, value: unknown): boolean {\n switch (type) {\n case \"object\":\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n case \"array\":\n return Array.isArray(value);\n case \"integer\":\n return Number.isInteger(value);\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"null\":\n return value === null;\n default:\n return typeof value === type;\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n return Array.isArray(value) ? \"array\" : typeof value;\n}\n","import { ACTOR_CLASSES, RESOLVER_CLASSES, type ActorClass } from \"@titan-design/authority\";\nimport { GateAuthorizeInvalid, GateResolverRefused, type GateAuthorization, type GateResolver } from \"./types.js\";\n\n/**\n * The default refusal: only an owner class may resolve a gate, so an agent or\n * automation never answers its own gate. Every store applies it before `authorize`.\n */\nexport function defaultResolverRefusal(resolver: GateResolver): string | undefined {\n if ((RESOLVER_CLASSES as readonly string[]).includes(resolver.class)) return undefined;\n return `actor class ${resolver.class} may not resolve a gate`;\n}\n\n/**\n * Reads each declared field exactly once into a frozen plain object, so the\n * checks and the stored row see the same resolver and nothing undeclared is kept.\n */\nexport function snapshotResolver(gateId: string, raw: unknown): Readonly<GateResolver> {\n if (typeof raw !== \"object\" || raw === null) throw new GateResolverRefused(gateId, undefined, \"the resolver is not an object\");\n const { class: actorClass, id, channel, confirmEvent } = raw as Record<string, unknown>;\n if (!isActorClass(actorClass)) {\n throw new GateResolverRefused(gateId, undefined, \"the resolver's class is not a known actor class\");\n }\n if (typeof id !== \"string\" || typeof channel !== \"string\") {\n throw new GateResolverRefused(gateId, actorClass, \"the resolver's id and channel must be strings\");\n }\n if (confirmEvent === undefined) return Object.freeze({ class: actorClass, id, channel });\n if (typeof confirmEvent !== \"string\") {\n throw new GateResolverRefused(gateId, actorClass, \"the resolver's confirmEvent must be a string\");\n }\n return Object.freeze({ class: actorClass, id, channel, confirmEvent });\n}\n\n/** Accepts only a synchronous `{ allowed: boolean }`; a promise is refused because the row cannot wait on it. */\nexport function readDecision(gateId: string, decision: unknown): GateAuthorization {\n if (typeof decision !== \"object\" || decision === null) throw new GateAuthorizeInvalid(gateId);\n const { then, allowed, reason } = decision as Record<string, unknown>;\n if (typeof then === \"function\") {\n (then as (onFulfilled: undefined, onRejected: () => void) => unknown).call(decision, undefined, () => {});\n throw new GateAuthorizeInvalid(gateId);\n }\n if (typeof allowed !== \"boolean\") throw new GateAuthorizeInvalid(gateId);\n if (allowed) return { allowed: true };\n return { allowed: false, reason: typeof reason === \"string\" ? reason : \"authorize refused the resolver\" };\n}\n\nfunction isActorClass(value: unknown): value is ActorClass {\n return typeof value === \"string\" && (ACTOR_CLASSES as readonly string[]).includes(value);\n}\n","import { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport { defaultResolverRefusal, readDecision, snapshotResolver } from \"./resolver-policy.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n GateResolverRefused,\n type GateAuthorize,\n type GateInput,\n type GateRecord,\n type GateResolver,\n type GateStore,\n} from \"./types.js\";\n\n/**\n * Every settle rule lives here so the memory and SQLite stores cannot drift\n * apart. Subclasses only move rows.\n */\nexport abstract class BaseGateStore implements GateStore {\n protected constructor(\n protected readonly clock: () => number,\n private readonly authorize?: GateAuthorize,\n ) {}\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n protected abstract update(record: GateRecord): void;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? globalThis.crypto.randomUUID();\n if (this.read(id)) throw new GateAlreadyExists(id);\n const record: GateRecord = {\n id,\n prompt: input.prompt,\n schema: input.schema,\n status: \"pending\",\n payload: undefined,\n reason: undefined,\n createdAt: this.nowIso(),\n resolvedAt: undefined,\n expiresAt: toIso(input.expiresAt),\n resolvedBy: undefined,\n };\n this.insert(record);\n return record;\n }\n\n get(id: string): GateRecord | undefined {\n const record = this.read(id);\n return record ? this.lapseIfExpired(record) : undefined;\n }\n\n resolve(id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord {\n const resolver = resolvedBy === undefined ? undefined : snapshotResolver(id, resolvedBy);\n const record = this.requirePending(id);\n this.requireAuthorized(record, resolver);\n if (record.schema) {\n const issues = checkAgainstJsonSchema(record.schema, payload);\n if (issues.length > 0) throw new GatePayloadInvalid(id, issues);\n }\n const resolved: GateRecord = { ...record, status: \"resolved\", payload, resolvedAt: this.nowIso(), resolvedBy: resolver };\n this.update(resolved);\n return resolved;\n }\n\n cancel(id: string, reason: string): GateRecord {\n const record = this.requirePending(id);\n const cancelled: GateRecord = { ...record, status: \"cancelled\", reason, resolvedAt: this.nowIso() };\n this.update(cancelled);\n return cancelled;\n }\n\n listPending(): GateRecord[] {\n return this.readByStatus(\"pending\")\n .map((record) => this.lapseIfExpired(record))\n .filter((record) => record.status === \"pending\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n private requirePending(id: string): GateRecord {\n const record = this.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"expired\") throw new GateExpired(id);\n if (record.status !== \"pending\") throw new GateAlreadySettled(id, record.status);\n return record;\n }\n\n /** The default check runs first and `authorize` second, so `authorize` can only narrow who may resolve. */\n private requireAuthorized(record: GateRecord, resolver: Readonly<GateResolver> | undefined): void {\n const refusal = resolver ? defaultResolverRefusal(resolver) : undefined;\n if (resolver && refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);\n if (!this.authorize) return;\n if (!resolver) throw new GateResolverRefused(record.id, undefined, \"a resolver is required when authorize is installed\");\n const decision = readDecision(record.id, this.authorize(Object.freeze({ ...record }), resolver));\n if (!decision.allowed) throw new GateResolverRefused(record.id, resolver.class, decision.reason);\n }\n\n /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */\n private lapseIfExpired(record: GateRecord): GateRecord {\n if (record.status !== \"pending\" || !record.expiresAt) return record;\n if (Date.parse(record.expiresAt) > this.clock()) return record;\n const expired: GateRecord = { ...record, status: \"expired\", resolvedAt: this.nowIso() };\n this.update(expired);\n return expired;\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction toIso(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === \"string\" ? value : value.toISOString();\n}\n"],"mappings":"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","names":[]}
|