@titan-design/hitl 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/{base-store-BeRbMZEp.d.ts → base-store-BvwOJL6M.d.ts} +44 -4
- package/dist/{chunk-5P4RFTKR.js → chunk-4GAZKT6Z.js} +84 -5
- package/dist/chunk-4GAZKT6Z.js.map +1 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.js +22 -9
- package/dist/index.js.map +1 -1
- package/dist/sqlite.d.ts +20 -2
- package/dist/sqlite.js +64 -5
- package/dist/sqlite.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-5P4RFTKR.js.map +0 -1
package/README.md
CHANGED
|
@@ -97,6 +97,43 @@ can send on as-is.
|
|
|
97
97
|
The row carries a `reason` column beyond the minimum, so a cancelled gate can
|
|
98
98
|
tell its waiter why.
|
|
99
99
|
|
|
100
|
+
## Who resolved it
|
|
101
|
+
|
|
102
|
+
`resolve` and `resolveGate` take an optional third argument, a `GateResolver`:
|
|
103
|
+
`{ class, id, channel, confirmEvent? }`. `class` is an actor class from
|
|
104
|
+
`@titan-design/authority`. The store records it as `resolvedBy`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
resolveGate(store, "deploy-approval", { approved: true }, {
|
|
108
|
+
class: "owner-terminal",
|
|
109
|
+
id: "owner",
|
|
110
|
+
channel: "cli",
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every store refuses a resolver whose class is not in authority's `RESOLVER_CLASSES`, so an
|
|
115
|
+
agent or automation never answers a gate. It throws `GateResolverRefused` and the gate stays
|
|
116
|
+
pending. The store reads each declared resolver field once into a frozen copy, and checks and stores
|
|
117
|
+
only that copy. A store's `authorize` option runs after the class check and can refuse more,
|
|
118
|
+
never fewer. It must return `{ allowed }` synchronously, or the store throws
|
|
119
|
+
`GateAuthorizeInvalid`. With `authorize` installed, a resolve that names no resolver is refused.
|
|
120
|
+
Refusals name the gate id and the actor class, never the resolver's other fields.
|
|
121
|
+
|
|
122
|
+
hitl records a claim about the resolver; it cannot prove one. Any process that can write
|
|
123
|
+
the database can claim any class.
|
|
124
|
+
|
|
125
|
+
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. Add it to your own
|
|
127
|
+
migration list after `gateMigration`. It is idempotent and does not backfill: gates resolved
|
|
128
|
+
before it read back with `resolvedBy` undefined. Until the migration runs, a store resolves
|
|
129
|
+
without a resolver as before and throws `GateStoreSchemaOutdated` when given one, rather
|
|
130
|
+
than dropping it. `migrate: true` does not run it yet.
|
|
131
|
+
|
|
132
|
+
After the migration, a writer built on hitl 0.2.x fails when it resolves: SQLite aborts the
|
|
133
|
+
statement with a raw error whose message is `hitl: resolvedBy required`. The same trigger
|
|
134
|
+
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.
|
|
136
|
+
|
|
100
137
|
## Settling
|
|
101
138
|
|
|
102
139
|
A gate is `pending`, then exactly one of `resolved`, `cancelled`, or `expired`.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ActorClass } from '@titan-design/authority';
|
|
2
|
+
|
|
1
3
|
/** A gate is a row, never a promise: pending work survives the process that opened it. */
|
|
2
4
|
type GateStatus = "pending" | "resolved" | "cancelled" | "expired";
|
|
3
5
|
/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */
|
|
@@ -14,7 +16,28 @@ interface GateRecord {
|
|
|
14
16
|
createdAt: string;
|
|
15
17
|
resolvedAt: string | undefined;
|
|
16
18
|
expiresAt: string | undefined;
|
|
19
|
+
/** Who resolved the gate, as claimed by the caller; unset for gates resolved before stores recorded it. */
|
|
20
|
+
resolvedBy: GateResolver | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** A claim about who answered a gate. hitl records it and checks its class; it cannot prove it. */
|
|
23
|
+
interface GateResolver {
|
|
24
|
+
class: ActorClass;
|
|
25
|
+
id: string;
|
|
26
|
+
channel: string;
|
|
27
|
+
/** The channel's own id for the confirming event, such as a chat reaction. */
|
|
28
|
+
confirmEvent?: string;
|
|
17
29
|
}
|
|
30
|
+
type GateAuthorization = {
|
|
31
|
+
allowed: true;
|
|
32
|
+
} | {
|
|
33
|
+
allowed: false;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Runs after the default resolver-class check, so it can refuse a resolver but
|
|
38
|
+
* never admit one the default refused. Must return synchronously.
|
|
39
|
+
*/
|
|
40
|
+
type GateAuthorize = (gate: Readonly<GateRecord>, resolver: Readonly<GateResolver>) => GateAuthorization;
|
|
18
41
|
interface GateInput {
|
|
19
42
|
/** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */
|
|
20
43
|
id?: string;
|
|
@@ -29,7 +52,7 @@ interface GateInput {
|
|
|
29
52
|
interface GateStore {
|
|
30
53
|
create(input: GateInput): GateRecord;
|
|
31
54
|
get(id: string): GateRecord | undefined;
|
|
32
|
-
resolve(id: string, payload: unknown): GateRecord;
|
|
55
|
+
resolve(id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord;
|
|
33
56
|
cancel(id: string, reason: string): GateRecord;
|
|
34
57
|
listPending(): GateRecord[];
|
|
35
58
|
}
|
|
@@ -61,6 +84,20 @@ declare class GatePayloadInvalid extends GateError {
|
|
|
61
84
|
declare class GateAborted extends GateError {
|
|
62
85
|
constructor(gateId: string, reason: string);
|
|
63
86
|
}
|
|
87
|
+
declare class GateResolverRefused extends GateError {
|
|
88
|
+
readonly actorClass: string | undefined;
|
|
89
|
+
readonly reason: string;
|
|
90
|
+
constructor(gateId: string, actorClass: string | undefined, reason: string);
|
|
91
|
+
}
|
|
92
|
+
/** The store cannot record a resolver because its table predates the resolver migration. */
|
|
93
|
+
declare class GateStoreSchemaOutdated extends GateError {
|
|
94
|
+
readonly table: string;
|
|
95
|
+
readonly migration: string;
|
|
96
|
+
constructor(gateId: string, table: string, migration: string);
|
|
97
|
+
}
|
|
98
|
+
declare class GateAuthorizeInvalid extends GateError {
|
|
99
|
+
constructor(gateId: string);
|
|
100
|
+
}
|
|
64
101
|
|
|
65
102
|
/**
|
|
66
103
|
* Every settle rule lives here so the memory and SQLite stores cannot drift
|
|
@@ -68,20 +105,23 @@ declare class GateAborted extends GateError {
|
|
|
68
105
|
*/
|
|
69
106
|
declare abstract class BaseGateStore implements GateStore {
|
|
70
107
|
protected readonly clock: () => number;
|
|
71
|
-
|
|
108
|
+
private readonly authorize?;
|
|
109
|
+
protected constructor(clock: () => number, authorize?: GateAuthorize | undefined);
|
|
72
110
|
protected abstract insert(record: GateRecord): void;
|
|
73
111
|
protected abstract read(id: string): GateRecord | undefined;
|
|
74
112
|
protected abstract update(record: GateRecord): void;
|
|
75
113
|
protected abstract readByStatus(status: GateRecord["status"]): GateRecord[];
|
|
76
114
|
create(input: GateInput): GateRecord;
|
|
77
115
|
get(id: string): GateRecord | undefined;
|
|
78
|
-
resolve(id: string, payload: unknown): GateRecord;
|
|
116
|
+
resolve(id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord;
|
|
79
117
|
cancel(id: string, reason: string): GateRecord;
|
|
80
118
|
listPending(): GateRecord[];
|
|
81
119
|
private requirePending;
|
|
120
|
+
/** The default check runs first and `authorize` second, so `authorize` can only narrow who may resolve. */
|
|
121
|
+
private requireAuthorized;
|
|
82
122
|
/** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
|
|
83
123
|
private lapseIfExpired;
|
|
84
124
|
protected nowIso(): string;
|
|
85
125
|
}
|
|
86
126
|
|
|
87
|
-
export { BaseGateStore as B, type GateRecord as G, type JsonSchema as J, type GateStore as a,
|
|
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 GateStatus as p, GateStoreSchemaOutdated as q };
|
|
@@ -48,6 +48,29 @@ var GateAborted = class extends GateError {
|
|
|
48
48
|
super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
var GateResolverRefused = class extends GateError {
|
|
52
|
+
constructor(gateId, actorClass, reason) {
|
|
53
|
+
super(`gate ${gateId} refused a resolution by ${actorClass ?? "an unnamed resolver"}: ${reason}`, gateId);
|
|
54
|
+
this.actorClass = actorClass;
|
|
55
|
+
this.reason = reason;
|
|
56
|
+
}
|
|
57
|
+
actorClass;
|
|
58
|
+
reason;
|
|
59
|
+
};
|
|
60
|
+
var GateStoreSchemaOutdated = class extends GateError {
|
|
61
|
+
constructor(gateId, table, migration) {
|
|
62
|
+
super(`gate ${gateId} cannot record its resolver: table ${table} needs ${migration}`, gateId);
|
|
63
|
+
this.table = table;
|
|
64
|
+
this.migration = migration;
|
|
65
|
+
}
|
|
66
|
+
table;
|
|
67
|
+
migration;
|
|
68
|
+
};
|
|
69
|
+
var GateAuthorizeInvalid = class extends GateError {
|
|
70
|
+
constructor(gateId) {
|
|
71
|
+
super(`authorize for gate ${gateId} must return a decision { allowed: boolean } synchronously`, gateId);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
51
74
|
|
|
52
75
|
// src/json-schema.ts
|
|
53
76
|
function checkAgainstJsonSchema(schema, value, path = "$") {
|
|
@@ -120,12 +143,51 @@ function describe(value) {
|
|
|
120
143
|
return Array.isArray(value) ? "array" : typeof value;
|
|
121
144
|
}
|
|
122
145
|
|
|
146
|
+
// src/resolver-policy.ts
|
|
147
|
+
import { ACTOR_CLASSES, RESOLVER_CLASSES } from "@titan-design/authority";
|
|
148
|
+
function defaultResolverRefusal(resolver) {
|
|
149
|
+
if (RESOLVER_CLASSES.includes(resolver.class)) return void 0;
|
|
150
|
+
return `actor class ${resolver.class} may not resolve a gate`;
|
|
151
|
+
}
|
|
152
|
+
function snapshotResolver(gateId, raw) {
|
|
153
|
+
if (typeof raw !== "object" || raw === null) throw new GateResolverRefused(gateId, void 0, "the resolver is not an object");
|
|
154
|
+
const { class: actorClass, id, channel, confirmEvent } = raw;
|
|
155
|
+
if (!isActorClass(actorClass)) {
|
|
156
|
+
throw new GateResolverRefused(gateId, void 0, "the resolver's class is not a known actor class");
|
|
157
|
+
}
|
|
158
|
+
if (typeof id !== "string" || typeof channel !== "string") {
|
|
159
|
+
throw new GateResolverRefused(gateId, actorClass, "the resolver's id and channel must be strings");
|
|
160
|
+
}
|
|
161
|
+
if (confirmEvent === void 0) return Object.freeze({ class: actorClass, id, channel });
|
|
162
|
+
if (typeof confirmEvent !== "string") {
|
|
163
|
+
throw new GateResolverRefused(gateId, actorClass, "the resolver's confirmEvent must be a string");
|
|
164
|
+
}
|
|
165
|
+
return Object.freeze({ class: actorClass, id, channel, confirmEvent });
|
|
166
|
+
}
|
|
167
|
+
function readDecision(gateId, decision) {
|
|
168
|
+
if (typeof decision !== "object" || decision === null) throw new GateAuthorizeInvalid(gateId);
|
|
169
|
+
const { then, allowed, reason } = decision;
|
|
170
|
+
if (typeof then === "function") {
|
|
171
|
+
then.call(decision, void 0, () => {
|
|
172
|
+
});
|
|
173
|
+
throw new GateAuthorizeInvalid(gateId);
|
|
174
|
+
}
|
|
175
|
+
if (typeof allowed !== "boolean") throw new GateAuthorizeInvalid(gateId);
|
|
176
|
+
if (allowed) return { allowed: true };
|
|
177
|
+
return { allowed: false, reason: typeof reason === "string" ? reason : "authorize refused the resolver" };
|
|
178
|
+
}
|
|
179
|
+
function isActorClass(value) {
|
|
180
|
+
return typeof value === "string" && ACTOR_CLASSES.includes(value);
|
|
181
|
+
}
|
|
182
|
+
|
|
123
183
|
// src/base-store.ts
|
|
124
184
|
var BaseGateStore = class {
|
|
125
|
-
constructor(clock) {
|
|
185
|
+
constructor(clock, authorize) {
|
|
126
186
|
this.clock = clock;
|
|
187
|
+
this.authorize = authorize;
|
|
127
188
|
}
|
|
128
189
|
clock;
|
|
190
|
+
authorize;
|
|
129
191
|
create(input) {
|
|
130
192
|
const id = input.id ?? globalThis.crypto.randomUUID();
|
|
131
193
|
if (this.read(id)) throw new GateAlreadyExists(id);
|
|
@@ -138,7 +200,8 @@ var BaseGateStore = class {
|
|
|
138
200
|
reason: void 0,
|
|
139
201
|
createdAt: this.nowIso(),
|
|
140
202
|
resolvedAt: void 0,
|
|
141
|
-
expiresAt: toIso(input.expiresAt)
|
|
203
|
+
expiresAt: toIso(input.expiresAt),
|
|
204
|
+
resolvedBy: void 0
|
|
142
205
|
};
|
|
143
206
|
this.insert(record);
|
|
144
207
|
return record;
|
|
@@ -147,13 +210,15 @@ var BaseGateStore = class {
|
|
|
147
210
|
const record = this.read(id);
|
|
148
211
|
return record ? this.lapseIfExpired(record) : void 0;
|
|
149
212
|
}
|
|
150
|
-
resolve(id, payload) {
|
|
213
|
+
resolve(id, payload, resolvedBy) {
|
|
214
|
+
const resolver = resolvedBy === void 0 ? void 0 : snapshotResolver(id, resolvedBy);
|
|
151
215
|
const record = this.requirePending(id);
|
|
216
|
+
this.requireAuthorized(record, resolver);
|
|
152
217
|
if (record.schema) {
|
|
153
218
|
const issues = checkAgainstJsonSchema(record.schema, payload);
|
|
154
219
|
if (issues.length > 0) throw new GatePayloadInvalid(id, issues);
|
|
155
220
|
}
|
|
156
|
-
const resolved = { ...record, status: "resolved", payload, resolvedAt: this.nowIso() };
|
|
221
|
+
const resolved = { ...record, status: "resolved", payload, resolvedAt: this.nowIso(), resolvedBy: resolver };
|
|
157
222
|
this.update(resolved);
|
|
158
223
|
return resolved;
|
|
159
224
|
}
|
|
@@ -173,6 +238,15 @@ var BaseGateStore = class {
|
|
|
173
238
|
if (record.status !== "pending") throw new GateAlreadySettled(id, record.status);
|
|
174
239
|
return record;
|
|
175
240
|
}
|
|
241
|
+
/** The default check runs first and `authorize` second, so `authorize` can only narrow who may resolve. */
|
|
242
|
+
requireAuthorized(record, resolver) {
|
|
243
|
+
const refusal = resolver ? defaultResolverRefusal(resolver) : void 0;
|
|
244
|
+
if (resolver && refusal) throw new GateResolverRefused(record.id, resolver.class, refusal);
|
|
245
|
+
if (!this.authorize) return;
|
|
246
|
+
if (!resolver) throw new GateResolverRefused(record.id, void 0, "a resolver is required when authorize is installed");
|
|
247
|
+
const decision = readDecision(record.id, this.authorize(Object.freeze({ ...record }), resolver));
|
|
248
|
+
if (!decision.allowed) throw new GateResolverRefused(record.id, resolver.class, decision.reason);
|
|
249
|
+
}
|
|
176
250
|
/** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
|
|
177
251
|
lapseIfExpired(record) {
|
|
178
252
|
if (record.status !== "pending" || !record.expiresAt) return record;
|
|
@@ -199,7 +273,12 @@ export {
|
|
|
199
273
|
GateExpired,
|
|
200
274
|
GatePayloadInvalid,
|
|
201
275
|
GateAborted,
|
|
276
|
+
GateResolverRefused,
|
|
277
|
+
GateStoreSchemaOutdated,
|
|
278
|
+
GateAuthorizeInvalid,
|
|
202
279
|
checkAgainstJsonSchema,
|
|
280
|
+
defaultResolverRefusal,
|
|
281
|
+
snapshotResolver,
|
|
203
282
|
BaseGateStore
|
|
204
283
|
};
|
|
205
|
-
//# sourceMappingURL=chunk-
|
|
284
|
+
//# sourceMappingURL=chunk-4GAZKT6Z.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 } 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":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ZodType } from 'zod';
|
|
2
|
-
import { G as GateRecord, a as GateStore, B as BaseGateStore, J as JsonSchema } from './base-store-
|
|
3
|
-
export {
|
|
2
|
+
import { G as GateRecord, a as GateStore, b as GateResolver, B as BaseGateStore, c as GateAuthorize, J as JsonSchema } from './base-store-BvwOJL6M.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 GateStatus, q as GateStoreSchemaOutdated } from './base-store-BvwOJL6M.js';
|
|
4
|
+
import '@titan-design/authority';
|
|
4
5
|
|
|
5
6
|
declare const DEFAULT_POLL_MS = 250;
|
|
6
7
|
interface OpenGateOptions<T> {
|
|
@@ -33,12 +34,14 @@ declare function openGate<T = unknown>(store: GateStore, options: OpenGateOption
|
|
|
33
34
|
*/
|
|
34
35
|
declare function waitForGate<T = unknown>(store: GateStore, id: string, options?: WaitOptions<T>): Promise<T>;
|
|
35
36
|
/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */
|
|
36
|
-
declare function resolveGate(store: GateStore, id: string, payload: unknown): GateRecord;
|
|
37
|
+
declare function resolveGate(store: GateStore, id: string, payload: unknown, resolvedBy?: GateResolver): GateRecord;
|
|
37
38
|
declare function cancelGate(store: GateStore, id: string, reason: string): GateRecord;
|
|
38
39
|
|
|
39
40
|
interface MemoryGateStoreOptions {
|
|
40
41
|
/** Epoch-millis clock, injectable so expiry is testable without waiting. */
|
|
41
42
|
now?: () => number;
|
|
43
|
+
/** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */
|
|
44
|
+
authorize?: GateAuthorize;
|
|
42
45
|
}
|
|
43
46
|
/**
|
|
44
47
|
* In-process gates. Right for tests and for a single-process product that only
|
|
@@ -54,6 +57,17 @@ declare class MemoryGateStore extends BaseGateStore {
|
|
|
54
57
|
protected readByStatus(status: GateRecord["status"]): GateRecord[];
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The default refusal: only an owner class may resolve a gate, so an agent or
|
|
62
|
+
* automation never answers its own gate. Every store applies it before `authorize`.
|
|
63
|
+
*/
|
|
64
|
+
declare function defaultResolverRefusal(resolver: GateResolver): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Reads each declared field exactly once into a frozen plain object, so the
|
|
67
|
+
* checks and the stored row see the same resolver and nothing undeclared is kept.
|
|
68
|
+
*/
|
|
69
|
+
declare function snapshotResolver(gateId: string, raw: unknown): Readonly<GateResolver>;
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`
|
|
59
73
|
* emits. It exists so the process that resolves a gate can reject a bad payload
|
|
@@ -63,4 +77,4 @@ declare class MemoryGateStore extends BaseGateStore {
|
|
|
63
77
|
*/
|
|
64
78
|
declare function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path?: string): string[];
|
|
65
79
|
|
|
66
|
-
export { BaseGateStore, DEFAULT_POLL_MS, type GateHandle, GateRecord, GateStore, JsonSchema, MemoryGateStore, type MemoryGateStoreOptions, type OpenGateOptions, type WaitOptions, cancelGate, checkAgainstJsonSchema, openGate, resolveGate, waitForGate };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,18 @@ import {
|
|
|
3
3
|
GateAborted,
|
|
4
4
|
GateAlreadyExists,
|
|
5
5
|
GateAlreadySettled,
|
|
6
|
+
GateAuthorizeInvalid,
|
|
6
7
|
GateCancelled,
|
|
7
8
|
GateError,
|
|
8
9
|
GateExpired,
|
|
9
10
|
GateNotFound,
|
|
10
11
|
GatePayloadInvalid,
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
GateResolverRefused,
|
|
13
|
+
GateStoreSchemaOutdated,
|
|
14
|
+
checkAgainstJsonSchema,
|
|
15
|
+
defaultResolverRefusal,
|
|
16
|
+
snapshotResolver
|
|
17
|
+
} from "./chunk-4GAZKT6Z.js";
|
|
13
18
|
|
|
14
19
|
// src/gate.ts
|
|
15
20
|
import { toJSONSchema } from "zod";
|
|
@@ -42,8 +47,8 @@ async function waitForGate(store, id, options = {}) {
|
|
|
42
47
|
await sleep(pollMs, options.signal);
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
|
-
function resolveGate(store, id, payload) {
|
|
46
|
-
return store.resolve(id, payload);
|
|
50
|
+
function resolveGate(store, id, payload, resolvedBy) {
|
|
51
|
+
return store.resolve(id, payload, resolvedBy);
|
|
47
52
|
}
|
|
48
53
|
function cancelGate(store, id, reason) {
|
|
49
54
|
return store.cancel(id, reason);
|
|
@@ -76,38 +81,46 @@ function sleep(ms, signal) {
|
|
|
76
81
|
var MemoryGateStore = class extends BaseGateStore {
|
|
77
82
|
rows = /* @__PURE__ */ new Map();
|
|
78
83
|
constructor(options = {}) {
|
|
79
|
-
super(options.now ?? Date.now);
|
|
84
|
+
super(options.now ?? Date.now, options.authorize);
|
|
80
85
|
}
|
|
81
86
|
insert(record) {
|
|
82
|
-
this.rows.set(record.id,
|
|
87
|
+
this.rows.set(record.id, copy(record));
|
|
83
88
|
}
|
|
84
89
|
read(id) {
|
|
85
90
|
const row = this.rows.get(id);
|
|
86
|
-
return row ?
|
|
91
|
+
return row ? copy(row) : void 0;
|
|
87
92
|
}
|
|
88
93
|
update(record) {
|
|
89
|
-
this.rows.set(record.id,
|
|
94
|
+
this.rows.set(record.id, copy(record));
|
|
90
95
|
}
|
|
91
96
|
readByStatus(status) {
|
|
92
|
-
return [...this.rows.values()].filter((row) => row.status === status).map(
|
|
97
|
+
return [...this.rows.values()].filter((row) => row.status === status).map(copy);
|
|
93
98
|
}
|
|
94
99
|
};
|
|
100
|
+
function copy(record) {
|
|
101
|
+
return { ...record, resolvedBy: record.resolvedBy && { ...record.resolvedBy } };
|
|
102
|
+
}
|
|
95
103
|
export {
|
|
96
104
|
BaseGateStore,
|
|
97
105
|
DEFAULT_POLL_MS,
|
|
98
106
|
GateAborted,
|
|
99
107
|
GateAlreadyExists,
|
|
100
108
|
GateAlreadySettled,
|
|
109
|
+
GateAuthorizeInvalid,
|
|
101
110
|
GateCancelled,
|
|
102
111
|
GateError,
|
|
103
112
|
GateExpired,
|
|
104
113
|
GateNotFound,
|
|
105
114
|
GatePayloadInvalid,
|
|
115
|
+
GateResolverRefused,
|
|
116
|
+
GateStoreSchemaOutdated,
|
|
106
117
|
MemoryGateStore,
|
|
107
118
|
cancelGate,
|
|
108
119
|
checkAgainstJsonSchema,
|
|
120
|
+
defaultResolverRefusal,
|
|
109
121
|
openGate,
|
|
110
122
|
resolveGate,
|
|
123
|
+
snapshotResolver,
|
|
111
124
|
waitForGate
|
|
112
125
|
};
|
|
113
126
|
//# sourceMappingURL=index.js.map
|
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 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): GateRecord {\n return store.resolve(id, payload);\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 { GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\n now?: () => number;\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);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id,
|
|
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` is nested, so a shallow spread would let a caller rewrite the stored resolver. */\nfunction copy(record: GateRecord): GateRecord {\n return { ...record, resolvedBy: record.resolvedBy && { ...record.resolvedBy } };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA,SAAS,oBAAkC;AAYpC,IAAM,kBAAkB;AA2BxB,SAAS,SAAsB,OAAkB,SAA4C;AAClG,QAAM,QAA4C,EAAE,QAAQ,QAAQ,OAAO;AAC3E,MAAI,QAAQ,OAAO,OAAW,OAAM,KAAK,QAAQ;AACjD,MAAI,QAAQ,cAAc,OAAW,OAAM,YAAY,QAAQ;AAC/D,MAAI,QAAQ,OAAQ,OAAM,SAAS,aAAa,QAAQ,MAAM;AAE9D,QAAM,SAAS,MAAM,OAAO,KAAK;AACjC,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX;AAAA,IACA,MAAM,CAAC,cAAc,CAAC,MAAM;AAC1B,YAAM,SAAyB,EAAE,GAAG,YAAY;AAChD,UAAI,QAAQ,OAAQ,QAAO,SAAS,QAAQ;AAC5C,aAAO,YAAe,OAAO,OAAO,IAAI,MAAM;AAAA,IAChD;AAAA,EACF;AACF;AAOA,eAAsB,YACpB,OACA,IACA,UAA0B,CAAC,GACf;AACZ,QAAM,SAAS,QAAQ,UAAU;AACjC,aAAS;AACP,mBAAe,IAAI,QAAQ,MAAM;AACjC,UAAM,SAAS,MAAM,IAAI,EAAE;AAC3B,QAAI,CAAC,OAAQ,OAAM,IAAI,aAAa,EAAE;AACtC,QAAI,OAAO,WAAW,WAAY,QAAO,SAAS,QAAQ,QAAQ,MAAM;AACxE,QAAI,OAAO,WAAW,YAAa,OAAM,IAAI,cAAc,IAAI,OAAO,UAAU,iBAAiB;AACjG,QAAI,OAAO,WAAW,UAAW,OAAM,IAAI,YAAY,EAAE;AACzD,UAAM,MAAM,QAAQ,QAAQ,MAAM;AAAA,EACpC;AACF;AAGO,SAAS,YAAY,OAAkB,IAAY,SAAkB,YAAuC;AACjH,SAAO,MAAM,QAAQ,IAAI,SAAS,UAAU;AAC9C;AAEO,SAAS,WAAW,OAAkB,IAAY,QAA4B;AACnF,SAAO,MAAM,OAAO,IAAI,MAAM;AAChC;AAEA,SAAS,SAAY,QAAoB,QAAmC;AAC1E,MAAI,CAAC,OAAQ,QAAO,OAAO;AAC3B,QAAM,SAAS,OAAO,UAAU,OAAO,OAAO;AAC9C,MAAI,OAAO,QAAS,QAAO,OAAO;AAClC,QAAM,IAAI;AAAA,IACR,OAAO;AAAA,IACP,OAAO,MAAM,OAAO,IAAI,CAAC,UAAU,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,GAAG,KAAK,MAAM,OAAO,EAAE;AAAA,EACvF;AACF;AAEA,SAAS,eAAe,IAAY,QAAuC;AACzE,MAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,IAAI,OAAO,OAAO,UAAU,SAAS,CAAC;AACnF;AAEA,SAAS,MAAM,IAAY,QAAgD;AACzE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,WAAW,QAAQ,EAAE;AACnC,YAAQ,iBAAiB,SAAS,QAAQ,EAAE,MAAM,KAAK,CAAC;AACxD,aAAS,SAAe;AACtB,mBAAa,KAAK;AAClB,cAAQ,oBAAoB,SAAS,MAAM;AAC3C,cAAQ;AAAA,IACV;AAAA,EACF,CAAC;AACH;;;ACjGO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EAChC,OAAO,oBAAI,IAAwB;AAAA,EAEpD,YAAY,UAAkC,CAAC,GAAG;AAChD,UAAM,QAAQ,OAAO,KAAK,KAAK,QAAQ,SAAS;AAAA,EAClD;AAAA,EAEU,OAAO,QAA0B;AACzC,SAAK,KAAK,IAAI,OAAO,IAAI,KAAK,MAAM,CAAC;AAAA,EACvC;AAAA,EAEU,KAAK,IAAoC;AACjD,UAAM,MAAM,KAAK,KAAK,IAAI,EAAE;AAC5B,WAAO,MAAM,KAAK,GAAG,IAAI;AAAA,EAC3B;AAAA,EAEU,OAAO,QAA0B;AACzC,SAAK,KAAK,IAAI,OAAO,IAAI,KAAK,MAAM,CAAC;AAAA,EACvC;AAAA,EAEU,aAAa,QAA4C;AACjE,WAAO,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,WAAW,MAAM,EAAE,IAAI,IAAI;AAAA,EAChF;AACF;AAGA,SAAS,KAAK,QAAgC;AAC5C,SAAO,EAAE,GAAG,QAAQ,YAAY,OAAO,cAAc,EAAE,GAAG,OAAO,WAAW,EAAE;AAChF;","names":[]}
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { Db, Migration } from '@titan-design/store-sqlite';
|
|
2
|
-
import { B as BaseGateStore, G as GateRecord,
|
|
2
|
+
import { B as BaseGateStore, c as GateAuthorize, G as GateRecord, p as GateStatus } from './base-store-BvwOJL6M.js';
|
|
3
|
+
import '@titan-design/authority';
|
|
3
4
|
|
|
4
5
|
declare const DEFAULT_GATE_TABLE = "hitl_gate";
|
|
5
6
|
declare function gateTableDdl(name?: string): string;
|
|
6
7
|
/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */
|
|
7
8
|
declare function gateMigration(version: number, name?: string): Migration;
|
|
9
|
+
/**
|
|
10
|
+
* The triggers that refuse a resolved row naming no resolver, on update and on
|
|
11
|
+
* insert, so a writer that predates `resolved_by` fails loudly instead of resolving anonymously.
|
|
12
|
+
*/
|
|
13
|
+
declare function resolverRequiredTriggerDdl(name?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Adds `resolved_by` and the resolver trigger. Idempotent and backfill-free:
|
|
16
|
+
* gates resolved before it keep an unknown resolver rather than a guessed one.
|
|
17
|
+
*/
|
|
18
|
+
declare function gateResolverMigration(version: number, name?: string): Migration;
|
|
8
19
|
interface SqliteGateStoreOptions {
|
|
9
20
|
table?: string;
|
|
10
21
|
/** Run `gateMigration` on construction. Off when the product owns its migration list. */
|
|
11
22
|
migrate?: boolean;
|
|
12
23
|
now?: () => number;
|
|
24
|
+
/** Refuses resolvers beyond the default class check; it cannot admit one the default refused. */
|
|
25
|
+
authorize?: GateAuthorize;
|
|
13
26
|
}
|
|
14
27
|
/**
|
|
15
28
|
* Durable gates. Two instances on the same file are two processes as far as the
|
|
@@ -19,11 +32,16 @@ interface SqliteGateStoreOptions {
|
|
|
19
32
|
declare class SqliteGateStore extends BaseGateStore {
|
|
20
33
|
private readonly db;
|
|
21
34
|
private readonly table;
|
|
35
|
+
private resolverColumnSeen;
|
|
22
36
|
constructor(db: Db, options?: SqliteGateStoreOptions);
|
|
23
37
|
protected insert(record: GateRecord): void;
|
|
24
38
|
protected read(id: string): GateRecord | undefined;
|
|
25
39
|
protected update(record: GateRecord): void;
|
|
40
|
+
/** Refuses rather than dropping the resolver when the table has no column to hold it. */
|
|
41
|
+
private updateWithResolver;
|
|
42
|
+
/** Only a positive probe is cached, so a migration run after construction is still noticed. */
|
|
43
|
+
private resolverColumnPresent;
|
|
26
44
|
protected readByStatus(status: GateStatus): GateRecord[];
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
export { DEFAULT_GATE_TABLE, SqliteGateStore, type SqliteGateStoreOptions, gateMigration, gateTableDdl };
|
|
47
|
+
export { DEFAULT_GATE_TABLE, SqliteGateStore, type SqliteGateStoreOptions, gateMigration, gateResolverMigration, gateTableDdl, resolverRequiredTriggerDdl };
|
package/dist/sqlite.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
BaseGateStore
|
|
3
|
-
|
|
2
|
+
BaseGateStore,
|
|
3
|
+
GateStoreSchemaOutdated
|
|
4
|
+
} from "./chunk-4GAZKT6Z.js";
|
|
4
5
|
|
|
5
6
|
// src/sqlite-store.ts
|
|
6
7
|
import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
|
|
@@ -26,15 +27,43 @@ function gateTableDdl(name = DEFAULT_GATE_TABLE) {
|
|
|
26
27
|
function gateMigration(version, name = DEFAULT_GATE_TABLE) {
|
|
27
28
|
return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };
|
|
28
29
|
}
|
|
30
|
+
function resolverRequiredTriggerDdl(name = DEFAULT_GATE_TABLE) {
|
|
31
|
+
return ["UPDATE", "INSERT"].map((event) => resolverTrigger(name, event)).join("\n");
|
|
32
|
+
}
|
|
33
|
+
function resolverTrigger(name, event) {
|
|
34
|
+
const suffix = event === "UPDATE" ? "resolver_required" : "resolver_required_insert";
|
|
35
|
+
return `
|
|
36
|
+
CREATE TRIGGER IF NOT EXISTS ${quoteIdent(`${name}_${suffix}`)}
|
|
37
|
+
BEFORE ${event} ON ${quoteIdent(name)}
|
|
38
|
+
FOR EACH ROW WHEN NEW.status = 'resolved' AND NEW.resolved_by IS NULL
|
|
39
|
+
BEGIN
|
|
40
|
+
SELECT RAISE(ABORT, 'hitl: resolvedBy required');
|
|
41
|
+
END;
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
function gateResolverMigration(version, name = DEFAULT_GATE_TABLE) {
|
|
45
|
+
return {
|
|
46
|
+
version,
|
|
47
|
+
name: `hitl:resolver:${name}`,
|
|
48
|
+
up: (db) => {
|
|
49
|
+
if (!hasResolverColumn(db, name)) db.exec(`ALTER TABLE ${quoteIdent(name)} ADD COLUMN resolved_by TEXT`);
|
|
50
|
+
db.exec(resolverRequiredTriggerDdl(name));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function hasResolverColumn(db, table) {
|
|
55
|
+
return db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = 'resolved_by'").get(table) !== void 0;
|
|
56
|
+
}
|
|
29
57
|
var SqliteGateStore = class extends BaseGateStore {
|
|
30
58
|
constructor(db, options = {}) {
|
|
31
|
-
super(options.now ?? Date.now);
|
|
59
|
+
super(options.now ?? Date.now, options.authorize);
|
|
32
60
|
this.db = db;
|
|
33
61
|
this.table = options.table ?? DEFAULT_GATE_TABLE;
|
|
34
62
|
if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);
|
|
35
63
|
}
|
|
36
64
|
db;
|
|
37
65
|
table;
|
|
66
|
+
resolverColumnSeen = false;
|
|
38
67
|
insert(record) {
|
|
39
68
|
this.db.prepare(
|
|
40
69
|
`INSERT INTO ${quoteIdent(this.table)}
|
|
@@ -47,12 +76,39 @@ var SqliteGateStore = class extends BaseGateStore {
|
|
|
47
76
|
return row ? toRecord(row) : void 0;
|
|
48
77
|
}
|
|
49
78
|
update(record) {
|
|
79
|
+
if (record.resolvedBy) {
|
|
80
|
+
this.updateWithResolver(record, record.resolvedBy);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
50
83
|
this.db.prepare(
|
|
51
84
|
`UPDATE ${quoteIdent(this.table)}
|
|
52
85
|
SET status = ?, payload = ?, reason = ?, resolved_at = ?
|
|
53
86
|
WHERE id = ?`
|
|
54
87
|
).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);
|
|
55
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
|
+
}
|
|
94
|
+
this.db.prepare(
|
|
95
|
+
`UPDATE ${quoteIdent(this.table)}
|
|
96
|
+
SET status = ?, payload = ?, reason = ?, resolved_at = ?, resolved_by = ?
|
|
97
|
+
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
|
+
);
|
|
106
|
+
}
|
|
107
|
+
/** Only a positive probe is cached, so a migration run after construction is still noticed. */
|
|
108
|
+
resolverColumnPresent() {
|
|
109
|
+
if (!this.resolverColumnSeen) this.resolverColumnSeen = hasResolverColumn(this.db, this.table);
|
|
110
|
+
return this.resolverColumnSeen;
|
|
111
|
+
}
|
|
56
112
|
readByStatus(status) {
|
|
57
113
|
const rows = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`).all(status);
|
|
58
114
|
return rows.map(toRecord);
|
|
@@ -84,13 +140,16 @@ function toRecord(row) {
|
|
|
84
140
|
reason: row.reason ?? void 0,
|
|
85
141
|
createdAt: row.created_at,
|
|
86
142
|
resolvedAt: row.resolved_at ?? void 0,
|
|
87
|
-
expiresAt: row.expires_at ?? void 0
|
|
143
|
+
expiresAt: row.expires_at ?? void 0,
|
|
144
|
+
resolvedBy: row.resolved_by ? JSON.parse(row.resolved_by) : void 0
|
|
88
145
|
};
|
|
89
146
|
}
|
|
90
147
|
export {
|
|
91
148
|
DEFAULT_GATE_TABLE,
|
|
92
149
|
SqliteGateStore,
|
|
93
150
|
gateMigration,
|
|
94
|
-
|
|
151
|
+
gateResolverMigration,
|
|
152
|
+
gateTableDdl,
|
|
153
|
+
resolverRequiredTriggerDdl
|
|
95
154
|
};
|
|
96
155
|
//# sourceMappingURL=sqlite.js.map
|
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 type { GateRecord, GateStatus, JsonSchema } 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\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}\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}\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\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now);\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 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 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 };\n}\n"],"mappings":";;;;;AAAA,SAAS,YAAY,qBAA8C;AAI5D,IAAM,qBAAqB;AAE3B,SAAS,aAAa,OAAe,oBAA4B;AACtE,QAAM,IAAI,WAAW,IAAI;AACzB,SAAO;AAAA,iCACwB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAWD,WAAW,GAAG,IAAI,UAAU,CAAC;AAAA,WACnD,CAAC;AAAA;AAEZ;AAGO,SAAS,cAAc,SAAiB,OAAe,oBAA+B;AAC3F,SAAO,EAAE,SAAS,MAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,GAAG,KAAK,aAAa,IAAI,CAAC,EAAE;AAClF;AA0BO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EAGjD,YACmB,IACjB,UAAkC,CAAC,GACnC;AACA,UAAM,QAAQ,OAAO,KAAK,GAAG;AAHZ;AAIjB,SAAK,QAAQ,QAAQ,SAAS;AAC9B,QAAI,QAAQ,WAAW,KAAM,eAAc,IAAI,CAAC,cAAc,GAAG,KAAK,KAAK,CAAC,CAAC;AAAA,EAC/E;AAAA,EANmB;AAAA,EAHF;AAAA,EAWP,OAAO,QAA0B;AACzC,SAAK,GACF;AAAA,MACC,eAAe,WAAW,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA,IAGvC,EACC,IAAI,GAAG,QAAQ,MAAM,CAAC;AAAA,EAC3B;AAAA,EAEU,KAAK,IAAoC;AACjD,UAAM,MAAM,KAAK,GAAG,QAAQ,iBAAiB,WAAW,KAAK,KAAK,CAAC,eAAe,EAAE,IAAI,EAAE;AAG1F,WAAO,MAAM,SAAS,GAAG,IAAI;AAAA,EAC/B;AAAA,EAEU,OAAO,QAA0B;AACzC,SAAK,GACF;AAAA,MACC,UAAU,WAAW,KAAK,KAAK,CAAC;AAAA;AAAA;AAAA,IAGlC,EACC,IAAI,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,OAAO,UAAU,MAAM,OAAO,cAAc,MAAM,OAAO,EAAE;AAAA,EAC3G;AAAA,EAEU,aAAa,QAAkC;AACvD,UAAM,OAAO,KAAK,GACf,QAAQ,iBAAiB,WAAW,KAAK,KAAK,CAAC,uCAAuC,EACtF,IAAI,MAAM;AACb,WAAO,KAAK,IAAI,QAAQ;AAAA,EAC1B;AACF;AAEA,SAAS,QAAQ,QAA+B;AAC9C,SAAO;AAAA,IACL,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO,SAAS,KAAK,UAAU,OAAO,MAAM,IAAI;AAAA,IAChD,OAAO;AAAA,IACP,OAAO,OAAO,OAAO;AAAA,IACrB,OAAO,UAAU;AAAA,IACjB,OAAO;AAAA,IACP,OAAO,cAAc;AAAA,IACrB,OAAO,aAAa;AAAA,EACtB;AACF;AAGA,SAAS,OAAO,SAAiC;AAC/C,SAAO,YAAY,SAAY,OAAO,KAAK,UAAU,OAAO;AAC9D;AAEA,SAAS,SAAS,KAA6B;AAC7C,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI,SAAU,KAAK,MAAM,IAAI,MAAM,IAAmB;AAAA,IAC9D,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI,YAAY,OAAO,SAAY,KAAK,MAAM,IAAI,OAAO;AAAA,IAClE,QAAQ,IAAI,UAAU;AAAA,IACtB,WAAW,IAAI;AAAA,IACf,YAAY,IAAI,eAAe;AAAA,IAC/B,WAAW,IAAI,cAAc;AAAA,EAC/B;AACF;","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 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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/hitl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Human-in-the-loop gate()/resolve() primitive",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@titan-design/authority": "^0.2.0",
|
|
38
39
|
"@titan-design/store-sqlite": "0.3.1"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/json-schema.ts","../src/base-store.ts"],"sourcesContent":["/** 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}\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): 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","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 { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateInput,\n type GateRecord,\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(protected readonly clock: () => number) {}\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 };\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): GateRecord {\n const record = this.requirePending(id);\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() };\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 /** 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":[]}
|