@tinycloud/sdk-services 3.8.0-beta.16 → 3.8.0-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{BaseService-DO7vZ67k.d.ts → BaseService-Ma2MwJAd.d.ts} +1 -1
  2. package/dist/{BaseService-D-29xShG.d.cts → BaseService-O0iyuFLk.d.cts} +1 -1
  3. package/dist/IHooksService-4QNODVy6.d.cts +120 -0
  4. package/dist/IHooksService-CvTpJbQD.d.ts +120 -0
  5. package/dist/ISQLService-Aly1CBsW.d.ts +242 -0
  6. package/dist/ISQLService-BOgWzHLT.d.cts +242 -0
  7. package/dist/context.d.cts +1 -1
  8. package/dist/context.d.ts +1 -1
  9. package/dist/encryption/index.d.cts +4 -4
  10. package/dist/encryption/index.d.ts +4 -4
  11. package/dist/encryption/named-keys.d.cts +2 -2
  12. package/dist/encryption/named-keys.d.ts +2 -2
  13. package/dist/feed/index.cjs +787 -0
  14. package/dist/feed/index.cjs.map +1 -0
  15. package/dist/feed/index.d.cts +176 -0
  16. package/dist/feed/index.d.ts +176 -0
  17. package/dist/feed/index.js +739 -0
  18. package/dist/feed/index.js.map +1 -0
  19. package/dist/index.cjs +51 -8
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +15 -115
  22. package/dist/index.d.ts +15 -115
  23. package/dist/index.js +51 -8
  24. package/dist/index.js.map +1 -1
  25. package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
  26. package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
  27. package/dist/kv/index.d.cts +4 -4
  28. package/dist/kv/index.d.ts +4 -4
  29. package/dist/kv/replication/index.d.cts +3 -3
  30. package/dist/kv/replication/index.d.ts +3 -3
  31. package/dist/{scope-1-8skVDk.d.ts → scope-BZr0QwxX.d.ts} +1 -1
  32. package/dist/{scope-Dv3IAXMr.d.cts → scope-DEgc9f-W.d.cts} +1 -1
  33. package/dist/sql/index.cjs +21 -6
  34. package/dist/sql/index.cjs.map +1 -1
  35. package/dist/sql/index.d.cts +6 -236
  36. package/dist/sql/index.d.ts +6 -236
  37. package/dist/sql/index.js +21 -6
  38. package/dist/sql/index.js.map +1 -1
  39. package/dist/telemetry/requestLog.d.cts +1 -1
  40. package/dist/telemetry/requestLog.d.ts +1 -1
  41. package/dist/{types-DZBExWAf.d.ts → types-Bf91gup6.d.ts} +1 -1
  42. package/dist/{types-DAOyIbXu.d.ts → types-CSd0RYir.d.cts} +1 -1
  43. package/dist/{types-DAOyIbXu.d.cts → types-CSd0RYir.d.ts} +1 -1
  44. package/dist/{types-CBKrdtP8.d.cts → types-D4XbIfFJ.d.cts} +1 -1
  45. package/package.json +6 -1
