@titan-design/hitl 0.2.0 → 0.3.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 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
- protected constructor(clock: () => number);
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, GateAborted as b, GateAlreadyExists as c, GateAlreadySettled as d, GateCancelled as e, GateError as f, GateExpired as g, type GateInput as h, GateNotFound as i, GatePayloadInvalid as j, type GateStatus as k };
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-5P4RFTKR.js.map
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-BeRbMZEp.js';
3
- export { b as GateAborted, c as GateAlreadyExists, d as GateAlreadySettled, e as GateCancelled, f as GateError, g as GateExpired, h as GateInput, i as GateNotFound, j as GatePayloadInvalid, k as GateStatus } from './base-store-BeRbMZEp.js';
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
- checkAgainstJsonSchema
12
- } from "./chunk-5P4RFTKR.js";
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, { ...record });
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 ? { ...row } : void 0;
91
+ return row ? copy(row) : void 0;
87
92
  }
88
93
  update(record) {
89
- this.rows.set(record.id, { ...record });
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((row) => ({ ...row }));
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, { ...record });\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? { ...row } : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected readByStatus(status: GateRecord[\"status\"]): GateRecord[] {\n return [...this.rows.values()].filter((row) => row.status === status).map((row) => ({ ...row }));\n }\n}\n"],"mappings":"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","names":[]}
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":"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","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, k as GateStatus } from './base-store-BeRbMZEp.js';
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
- } from "./chunk-5P4RFTKR.js";
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
- gateTableDdl
151
+ gateResolverMigration,
152
+ gateTableDdl,
153
+ resolverRequiredTriggerDdl
95
154
  };
96
155
  //# sourceMappingURL=sqlite.js.map