@@ -0,0 +1,739 @@
1
+ // src/types.ts
2
+ var ErrorCodes = {
3
+ // Common errors
4
+ NOT_FOUND: "NOT_FOUND",
5
+ AUTH_EXPIRED: "AUTH_EXPIRED",
6
+ AUTH_REQUIRED: "AUTH_REQUIRED",
7
+ AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
8
+ /** HTTP 401: the delegation the invocation cites was revoked. */
9
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
10
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
11
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
12
+ NETWORK_ERROR: "NETWORK_ERROR",
13
+ TIMEOUT: "TIMEOUT",
14
+ ABORTED: "ABORTED",
15
+ INVALID_INPUT: "INVALID_INPUT",
16
+ PERMISSION_DENIED: "PERMISSION_DENIED",
17
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
18
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
19
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
20
+ NODE_BUSY: "NODE_BUSY",
21
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
22
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
23
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
24
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
25
+ // KV-specific errors
26
+ KV_NOT_FOUND: "KV_NOT_FOUND",
27
+ KV_WRITE_FAILED: "KV_WRITE_FAILED",
28
+ KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
29
+ KV_CONFLICT: "KV_CONFLICT",
30
+ KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
31
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
32
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
33
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
34
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
35
+ // SQL-specific errors
36
+ SQL_ERROR: "SQL_ERROR",
37
+ SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
38
+ SQL_DATABASE_NOT_FOUND: "SQL_DATABASE_NOT_FOUND",
39
+ SQL_RESPONSE_TOO_LARGE: "SQL_RESPONSE_TOO_LARGE",
40
+ SQL_QUOTA_EXCEEDED: "SQL_QUOTA_EXCEEDED",
41
+ SQL_INVALID_STATEMENT: "SQL_INVALID_STATEMENT",
42
+ SQL_SCHEMA_ERROR: "SQL_SCHEMA_ERROR",
43
+ SQL_READONLY_VIOLATION: "SQL_READONLY_VIOLATION",
44
+ // Storage quota errors
45
+ STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
46
+ STORAGE_LIMIT_REACHED: "STORAGE_LIMIT_REACHED",
47
+ /** The node answered the usage read without usage numbers (it predates the read). */
48
+ STORAGE_STATUS_UNAVAILABLE: "STORAGE_STATUS_UNAVAILABLE",
49
+ // DuckDB-specific errors
50
+ DUCKDB_ERROR: "DUCKDB_ERROR",
51
+ DUCKDB_PERMISSION_DENIED: "DUCKDB_PERMISSION_DENIED",
52
+ DUCKDB_DATABASE_NOT_FOUND: "DUCKDB_DATABASE_NOT_FOUND",
53
+ DUCKDB_RESPONSE_TOO_LARGE: "DUCKDB_RESPONSE_TOO_LARGE",
54
+ DUCKDB_QUOTA_EXCEEDED: "DUCKDB_QUOTA_EXCEEDED",
55
+ DUCKDB_INVALID_STATEMENT: "DUCKDB_INVALID_STATEMENT",
56
+ DUCKDB_SCHEMA_ERROR: "DUCKDB_SCHEMA_ERROR",
57
+ DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
58
+ };
59
+ var defaultRetryPolicy = {
60
+ maxAttempts: 4,
61
+ backoff: "exponential",
62
+ baseDelayMs: 250,
63
+ maxDelayMs: 4e3,
64
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
65
+ maxTotalMs: 12e3
66
+ };
67
+ function ok(data) {
68
+ return { ok: true, data };
69
+ }
70
+ function err(error) {
71
+ return { ok: false, error };
72
+ }
73
+
74
+ // src/feed/schemas.ts
75
+ import { z } from "zod";
76
+ function isJSONValue(value, active = /* @__PURE__ */ new Set()) {
77
+ if (value === null || typeof value === "boolean" || typeof value === "string") return true;
78
+ if (typeof value === "number") return Number.isFinite(value);
79
+ if (typeof value !== "object" || active.has(value)) return false;
80
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
81
+ active.add(value);
82
+ let valid = true;
83
+ for (const child of Array.isArray(value) ? value : Object.values(value)) {
84
+ if (!isJSONValue(child, active)) {
85
+ valid = false;
86
+ break;
87
+ }
88
+ }
89
+ active.delete(value);
90
+ return valid;
91
+ }
92
+ var jsonValueSchema = z.custom((value) => isJSONValue(value));
93
+ var feedResponsePayloadSchema = z.object({
94
+ type: z.string().min(1),
95
+ version: z.number().int().positive(),
96
+ data: jsonValueSchema
97
+ }).strict();
98
+ var identity = {
99
+ cardId: z.string().min(1),
100
+ revision: z.number().int().positive(),
101
+ issuanceId: z.string().min(1)
102
+ };
103
+ var feedCardSchema = z.object({
104
+ ...identity,
105
+ kind: z.string().min(1),
106
+ needsResponse: z.boolean(),
107
+ title: z.string(),
108
+ body: z.string(),
109
+ choices: z.array(z.object({ id: z.string().min(1), label: z.string() })).optional(),
110
+ urgency: z.number().finite(),
111
+ createdAt: z.string().min(1),
112
+ payload: z.record(z.unknown()).optional()
113
+ });
114
+ var newFeedResponseSchema = z.object({
115
+ ...identity,
116
+ responseId: z.string().min(1),
117
+ kind: z.enum(["answer", "followup"]),
118
+ choiceId: z.string().min(1).optional(),
119
+ text: z.string().optional(),
120
+ supersedesResponseId: z.string().min(1).optional(),
121
+ createdAt: z.string().min(1)
122
+ });
123
+ var newFeedMessageSchema = z.object({
124
+ ...identity,
125
+ responseId: z.string().min(1),
126
+ kind: z.literal("message"),
127
+ text: z.string(),
128
+ createdAt: z.string().min(1)
129
+ }).strict();
130
+ var feedResponseHeaderSchema = z.union([newFeedResponseSchema, newFeedMessageSchema]);
131
+ var feedHandlingSchema = z.object({
132
+ ...identity,
133
+ handlingId: z.string().min(1),
134
+ responseId: z.string().min(1),
135
+ phase: z.enum(["acknowledged", "succeeded", "failed"]),
136
+ createdAt: z.string().min(1),
137
+ detail: z.record(z.unknown()).optional()
138
+ });
139
+
140
+ // src/feed/notification.ts
141
+ var projectFeedResponseNotification = (response) => {
142
+ const notification = {
143
+ responseId: response.responseId,
144
+ cardId: response.cardId,
145
+ revision: response.revision,
146
+ issuanceId: response.issuanceId,
147
+ kind: response.kind,
148
+ createdAt: response.createdAt,
149
+ sequence: response.sequence
150
+ };
151
+ if (response.kind !== "message" && response.choiceId !== void 0) notification.choiceId = response.choiceId;
152
+ if (response.text !== void 0) notification.text = response.text;
153
+ if (response.kind !== "message" && response.supersedesResponseId !== void 0) notification.supersedesResponseId = response.supersedesResponseId;
154
+ if (response.payloadType !== void 0) notification.payloadType = response.payloadType;
155
+ if (response.payloadVersion !== void 0) notification.payloadVersion = response.payloadVersion;
156
+ return notification;
157
+ };
158
+
159
+ // src/feed/store.ts
160
+ var PRIVATE_RESPONSE_OPTIONS = { redactErrors: true };
161
+ function feedError(code, message) {
162
+ return err({ code, message, service: "feed" });
163
+ }
164
+ function feedJson(value) {
165
+ return JSON.stringify(value, (_key, item) => {
166
+ if (item && typeof item === "object" && !Array.isArray(item)) {
167
+ const sorted = /* @__PURE__ */ Object.create(null);
168
+ for (const key of Object.keys(item).sort()) sorted[key] = item[key];
169
+ return sorted;
170
+ }
171
+ return item;
172
+ });
173
+ }
174
+ function validIdentity(identity2) {
175
+ return typeof identity2.cardId === "string" && identity2.cardId.length > 0 && Number.isSafeInteger(identity2.revision) && identity2.revision > 0 && typeof identity2.issuanceId === "string" && identity2.issuanceId.length > 0;
176
+ }
177
+ function sameFeedIdentity(a, b) {
178
+ return a.cardId === b.cardId && a.revision === b.revision && a.issuanceId === b.issuanceId;
179
+ }
180
+ async function jsonRows(db, sql, params = [], options) {
181
+ const result = await db.query(sql, params, options);
182
+ if (!result.ok) return result;
183
+ try {
184
+ return ok(result.data.rows.map((row) => JSON.parse(row[0])));
185
+ } catch {
186
+ return feedError("INVALID_INPUT", "Feed record contains invalid JSON");
187
+ }
188
+ }
189
+ var RESPONSE_HEADER_SELECT = `SELECT r.sequence, r.response_json, p.payload_type, p.payload_version
190
+ FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id`;
191
+ function responseDataFailure(message, error) {
192
+ let code = ErrorCodes.SQL_ERROR;
193
+ if (error !== null && typeof error === "object" && "code" in error) {
194
+ const candidate = error.code;
195
+ if (typeof candidate === "string" && Object.values(ErrorCodes).some((known) => known === candidate)) code = candidate;
196
+ }
197
+ return feedError(code, message);
198
+ }
199
+ function responseFromRow(row) {
200
+ return responseFromHeader(row, feedResponseHeaderSchema.parse(JSON.parse(String(row[1]))));
201
+ }
202
+ function responseFromHeader(row, header) {
203
+ const response = { ...header, sequence: Number(row[0]) };
204
+ if (row[2] !== null && row[2] !== void 0) {
205
+ response.payloadType = String(row[2]);
206
+ response.payloadVersion = Number(row[3]);
207
+ }
208
+ return projectFeedResponseNotification(response);
209
+ }
210
+ async function storedResponse(db, responseId) {
211
+ try {
212
+ const result = await db.query(`SELECT r.sequence, r.response_json, p.payload_type, p.payload_version, p.payload_json
213
+ FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
214
+ WHERE r.response_id = ?`, [responseId], PRIVATE_RESPONSE_OPTIONS);
215
+ if (!result.ok) return responseDataFailure("Could not read the saved feed response", result.error);
216
+ const row = result.data.rows[0];
217
+ if (!row) return ok(void 0);
218
+ const header = feedResponseHeaderSchema.parse(JSON.parse(String(row[1])));
219
+ const response = responseFromHeader(row, header);
220
+ const payload = row[2] === null || row[2] === void 0 ? void 0 : feedResponsePayloadSchema.parse({ type: row[2], version: Number(row[3]), data: JSON.parse(String(row[4])) });
221
+ return ok({ response, headerJson: feedJson(header), payload });
222
+ } catch (error) {
223
+ return responseDataFailure("Could not read the saved feed response", error);
224
+ }
225
+ }
226
+ var initializeFeedDatabases = async ({ output, responses }) => {
227
+ const initialized = await output.batch([
228
+ { sql: `CREATE TABLE IF NOT EXISTS feed_cards (
229
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
230
+ card_id TEXT NOT NULL, revision INTEGER NOT NULL CHECK (revision > 0), issuance_id TEXT NOT NULL,
231
+ card_json TEXT NOT NULL, UNIQUE (card_id, revision, issuance_id)
232
+ )` },
233
+ { sql: `CREATE TABLE IF NOT EXISTS feed_handling (
234
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT, handling_id TEXT NOT NULL UNIQUE,
235
+ response_id TEXT NOT NULL, card_id TEXT NOT NULL, revision INTEGER NOT NULL, issuance_id TEXT NOT NULL,
236
+ phase TEXT NOT NULL CHECK (phase IN ('acknowledged', 'succeeded', 'failed')),
237
+ handling_json TEXT NOT NULL
238
+ )` },
239
+ { sql: "CREATE INDEX IF NOT EXISTS feed_handling_response ON feed_handling (response_id, sequence)" }
240
+ ]);
241
+ if (!initialized.ok) return initialized;
242
+ const responseSchema = await responses.batch([
243
+ { sql: `CREATE TABLE IF NOT EXISTS feed_responses (
244
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT, response_id TEXT NOT NULL UNIQUE,
245
+ card_id TEXT NOT NULL, revision INTEGER NOT NULL, issuance_id TEXT NOT NULL,
246
+ response_json TEXT NOT NULL
247
+ )` },
248
+ { sql: "CREATE INDEX IF NOT EXISTS feed_responses_card ON feed_responses (card_id, revision, issuance_id, sequence)" },
249
+ { sql: `CREATE TABLE IF NOT EXISTS feed_response_payloads (
250
+ response_id TEXT PRIMARY KEY, payload_type TEXT NOT NULL, payload_version INTEGER NOT NULL, payload_json TEXT NOT NULL
251
+ )` }
252
+ ]);
253
+ return responseSchema.ok ? ok(void 0) : responseSchema;
254
+ };
255
+ var publishFeedCard = async (output, card) => {
256
+ if (!validIdentity(card)) return feedError("INVALID_INPUT", "A card requires its full ID, positive revision and issuance ID");
257
+ const encoded = feedJson(card);
258
+ const inserted = await output.execute(
259
+ `INSERT INTO feed_cards (card_id, revision, issuance_id, card_json) VALUES (?, ?, ?, ?)
260
+ ON CONFLICT (card_id, revision, issuance_id) DO NOTHING`,
261
+ [card.cardId, card.revision, card.issuanceId, encoded]
262
+ );
263
+ if (!inserted.ok) return inserted;
264
+ const stored = await jsonRows(
265
+ output,
266
+ "SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
267
+ [card.cardId, card.revision, card.issuanceId]
268
+ );
269
+ if (!stored.ok) return stored;
270
+ if (!stored.data[0] || feedJson(stored.data[0]) !== encoded) {
271
+ return feedError("INVALID_INPUT", "A published card identity cannot be silently changed");
272
+ }
273
+ return ok(stored.data[0]);
274
+ };
275
+ var listFeedCards = async (output) => {
276
+ return jsonRows(output, `SELECT card_json FROM feed_cards
277
+ WHERE sequence IN (SELECT MAX(sequence) FROM feed_cards GROUP BY card_id) ORDER BY sequence DESC`);
278
+ };
279
+ var appendFeedResponse = async (output, responses, input, payload) => {
280
+ try {
281
+ const header = newFeedResponseSchema.safeParse(input);
282
+ if (!header.success) return feedError("INVALID_INPUT", "Invalid feed response header");
283
+ return appendResponse(output, responses, header.data, payload);
284
+ } catch {
285
+ return feedError("INVALID_INPUT", "Invalid feed response header");
286
+ }
287
+ };
288
+ var appendFeedMessage = async (output, responses, input, payload) => {
289
+ try {
290
+ const header = newFeedMessageSchema.safeParse(input);
291
+ if (!header.success) return feedError("INVALID_INPUT", "Invalid feed message header");
292
+ return appendResponse(output, responses, header.data, payload);
293
+ } catch {
294
+ return feedError("INVALID_INPUT", "Invalid feed message header");
295
+ }
296
+ };
297
+ async function appendResponse(output, responses, response, inputPayload) {
298
+ let payload;
299
+ try {
300
+ if (inputPayload !== void 0) {
301
+ const parsed = feedResponsePayloadSchema.safeParse(inputPayload);
302
+ if (!parsed.success) return feedError("INVALID_INPUT", "Invalid private feed response payload");
303
+ payload = parsed.data;
304
+ }
305
+ } catch {
306
+ return feedError("INVALID_INPUT", "Invalid feed response or private payload");
307
+ }
308
+ try {
309
+ const cards = await jsonRows(
310
+ output,
311
+ "SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
312
+ [response.cardId, response.revision, response.issuanceId],
313
+ PRIVATE_RESPONSE_OPTIONS
314
+ );
315
+ if (!cards.ok) return responseDataFailure("Could not verify the response card issuance", cards.error);
316
+ const card = cards.data[0];
317
+ if (!card || !sameFeedIdentity(card, response)) {
318
+ return feedError("INVALID_INPUT", "The response does not identify a published card issuance");
319
+ }
320
+ if (response.kind !== "message") {
321
+ if (!card.needsResponse) {
322
+ return feedError("INVALID_INPUT", "The response does not identify a published actionable card issuance");
323
+ }
324
+ if (response.choiceId !== void 0 && !card.choices?.some((choice) => choice.id === response.choiceId)) {
325
+ return feedError("INVALID_INPUT", "The response choice is not part of this card issuance");
326
+ }
327
+ if (response.supersedesResponseId) {
328
+ const previous = await jsonRows(
329
+ responses,
330
+ "SELECT response_json FROM feed_responses WHERE response_id = ?",
331
+ [response.supersedesResponseId],
332
+ PRIVATE_RESPONSE_OPTIONS
333
+ );
334
+ if (!previous.ok) return responseDataFailure("Could not verify the corrected response", previous.error);
335
+ if (!previous.data[0] || !sameFeedIdentity(previous.data[0], response)) {
336
+ return feedError("INVALID_INPUT", "A correction must reference a response to the same card issuance");
337
+ }
338
+ }
339
+ }
340
+ const encoded = feedJson(response);
341
+ const match = (stored) => {
342
+ const samePayload = payload === void 0 ? stored.payload === void 0 : stored.payload !== void 0 && feedJson(stored.payload) === feedJson(payload);
343
+ return stored.headerJson === encoded && samePayload ? ok(stored.response) : feedError("INVALID_INPUT", "A response ID cannot be reused for different header or private payload content");
344
+ };
345
+ const existing = await storedResponse(responses, response.responseId);
346
+ if (!existing.ok) return existing;
347
+ if (existing.data) return match(existing.data);
348
+ const statements = [{
349
+ sql: "INSERT INTO feed_responses (response_id, card_id, revision, issuance_id, response_json) VALUES (?, ?, ?, ?, ?)",
350
+ params: [response.responseId, response.cardId, response.revision, response.issuanceId, encoded]
351
+ }];
352
+ if (payload !== void 0) statements.push({
353
+ sql: "INSERT INTO feed_response_payloads (response_id, payload_type, payload_version, payload_json) VALUES (?, ?, ?, ?)",
354
+ params: [response.responseId, payload.type, payload.version, feedJson(payload.data)]
355
+ });
356
+ let failed = false;
357
+ let failure;
358
+ try {
359
+ const saved = await responses.batch(statements, PRIVATE_RESPONSE_OPTIONS);
360
+ if (!saved.ok) {
361
+ failed = true;
362
+ failure = saved.error;
363
+ }
364
+ } catch (error) {
365
+ failed = true;
366
+ failure = error;
367
+ }
368
+ const committed = await storedResponse(responses, response.responseId);
369
+ if (!committed.ok) return failed ? responseDataFailure("Could not save the feed response and private payload", failure) : committed;
370
+ if (!committed.data) return responseDataFailure("Could not save the feed response and private payload", failure);
371
+ return match(committed.data);
372
+ } catch (error) {
373
+ return responseDataFailure("Could not save the feed response and private payload", error);
374
+ }
375
+ }
376
+ var readFeedResponsePayload = async (responses, identity2) => {
377
+ if (!validIdentity(identity2) || typeof identity2.responseId !== "string" || !identity2.responseId) {
378
+ return feedError("INVALID_INPUT", "A private payload read requires the full response and card identity");
379
+ }
380
+ try {
381
+ const result = await responses.query(
382
+ `SELECT p.payload_type, p.payload_version, p.payload_json
383
+ FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
384
+ WHERE r.response_id = ? AND r.card_id = ? AND r.revision = ? AND r.issuance_id = ?`,
385
+ [identity2.responseId, identity2.cardId, identity2.revision, identity2.issuanceId],
386
+ PRIVATE_RESPONSE_OPTIONS
387
+ );
388
+ if (!result.ok) return responseDataFailure("Could not read the private feed response payload", result.error);
389
+ const row = result.data.rows[0];
390
+ if (!row) return feedError("NOT_FOUND", "The response identity does not match a saved response");
391
+ if (row[0] === null || row[0] === void 0) return ok(void 0);
392
+ const parsed = feedResponsePayloadSchema.safeParse({ type: row[0], version: Number(row[1]), data: JSON.parse(String(row[2])) });
393
+ return parsed.success ? ok(parsed.data) : feedError("INVALID_INPUT", "The saved private response payload is invalid");
394
+ } catch (error) {
395
+ return responseDataFailure("Could not read the private feed response payload", error);
396
+ }
397
+ };
398
+ var listFeedResponses = async (responses, options = {}) => {
399
+ const after = options.afterSequence ?? 0;
400
+ const limit = options.limit ?? 100;
401
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
402
+ return feedError("INVALID_INPUT", "Response pagination requires a nonnegative sequence and limit between 1 and 1000");
403
+ }
404
+ try {
405
+ const result = await responses.query(
406
+ `${RESPONSE_HEADER_SELECT} WHERE r.sequence > ? ORDER BY r.sequence LIMIT ?`,
407
+ [after, limit],
408
+ { ...PRIVATE_RESPONSE_OPTIONS, signal: options.signal }
409
+ );
410
+ if (!result.ok) return responseDataFailure("Could not list feed responses", result.error);
411
+ return ok(result.data.rows.map(responseFromRow));
412
+ } catch (error) {
413
+ return responseDataFailure("Could not list feed responses", error);
414
+ }
415
+ };
416
+ var recordFeedHandling = async (output, handling) => {
417
+ if (!validIdentity(handling) || !handling.handlingId || !handling.responseId || !["acknowledged", "succeeded", "failed"].includes(handling.phase)) {
418
+ return feedError("INVALID_INPUT", "Handling requires full response and card identities and a known phase");
419
+ }
420
+ const previous = await jsonRows(
421
+ output,
422
+ "SELECT handling_json FROM feed_handling WHERE response_id = ? ORDER BY sequence",
423
+ [handling.responseId]
424
+ );
425
+ if (!previous.ok) return previous;
426
+ if (previous.data.some((item) => !sameFeedIdentity(item, handling))) {
427
+ return feedError("INVALID_INPUT", "Handling cannot move a response to another card issuance");
428
+ }
429
+ const encoded = feedJson(handling);
430
+ const duplicate = previous.data.find((item) => item.handlingId === handling.handlingId);
431
+ if (duplicate) {
432
+ return feedJson(duplicate) === encoded ? ok(duplicate) : feedError("INVALID_INPUT", "A handling ID cannot change content");
433
+ }
434
+ const knownOutcome = previous.data.find((item) => item.phase !== "acknowledged");
435
+ if (knownOutcome && handling.phase !== "acknowledged") {
436
+ return feedError("INVALID_INPUT", "An existing outcome requires an explicit follow-up response, not a replacement");
437
+ }
438
+ const inserted = await output.execute(
439
+ `INSERT INTO feed_handling (handling_id, response_id, card_id, revision, issuance_id, phase, handling_json)
440
+ SELECT ?, ?, ?, ?, ?, ?, ?
441
+ WHERE NOT EXISTS (
442
+ SELECT 1 FROM feed_handling WHERE response_id = ? AND (card_id <> ? OR revision <> ? OR issuance_id <> ?)
443
+ ) AND (? = 'acknowledged' OR NOT EXISTS (
444
+ SELECT 1 FROM feed_handling WHERE response_id = ? AND phase IN ('succeeded', 'failed')
445
+ )) ON CONFLICT (handling_id) DO NOTHING`,
446
+ [
447
+ handling.handlingId,
448
+ handling.responseId,
449
+ handling.cardId,
450
+ handling.revision,
451
+ handling.issuanceId,
452
+ handling.phase,
453
+ encoded,
454
+ handling.responseId,
455
+ handling.cardId,
456
+ handling.revision,
457
+ handling.issuanceId,
458
+ handling.phase,
459
+ handling.responseId
460
+ ]
461
+ );
462
+ if (!inserted.ok) return inserted;
463
+ const stored = await jsonRows(
464
+ output,
465
+ "SELECT handling_json FROM feed_handling WHERE handling_id = ?",
466
+ [handling.handlingId]
467
+ );
468
+ if (!stored.ok) return stored;
469
+ if (!stored.data[0]) return feedError("INVALID_INPUT", "Handling conflicts with an existing outcome or response issuance");
470
+ return feedJson(stored.data[0]) === encoded ? ok(stored.data[0]) : feedError("INVALID_INPUT", "A handling ID cannot change content");
471
+ };
472
+ async function selectResponseHandling(output, identity2) {
473
+ const records = await jsonRows(
474
+ output,
475
+ `SELECT handling_json FROM feed_handling WHERE response_id = ? AND card_id = ? AND revision = ? AND issuance_id = ?
476
+ ORDER BY sequence`,
477
+ [identity2.responseId, identity2.cardId, identity2.revision, identity2.issuanceId]
478
+ );
479
+ if (!records.ok) return records;
480
+ if (records.data.length === 0) return ok(void 0);
481
+ const handling = { acknowledgement: null, outcome: null };
482
+ for (const item of records.data) {
483
+ if (item.responseId !== identity2.responseId || !sameFeedIdentity(item, identity2)) {
484
+ return feedError("INVALID_INPUT", "Stored handling does not match the response identity");
485
+ }
486
+ if (item.phase === "acknowledged") handling.acknowledgement = item;
487
+ else if (item.phase === "succeeded" || item.phase === "failed") handling.outcome = item;
488
+ else return feedError("INVALID_INPUT", "Stored handling has an unknown phase");
489
+ }
490
+ return ok(handling);
491
+ }
492
+ var readFeedResponseHandling = async (output, identity2) => {
493
+ if (!validIdentity(identity2) || typeof identity2.responseId !== "string" || !identity2.responseId) {
494
+ return feedError("INVALID_INPUT", "Handling reads require full response and card identities");
495
+ }
496
+ const handling = await selectResponseHandling(output, identity2);
497
+ if (!handling.ok) return handling;
498
+ return handling.data ? ok(handling.data) : feedError("NOT_FOUND", "Matching response handling not found");
499
+ };
500
+ var getFeedCardState = async (output, responses, identity2) => {
501
+ const params = [identity2.cardId, identity2.revision, identity2.issuanceId];
502
+ const cards = await jsonRows(
503
+ output,
504
+ "SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
505
+ params
506
+ );
507
+ if (!cards.ok) return cards;
508
+ if (!cards.data[0]) return feedError("NOT_FOUND", "Card issuance not found");
509
+ const saved = await responses.query(
510
+ `${RESPONSE_HEADER_SELECT} WHERE r.card_id = ? AND r.revision = ? AND r.issuance_id = ?
511
+ AND json_extract(r.response_json, '$.kind') IN ('answer', 'followup')
512
+ ORDER BY r.sequence DESC LIMIT 1`,
513
+ params,
514
+ PRIVATE_RESPONSE_OPTIONS
515
+ );
516
+ if (!saved.ok) return responseDataFailure("Could not read the card's saved response", saved.error);
517
+ const row = saved.data.rows[0];
518
+ let response;
519
+ try {
520
+ response = row ? responseFromRow(row) : null;
521
+ } catch (error) {
522
+ return responseDataFailure("Could not read the card's saved response", error);
523
+ }
524
+ const handling = response ? await selectResponseHandling(output, response) : ok(void 0);
525
+ if (!handling.ok) return handling;
526
+ return ok({
527
+ card: cards.data[0],
528
+ response,
529
+ acknowledgement: handling.data?.acknowledgement ?? null,
530
+ outcome: handling.data?.outcome ?? null
531
+ });
532
+ };
533
+ var resolveFeedCard = async (output, reference) => {
534
+ if (!reference) return feedError("INVALID_INPUT", "A card reference is required");
535
+ const rows = await output.query(
536
+ `SELECT card_id, revision, issuance_id FROM feed_cards
537
+ WHERE card_id = ? ORDER BY sequence DESC LIMIT 1`,
538
+ [reference]
539
+ );
540
+ if (!rows.ok) return rows;
541
+ let row = rows.data.rows[0];
542
+ if (!row) {
543
+ const matches = await output.query(
544
+ `SELECT card_id, revision, issuance_id FROM feed_cards
545
+ WHERE substr(card_id, 1, length(?)) = ? AND sequence IN
546
+ (SELECT MAX(sequence) FROM feed_cards GROUP BY card_id) LIMIT 2`,
547
+ [reference, reference]
548
+ );
549
+ if (!matches.ok) return matches;
550
+ if (matches.data.rows.length !== 1) return feedError("NOT_FOUND", "Card reference is absent or ambiguous; use its full ID");
551
+ row = matches.data.rows[0];
552
+ }
553
+ return ok({ cardId: String(row[0]), revision: Number(row[1]), issuanceId: String(row[2]) });
554
+ };
555
+
556
+ // src/feed/handoff.ts
557
+ var initializeFeedHandoff = async (handoff) => {
558
+ const result = await handoff.execute(`CREATE TABLE IF NOT EXISTS feed_handoffs (
559
+ response_id TEXT NOT NULL, recipient TEXT NOT NULL, response_json TEXT NOT NULL,
560
+ state TEXT NOT NULL CHECK (state IN ('prepared', 'unconfirmed', 'delivered')),
561
+ PRIMARY KEY (response_id, recipient)
562
+ )`);
563
+ return result.ok ? ok(void 0) : result;
564
+ };
565
+ var getFeedHandoff = async (handoff, responseId, recipient) => {
566
+ const result = await handoff.query(
567
+ "SELECT response_json, state FROM feed_handoffs WHERE response_id = ? AND recipient = ?",
568
+ [responseId, recipient]
569
+ );
570
+ if (!result.ok) return result;
571
+ const row = result.data.rows[0];
572
+ if (!row) return ok(null);
573
+ try {
574
+ return ok({ responseId, recipient, response: projectFeedResponseNotification(JSON.parse(row[0])), state: row[1] });
575
+ } catch {
576
+ return feedError("INVALID_INPUT", "Handoff contains invalid response JSON");
577
+ }
578
+ };
579
+ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver }) => {
580
+ const response = projectFeedResponseNotification(input);
581
+ if (!recipient || !response.responseId) return feedError("INVALID_INPUT", "Handoff requires a recipient and full response ID");
582
+ const inserted = await handoff.execute(
583
+ `INSERT INTO feed_handoffs (response_id, recipient, response_json, state) VALUES (?, ?, ?, 'prepared')
584
+ ON CONFLICT (response_id, recipient) DO NOTHING`,
585
+ [response.responseId, recipient, feedJson(response)]
586
+ );
587
+ if (!inserted.ok) return inserted;
588
+ const existing = await getFeedHandoff(handoff, response.responseId, recipient);
589
+ if (!existing.ok) return existing;
590
+ if (!existing.data || feedJson(existing.data.response) !== feedJson(response)) {
591
+ return feedError("INVALID_INPUT", "A response handoff identity cannot change its payload");
592
+ }
593
+ if (existing.data.state !== "prepared") return ok(existing.data);
594
+ const claimed = await handoff.execute(
595
+ "UPDATE feed_handoffs SET state = 'unconfirmed' WHERE response_id = ? AND recipient = ? AND state = 'prepared'",
596
+ [response.responseId, recipient]
597
+ );
598
+ if (!claimed.ok) return claimed;
599
+ if (claimed.data.changes === 0) {
600
+ const current = await getFeedHandoff(handoff, response.responseId, recipient);
601
+ return current.ok && current.data ? ok(current.data) : current.ok ? feedError("NOT_FOUND", "Handoff disappeared before delivery") : current;
602
+ }
603
+ let state = "unconfirmed";
604
+ try {
605
+ const delivered = await deliver(response);
606
+ if (delivered === "delivered") state = "delivered";
607
+ } catch {
608
+ }
609
+ if (state === "delivered") {
610
+ const saved = await handoff.execute(
611
+ "UPDATE feed_handoffs SET state = 'delivered' WHERE response_id = ? AND recipient = ? AND state = 'unconfirmed'",
612
+ [response.responseId, recipient]
613
+ );
614
+ if (!saved.ok) return saved;
615
+ }
616
+ return ok({ ...existing.data, state });
617
+ };
618
+
619
+ // src/feed/listener.ts
620
+ var listenFeedResponses = async (options) => {
621
+ const { signal } = options;
622
+ const renewalSeconds = options.renewalSeconds ?? 300;
623
+ if (!Number.isInteger(renewalSeconds) || renewalSeconds < 60 || renewalSeconds > 300) {
624
+ throw new Error("Feed renewal must be between 60 and 300 seconds");
625
+ }
626
+ if (options.subscription.service !== "sql" || !options.subscription.pathPrefix) {
627
+ throw new Error("Feed listener requires the exact response SQL resource subscription");
628
+ }
629
+ if (signal.aborted) return;
630
+ const initialized = await initializeFeedHandoff(options.handoff);
631
+ if (!initialized.ok) throw initialized.error;
632
+ let ready = false;
633
+ let dirty = false;
634
+ let draining;
635
+ let failed = false;
636
+ let failure;
637
+ let current;
638
+ const drain = async () => {
639
+ do {
640
+ dirty = false;
641
+ let afterSequence = 0;
642
+ while (!signal.aborted) {
643
+ const page = await listFeedResponses(options.responses, { afterSequence, limit: 100, signal });
644
+ if (!page.ok) {
645
+ if (signal.aborted && page.error.code === ErrorCodes.ABORTED) return;
646
+ throw page.error;
647
+ }
648
+ for (const response of page.data) {
649
+ if (signal.aborted) return;
650
+ const handoff = await deliverFeedResponse({
651
+ handoff: options.handoff,
652
+ recipient: options.recipient,
653
+ response,
654
+ deliver: options.deliver
655
+ });
656
+ if (!handoff.ok) throw handoff.error;
657
+ afterSequence = response.sequence;
658
+ }
659
+ if (page.data.length < 100) break;
660
+ }
661
+ } while (dirty && !signal.aborted);
662
+ };
663
+ const schedule = () => {
664
+ if (!ready || signal.aborted || failed) return;
665
+ dirty = true;
666
+ if (draining) return;
667
+ draining = drain().catch((error) => {
668
+ failed = true;
669
+ failure = error;
670
+ current?.abort();
671
+ }).finally(() => {
672
+ draining = void 0;
673
+ if (dirty && !failed) schedule();
674
+ });
675
+ };
676
+ while (!signal.aborted && !failed) {
677
+ const connection = new AbortController();
678
+ current = connection;
679
+ const stop = () => connection.abort();
680
+ signal.addEventListener("abort", stop, { once: true });
681
+ const renewal = setTimeout(stop, renewalSeconds * 1e3);
682
+ renewal.unref?.();
683
+ try {
684
+ for await (const _event of options.hooks.subscribe([options.subscription], {
685
+ signal: connection.signal,
686
+ ttlSeconds: renewalSeconds,
687
+ onReady: () => {
688
+ if (current !== connection || connection.signal.aborted || signal.aborted) return;
689
+ ready = true;
690
+ schedule();
691
+ }
692
+ })) schedule();
693
+ } finally {
694
+ ready = false;
695
+ clearTimeout(renewal);
696
+ signal.removeEventListener("abort", stop);
697
+ await draining;
698
+ }
699
+ if (failed) throw failure;
700
+ if (!connection.signal.aborted) return;
701
+ }
702
+ if (failed) throw failure;
703
+ };
704
+
705
+ // src/feed/resources.ts
706
+ var feedResourceNames = (prefix, stableToken) => {
707
+ if (!/^[A-Za-z0-9_-]+$/.test(stableToken) || !prefix || prefix.endsWith("/") || prefix.split("/").some((part) => !part || part === "." || part === ".." || !/^[A-Za-z0-9._-]+$/.test(part))) {
708
+ return feedError("INVALID_INPUT", "Feed resources require a canonical prefix and a stable alphanumeric, underscore or hyphen agent token");
709
+ }
710
+ return ok({
711
+ outputResource: `${prefix}/feed_output_${stableToken}`,
712
+ responseResource: `${prefix}/feed_responses_${stableToken}`
713
+ });
714
+ };
715
+ export {
716
+ appendFeedMessage,
717
+ appendFeedResponse,
718
+ deliverFeedResponse,
719
+ feedCardSchema,
720
+ feedHandlingSchema,
721
+ feedResourceNames,
722
+ feedResponsePayloadSchema,
723
+ getFeedCardState,
724
+ getFeedHandoff,
725
+ initializeFeedDatabases,
726
+ initializeFeedHandoff,
727
+ listFeedCards,
728
+ listFeedResponses,
729
+ listenFeedResponses,
730
+ newFeedMessageSchema,
731
+ newFeedResponseSchema,
732
+ projectFeedResponseNotification,
733
+ publishFeedCard,
734
+ readFeedResponseHandling,
735
+ readFeedResponsePayload,
736
+ recordFeedHandling,
737
+ resolveFeedCard
738
+ };
739
+ //# sourceMappingURL=index.js.map