@@ -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.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Human-in-the-loop gate()/resolve() primitive",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,8 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@titan-design/store-sqlite": "0.3.0"
38
+ "@titan-design/authority": "^0.1.0",
39
+ "@titan-design/store-sqlite": "0.3.1"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "zod": "^4.0.0"
@@ -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":";AAwCO,IAAM,YAAN,cAAwB,MAAM;AAAA,EACnC,YACE,SACS,QACT;AACA,UAAM,OAAO;AAFJ;AAGT,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAJW;AAKb;AAEO,IAAM,eAAN,cAA2B,UAAU;AAAA,EAC1C,YAAY,QAAgB;AAC1B,UAAM,mBAAmB,MAAM,IAAI,MAAM;AAAA,EAC3C;AACF;AAEO,IAAM,oBAAN,cAAgC,UAAU;AAAA,EAC/C,YAAY,QAAgB;AAC1B,UAAM,kBAAkB,MAAM,mBAAmB,MAAM;AAAA,EACzD;AACF;AAEO,IAAM,qBAAN,cAAiC,UAAU;AAAA,EAChD,YACE,QACS,QACT;AACA,UAAM,QAAQ,MAAM,eAAe,MAAM,IAAI,MAAM;AAF1C;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,gBAAN,cAA4B,UAAU;AAAA,EAC3C,YACE,QACS,QACT;AACA,UAAM,QAAQ,MAAM,mBAAmB,MAAM,IAAI,MAAM;AAF9C;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,cAAN,cAA0B,UAAU;AAAA,EACzC,YAAY,QAAgB;AAC1B,UAAM,QAAQ,MAAM,sCAAsC,MAAM;AAAA,EAClE;AACF;AAEO,IAAM,qBAAN,cAAiC,UAAU;AAAA,EAChD,YACE,QACS,QACT;AACA,UAAM,oBAAoB,MAAM,+BAA+B,OAAO,KAAK,IAAI,CAAC,IAAI,MAAM;AAFjF;AAAA,EAGX;AAAA,EAHW;AAIb;AAEO,IAAM,cAAN,cAA0B,UAAU;AAAA,EACzC,YAAY,QAAgB,QAAgB;AAC1C,UAAM,2BAA2B,MAAM,KAAK,MAAM,IAAI,MAAM;AAAA,EAC9D;AACF;;;AC1FO,SAAS,uBAAuB,QAAoB,OAAgB,OAAO,KAAe;AAC/F,QAAM,SAAmB,CAAC;AAC1B,UAAQ,QAAQ,OAAO,MAAM,MAAM;AACnC,SAAO;AACT;AAEA,SAAS,QAAQ,QAAoB,OAAgB,MAAc,QAAwB;AACzF,MAAI,MAAM,QAAQ,OAAO,KAAK,KAAK,MAAM,QAAQ,OAAO,KAAK,GAAG;AAC9D,eAAY,OAAO,SAAS,OAAO,OAAwB,OAAO,MAAM,MAAM;AAC9E;AAAA,EACF;AACA,MAAI,WAAW,UAAU,CAAC,OAAO,GAAG,OAAO,OAAO,KAAK,GAAG;AACxD,WAAO,KAAK,GAAG,IAAI,cAAc,KAAK,UAAU,OAAO,KAAK,CAAC,EAAE;AAC/D;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,WAAW,OAAO,GAAG,QAAQ,KAAK,CAAC,GAAG;AACzF,WAAO,KAAK,GAAG,IAAI,qBAAqB,KAAK,UAAU,OAAO,IAAI,CAAC,EAAE;AACrE;AAAA,EACF;AACA,MAAI,OAAO,OAAO,SAAS,SAAU;AACrC,MAAI,CAAC,YAAY,OAAO,MAAM,KAAK,GAAG;AACpC,WAAO,KAAK,GAAG,IAAI,cAAc,OAAO,IAAI,cAAc,SAAS,KAAK,CAAC,EAAE;AAC3E;AAAA,EACF;AACA,MAAI,OAAO,SAAS,SAAU,aAAY,QAAQ,OAAkC,MAAM,MAAM;AAChG,MAAI,OAAO,SAAS,QAAS,YAAW,QAAQ,OAAoB,MAAM,MAAM;AAClF;AAEA,SAAS,WAAW,UAAwB,OAAgB,MAAc,QAAwB;AAChG,QAAM,UAAU,SAAS,KAAK,CAAC,WAAW,uBAAuB,QAAQ,OAAO,IAAI,EAAE,WAAW,CAAC;AAClG,MAAI,CAAC,QAAS,QAAO,KAAK,GAAG,IAAI,yBAAyB,SAAS,MAAM,iBAAiB;AAC5F;AAEA,SAAS,YAAY,QAAoB,OAAgC,MAAc,QAAwB;AAC7G,QAAM,aAAc,OAAO,cAAc,CAAC;AAC1C,aAAW,OAAQ,OAAO,YAAY,CAAC,GAAgB;AACrD,QAAI,EAAE,OAAO,OAAQ,QAAO,KAAK,GAAG,IAAI,IAAI,GAAG,YAAY;AAAA,EAC7D;AACA,MAAI,OAAO,yBAAyB,OAAO;AACzC,eAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAI,EAAE,OAAO,YAAa,QAAO,KAAK,GAAG,IAAI,IAAI,GAAG,uBAAuB;AAAA,IAC7E;AAAA,EACF;AACA,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,UAAU,GAAG;AACnD,QAAI,OAAO,MAAO,SAAQ,KAAK,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,GAAG,IAAI,MAAM;AAAA,EACrE;AACF;AAEA,SAAS,WAAW,QAAoB,OAAkB,MAAc,QAAwB;AAC9F,QAAM,QAAQ,OAAO;AACrB,MAAI,CAAC,MAAO;AACZ,QAAM,QAAQ,CAAC,OAAO,UAAU,QAAQ,OAAO,OAAO,GAAG,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC;AACpF;AAEA,SAAS,YAAY,MAAc,OAAyB;AAC1D,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAAA,IAC5E,KAAK;AACH,aAAO,MAAM,QAAQ,KAAK;AAAA,IAC5B,KAAK;AACH,aAAO,OAAO,UAAU,KAAK;AAAA,IAC/B,KAAK;AACH,aAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AAAA,IAC3D,KAAK;AACH,aAAO,UAAU;AAAA,IACnB;AACE,aAAO,OAAO,UAAU;AAAA,EAC5B;AACF;AAEA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,KAAM,QAAO;AAC3B,SAAO,MAAM,QAAQ,KAAK,IAAI,UAAU,OAAO;AACjD;;;ACnEO,IAAe,gBAAf,MAAkD;AAAA,EAC7C,YAA+B,OAAqB;AAArB;AAAA,EAAsB;AAAA,EAAtB;AAAA,EAOzC,OAAO,OAA8B;AACnC,UAAM,KAAK,MAAM,MAAM,WAAW,OAAO,WAAW;AACpD,QAAI,KAAK,KAAK,EAAE,EAAG,OAAM,IAAI,kBAAkB,EAAE;AACjD,UAAM,SAAqB;AAAA,MACzB;AAAA,MACA,QAAQ,MAAM;AAAA,MACd,QAAQ,MAAM;AAAA,MACd,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,WAAW,KAAK,OAAO;AAAA,MACvB,YAAY;AAAA,MACZ,WAAW,MAAM,MAAM,SAAS;AAAA,IAClC;AACA,SAAK,OAAO,MAAM;AAClB,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,IAAoC;AACtC,UAAM,SAAS,KAAK,KAAK,EAAE;AAC3B,WAAO,SAAS,KAAK,eAAe,MAAM,IAAI;AAAA,EAChD;AAAA,EAEA,QAAQ,IAAY,SAA8B;AAChD,UAAM,SAAS,KAAK,eAAe,EAAE;AACrC,QAAI,OAAO,QAAQ;AACjB,YAAM,SAAS,uBAAuB,OAAO,QAAQ,OAAO;AAC5D,UAAI,OAAO,SAAS,EAAG,OAAM,IAAI,mBAAmB,IAAI,MAAM;AAAA,IAChE;AACA,UAAM,WAAuB,EAAE,GAAG,QAAQ,QAAQ,YAAY,SAAS,YAAY,KAAK,OAAO,EAAE;AACjG,SAAK,OAAO,QAAQ;AACpB,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,IAAY,QAA4B;AAC7C,UAAM,SAAS,KAAK,eAAe,EAAE;AACrC,UAAM,YAAwB,EAAE,GAAG,QAAQ,QAAQ,aAAa,QAAQ,YAAY,KAAK,OAAO,EAAE;AAClG,SAAK,OAAO,SAAS;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,cAA4B;AAC1B,WAAO,KAAK,aAAa,SAAS,EAC/B,IAAI,CAAC,WAAW,KAAK,eAAe,MAAM,CAAC,EAC3C,OAAO,CAAC,WAAW,OAAO,WAAW,SAAS,EAC9C,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;AAAA,EAC1D;AAAA,EAEQ,eAAe,IAAwB;AAC7C,UAAM,SAAS,KAAK,IAAI,EAAE;AAC1B,QAAI,CAAC,OAAQ,OAAM,IAAI,aAAa,EAAE;AACtC,QAAI,OAAO,WAAW,UAAW,OAAM,IAAI,YAAY,EAAE;AACzD,QAAI,OAAO,WAAW,UAAW,OAAM,IAAI,mBAAmB,IAAI,OAAO,MAAM;AAC/E,WAAO;AAAA,EACT;AAAA;AAAA,EAGQ,eAAe,QAAgC;AACrD,QAAI,OAAO,WAAW,aAAa,CAAC,OAAO,UAAW,QAAO;AAC7D,QAAI,KAAK,MAAM,OAAO,SAAS,IAAI,KAAK,MAAM,EAAG,QAAO;AACxD,UAAM,UAAsB,EAAE,GAAG,QAAQ,QAAQ,WAAW,YAAY,KAAK,OAAO,EAAE;AACtF,SAAK,OAAO,OAAO;AACnB,WAAO;AAAA,EACT;AAAA,EAEU,SAAiB;AACzB,WAAO,IAAI,KAAK,KAAK,MAAM,CAAC,EAAE,YAAY;AAAA,EAC5C;AACF;AAEA,SAAS,MAAM,OAAsD;AACnE,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO,UAAU,WAAW,QAAQ,MAAM,YAAY;AAC/D;","names":[]}