@tinycloud/sdk-services 3.8.0-beta.9 → 3.8.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/dist/{BaseService-DO7vZ67k.d.ts → BaseService-Ma2MwJAd.d.ts} +1 -1
- package/dist/{BaseService-D-29xShG.d.cts → BaseService-O0iyuFLk.d.cts} +1 -1
- package/dist/IHooksService-4QNODVy6.d.cts +120 -0
- package/dist/IHooksService-CvTpJbQD.d.ts +120 -0
- package/dist/ISQLService-Aly1CBsW.d.ts +242 -0
- package/dist/ISQLService-BOgWzHLT.d.cts +242 -0
- package/dist/context.d.cts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/encryption/index.cjs +80 -7
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +23 -11
- package/dist/encryption/index.d.ts +23 -11
- package/dist/encryption/index.js +79 -7
- package/dist/encryption/index.js.map +1 -1
- package/dist/encryption/named-keys.cjs.map +1 -1
- package/dist/encryption/named-keys.d.cts +2 -2
- package/dist/encryption/named-keys.d.ts +2 -2
- package/dist/encryption/named-keys.js.map +1 -1
- package/dist/encryption/resource.cjs.map +1 -1
- package/dist/encryption/resource.js.map +1 -1
- package/dist/feed/index.cjs +1309 -0
- package/dist/feed/index.cjs.map +1 -0
- package/dist/feed/index.d.cts +238 -0
- package/dist/feed/index.d.ts +238 -0
- package/dist/feed/index.js +1258 -0
- package/dist/feed/index.js.map +1 -0
- package/dist/index.cjs +131 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -116
- package/dist/index.d.ts +16 -116
- package/dist/index.js +130 -15
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +4 -4
- package/dist/kv/index.d.ts +4 -4
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/index.d.cts +3 -3
- package/dist/kv/replication/index.d.ts +3 -3
- package/dist/{scope-1-8skVDk.d.ts → scope-BZr0QwxX.d.ts} +1 -1
- package/dist/{scope-Dv3IAXMr.d.cts → scope-DEgc9f-W.d.cts} +1 -1
- package/dist/sql/index.cjs +21 -6
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +6 -236
- package/dist/sql/index.d.ts +6 -236
- package/dist/sql/index.js +21 -6
- package/dist/sql/index.js.map +1 -1
- package/dist/telemetry/requestLog.d.cts +1 -1
- package/dist/telemetry/requestLog.d.ts +1 -1
- package/dist/{types-BGRzS1ob.d.ts → types-Bf91gup6.d.ts} +7 -2
- package/dist/{types-DAOyIbXu.d.cts → types-CSd0RYir.d.cts} +1 -1
- package/dist/{types-DAOyIbXu.d.ts → types-CSd0RYir.d.ts} +1 -1
- package/dist/{types-B_rbS4XE.d.cts → types-D4XbIfFJ.d.cts} +7 -2
- package/package.json +6 -1
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/feed/index.ts
|
|
21
|
+
var feed_exports = {};
|
|
22
|
+
__export(feed_exports, {
|
|
23
|
+
appendFeedMessage: () => appendFeedMessage,
|
|
24
|
+
appendFeedResponse: () => appendFeedResponse,
|
|
25
|
+
deliverFeedResponse: () => deliverFeedResponse,
|
|
26
|
+
feedCardSchema: () => feedCardSchema,
|
|
27
|
+
feedHandlingSchema: () => feedHandlingSchema,
|
|
28
|
+
feedResourceNames: () => feedResourceNames,
|
|
29
|
+
feedResponsePayloadSchema: () => feedResponsePayloadSchema,
|
|
30
|
+
getFeedCardState: () => getFeedCardState,
|
|
31
|
+
getFeedHandoff: () => getFeedHandoff,
|
|
32
|
+
initializeFeedDatabases: () => initializeFeedDatabases,
|
|
33
|
+
initializeFeedHandoff: () => initializeFeedHandoff,
|
|
34
|
+
listFeedCardHistory: () => listFeedCardHistory,
|
|
35
|
+
listFeedCards: () => listFeedCards,
|
|
36
|
+
listFeedResponseDeliveries: () => listFeedResponseDeliveries,
|
|
37
|
+
listFeedResponses: () => listFeedResponses,
|
|
38
|
+
listenFeedResponses: () => listenFeedResponses,
|
|
39
|
+
newFeedMessageSchema: () => newFeedMessageSchema,
|
|
40
|
+
newFeedResponseSchema: () => newFeedResponseSchema,
|
|
41
|
+
projectFeedResponseNotification: () => projectFeedResponseNotification,
|
|
42
|
+
publishFeedCard: () => publishFeedCard,
|
|
43
|
+
readFeedResponseHandling: () => readFeedResponseHandling,
|
|
44
|
+
readFeedResponsePayload: () => readFeedResponsePayload,
|
|
45
|
+
recordFeedHandling: () => recordFeedHandling,
|
|
46
|
+
resolveFeedCard: () => resolveFeedCard,
|
|
47
|
+
restoreFeedRecords: () => restoreFeedRecords
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(feed_exports);
|
|
50
|
+
|
|
51
|
+
// src/types.ts
|
|
52
|
+
var ErrorCodes = {
|
|
53
|
+
// Common errors
|
|
54
|
+
NOT_FOUND: "NOT_FOUND",
|
|
55
|
+
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
56
|
+
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
57
|
+
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
58
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
59
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
60
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
61
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
62
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
63
|
+
TIMEOUT: "TIMEOUT",
|
|
64
|
+
ABORTED: "ABORTED",
|
|
65
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
66
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
67
|
+
// Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
|
|
68
|
+
// retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
|
|
69
|
+
/** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
|
|
70
|
+
NODE_BUSY: "NODE_BUSY",
|
|
71
|
+
/** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
|
|
72
|
+
REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
|
|
73
|
+
/** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
|
|
74
|
+
OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
|
|
75
|
+
// KV-specific errors
|
|
76
|
+
KV_NOT_FOUND: "KV_NOT_FOUND",
|
|
77
|
+
KV_WRITE_FAILED: "KV_WRITE_FAILED",
|
|
78
|
+
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
79
|
+
KV_CONFLICT: "KV_CONFLICT",
|
|
80
|
+
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
81
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
82
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
83
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
84
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
85
|
+
// SQL-specific errors
|
|
86
|
+
SQL_ERROR: "SQL_ERROR",
|
|
87
|
+
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
88
|
+
SQL_DATABASE_NOT_FOUND: "SQL_DATABASE_NOT_FOUND",
|
|
89
|
+
SQL_RESPONSE_TOO_LARGE: "SQL_RESPONSE_TOO_LARGE",
|
|
90
|
+
SQL_QUOTA_EXCEEDED: "SQL_QUOTA_EXCEEDED",
|
|
91
|
+
SQL_INVALID_STATEMENT: "SQL_INVALID_STATEMENT",
|
|
92
|
+
SQL_SCHEMA_ERROR: "SQL_SCHEMA_ERROR",
|
|
93
|
+
SQL_READONLY_VIOLATION: "SQL_READONLY_VIOLATION",
|
|
94
|
+
// Storage quota errors
|
|
95
|
+
STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
|
|
96
|
+
STORAGE_LIMIT_REACHED: "STORAGE_LIMIT_REACHED",
|
|
97
|
+
/** The node answered the usage read without usage numbers (it predates the read). */
|
|
98
|
+
STORAGE_STATUS_UNAVAILABLE: "STORAGE_STATUS_UNAVAILABLE",
|
|
99
|
+
// DuckDB-specific errors
|
|
100
|
+
DUCKDB_ERROR: "DUCKDB_ERROR",
|
|
101
|
+
DUCKDB_PERMISSION_DENIED: "DUCKDB_PERMISSION_DENIED",
|
|
102
|
+
DUCKDB_DATABASE_NOT_FOUND: "DUCKDB_DATABASE_NOT_FOUND",
|
|
103
|
+
DUCKDB_RESPONSE_TOO_LARGE: "DUCKDB_RESPONSE_TOO_LARGE",
|
|
104
|
+
DUCKDB_QUOTA_EXCEEDED: "DUCKDB_QUOTA_EXCEEDED",
|
|
105
|
+
DUCKDB_INVALID_STATEMENT: "DUCKDB_INVALID_STATEMENT",
|
|
106
|
+
DUCKDB_SCHEMA_ERROR: "DUCKDB_SCHEMA_ERROR",
|
|
107
|
+
DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
|
|
108
|
+
};
|
|
109
|
+
var defaultRetryPolicy = {
|
|
110
|
+
maxAttempts: 4,
|
|
111
|
+
backoff: "exponential",
|
|
112
|
+
baseDelayMs: 250,
|
|
113
|
+
maxDelayMs: 4e3,
|
|
114
|
+
retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
|
|
115
|
+
maxTotalMs: 12e3
|
|
116
|
+
};
|
|
117
|
+
function ok(data) {
|
|
118
|
+
return { ok: true, data };
|
|
119
|
+
}
|
|
120
|
+
function err(error) {
|
|
121
|
+
return { ok: false, error };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/feed/schemas.ts
|
|
125
|
+
var import_zod = require("zod");
|
|
126
|
+
function isJSONValue(value, active = /* @__PURE__ */ new Set()) {
|
|
127
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return true;
|
|
128
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
129
|
+
if (typeof value !== "object" || active.has(value)) return false;
|
|
130
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
|
|
131
|
+
active.add(value);
|
|
132
|
+
let valid = true;
|
|
133
|
+
for (const child of Array.isArray(value) ? value : Object.values(value)) {
|
|
134
|
+
if (!isJSONValue(child, active)) {
|
|
135
|
+
valid = false;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
active.delete(value);
|
|
140
|
+
return valid;
|
|
141
|
+
}
|
|
142
|
+
var jsonValueSchema = import_zod.z.custom((value) => isJSONValue(value));
|
|
143
|
+
var feedResponsePayloadSchema = import_zod.z.object({
|
|
144
|
+
type: import_zod.z.string().min(1),
|
|
145
|
+
version: import_zod.z.number().int().positive(),
|
|
146
|
+
data: jsonValueSchema
|
|
147
|
+
}).strict();
|
|
148
|
+
var identity = {
|
|
149
|
+
cardId: import_zod.z.string().min(1),
|
|
150
|
+
revision: import_zod.z.number().int().positive(),
|
|
151
|
+
issuanceId: import_zod.z.string().min(1)
|
|
152
|
+
};
|
|
153
|
+
var feedCardSchema = import_zod.z.object({
|
|
154
|
+
...identity,
|
|
155
|
+
kind: import_zod.z.string().min(1),
|
|
156
|
+
needsResponse: import_zod.z.boolean(),
|
|
157
|
+
title: import_zod.z.string(),
|
|
158
|
+
body: import_zod.z.string(),
|
|
159
|
+
choices: import_zod.z.array(import_zod.z.object({ id: import_zod.z.string().min(1), label: import_zod.z.string() })).optional(),
|
|
160
|
+
urgency: import_zod.z.number().finite(),
|
|
161
|
+
createdAt: import_zod.z.string().min(1),
|
|
162
|
+
payload: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
163
|
+
});
|
|
164
|
+
var newFeedResponseSchema = import_zod.z.object({
|
|
165
|
+
...identity,
|
|
166
|
+
responseId: import_zod.z.string().min(1),
|
|
167
|
+
kind: import_zod.z.enum(["answer", "followup"]),
|
|
168
|
+
choiceId: import_zod.z.string().min(1).optional(),
|
|
169
|
+
text: import_zod.z.string().optional(),
|
|
170
|
+
supersedesResponseId: import_zod.z.string().min(1).optional(),
|
|
171
|
+
createdAt: import_zod.z.string().min(1)
|
|
172
|
+
});
|
|
173
|
+
var newFeedMessageSchema = import_zod.z.object({
|
|
174
|
+
...identity,
|
|
175
|
+
responseId: import_zod.z.string().min(1),
|
|
176
|
+
kind: import_zod.z.literal("message"),
|
|
177
|
+
text: import_zod.z.string(),
|
|
178
|
+
createdAt: import_zod.z.string().min(1)
|
|
179
|
+
}).strict();
|
|
180
|
+
var feedResponseHeaderSchema = import_zod.z.union([newFeedResponseSchema, newFeedMessageSchema]);
|
|
181
|
+
var feedHandlingSchema = import_zod.z.object({
|
|
182
|
+
...identity,
|
|
183
|
+
handlingId: import_zod.z.string().min(1),
|
|
184
|
+
responseId: import_zod.z.string().min(1),
|
|
185
|
+
phase: import_zod.z.enum(["acknowledged", "succeeded", "failed"]),
|
|
186
|
+
createdAt: import_zod.z.string().min(1),
|
|
187
|
+
detail: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// src/feed/dispatch.ts
|
|
191
|
+
var import_zod2 = require("zod");
|
|
192
|
+
var timestamp = import_zod2.z.string().datetime({ offset: true });
|
|
193
|
+
function validDispatchBefore(value) {
|
|
194
|
+
return value === null || typeof value === "string" && timestamp.safeParse(value).success && Number.isFinite(Date.parse(value));
|
|
195
|
+
}
|
|
196
|
+
function dispatchDeadlineDenied(response) {
|
|
197
|
+
const cutoff = response.dispatchBefore;
|
|
198
|
+
return cutoff !== void 0 && (!validDispatchBefore(cutoff) || typeof cutoff === "string" && Date.now() >= Date.parse(cutoff));
|
|
199
|
+
}
|
|
200
|
+
function dispatchDeadlineFailure() {
|
|
201
|
+
return err({ code: ErrorCodes.PERMISSION_DENIED, service: "feed", message: "The response dispatch deadline does not permit a new handoff.", meta: { reason: "dispatch_deadline" } });
|
|
202
|
+
}
|
|
203
|
+
function isDispatchDeadlineFailure(error) {
|
|
204
|
+
return error.service === "feed" && error.code === ErrorCodes.PERMISSION_DENIED && error.meta?.reason === "dispatch_deadline";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/feed/notification.ts
|
|
208
|
+
var projectFeedResponseNotification = (response) => {
|
|
209
|
+
const notification = {
|
|
210
|
+
responseId: response.responseId,
|
|
211
|
+
cardId: response.cardId,
|
|
212
|
+
revision: response.revision,
|
|
213
|
+
issuanceId: response.issuanceId,
|
|
214
|
+
kind: response.kind,
|
|
215
|
+
createdAt: response.createdAt,
|
|
216
|
+
sequence: response.sequence
|
|
217
|
+
};
|
|
218
|
+
if (response.kind !== "message" && response.choiceId !== void 0) notification.choiceId = response.choiceId;
|
|
219
|
+
if (response.text !== void 0) notification.text = response.text;
|
|
220
|
+
if (response.kind !== "message" && response.supersedesResponseId !== void 0) notification.supersedesResponseId = response.supersedesResponseId;
|
|
221
|
+
if (response.payloadType !== void 0) notification.payloadType = response.payloadType;
|
|
222
|
+
if (response.payloadVersion !== void 0) notification.payloadVersion = response.payloadVersion;
|
|
223
|
+
if (validDispatchBefore(response.dispatchBefore)) notification.dispatchBefore = response.dispatchBefore;
|
|
224
|
+
return notification;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/feed/store.ts
|
|
228
|
+
var PRIVATE_RESPONSE_OPTIONS = { redactErrors: true };
|
|
229
|
+
function feedError(code, message) {
|
|
230
|
+
return err({ code, message, service: "feed" });
|
|
231
|
+
}
|
|
232
|
+
function feedJson(value) {
|
|
233
|
+
return JSON.stringify(value, (_key, item) => {
|
|
234
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
235
|
+
const sorted = /* @__PURE__ */ Object.create(null);
|
|
236
|
+
for (const key of Object.keys(item).sort()) sorted[key] = item[key];
|
|
237
|
+
return sorted;
|
|
238
|
+
}
|
|
239
|
+
return item;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function validIdentity(identity2) {
|
|
243
|
+
return typeof identity2.cardId === "string" && identity2.cardId.length > 0 && Number.isSafeInteger(identity2.revision) && identity2.revision > 0 && typeof identity2.issuanceId === "string" && identity2.issuanceId.length > 0;
|
|
244
|
+
}
|
|
245
|
+
function sameFeedIdentity(a, b) {
|
|
246
|
+
return a.cardId === b.cardId && a.revision === b.revision && a.issuanceId === b.issuanceId;
|
|
247
|
+
}
|
|
248
|
+
async function jsonRows(db, sql, params = [], options) {
|
|
249
|
+
const result = await db.query(sql, params, options);
|
|
250
|
+
if (!result.ok) return result;
|
|
251
|
+
try {
|
|
252
|
+
return ok(result.data.rows.map((row) => JSON.parse(row[0])));
|
|
253
|
+
} catch {
|
|
254
|
+
return feedError("INVALID_INPUT", "Feed record contains invalid JSON");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
var RESPONSE_HEADER_SELECT = `SELECT r.sequence, r.response_json, p.payload_type, p.payload_version
|
|
258
|
+
FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id`;
|
|
259
|
+
function responseDataFailure(message, error) {
|
|
260
|
+
let code = ErrorCodes.SQL_ERROR;
|
|
261
|
+
if (error !== null && typeof error === "object" && "code" in error) {
|
|
262
|
+
const candidate = error.code;
|
|
263
|
+
if (typeof candidate === "string" && Object.values(ErrorCodes).some((known) => known === candidate)) code = candidate;
|
|
264
|
+
}
|
|
265
|
+
return feedError(code, message);
|
|
266
|
+
}
|
|
267
|
+
function responseFromRow(row) {
|
|
268
|
+
return responseFromHeader(row, feedResponseHeaderSchema.parse(JSON.parse(String(row[1]))));
|
|
269
|
+
}
|
|
270
|
+
function responseFromHeader(row, header) {
|
|
271
|
+
const response = { ...header, sequence: Number(row[0]) };
|
|
272
|
+
if (row[2] !== null && row[2] !== void 0) {
|
|
273
|
+
response.payloadType = String(row[2]);
|
|
274
|
+
response.payloadVersion = Number(row[3]);
|
|
275
|
+
}
|
|
276
|
+
return projectFeedResponseNotification(response);
|
|
277
|
+
}
|
|
278
|
+
async function storedResponse(db, responseId, options = PRIVATE_RESPONSE_OPTIONS) {
|
|
279
|
+
try {
|
|
280
|
+
const result = await db.query(`SELECT r.sequence, r.response_json, p.payload_type, p.payload_version, p.payload_json
|
|
281
|
+
FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
|
|
282
|
+
WHERE r.response_id = ?`, [responseId], options);
|
|
283
|
+
if (!result.ok) return responseDataFailure("Could not read the saved feed response", result.error);
|
|
284
|
+
const row = result.data.rows[0];
|
|
285
|
+
if (!row) return ok(void 0);
|
|
286
|
+
const header = feedResponseHeaderSchema.parse(JSON.parse(String(row[1])));
|
|
287
|
+
const response = responseFromHeader(row, header);
|
|
288
|
+
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])) });
|
|
289
|
+
return ok({ response, rawHeaderJson: String(row[1]), headerJson: feedJson(header), payload });
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return responseDataFailure("Could not read the saved feed response", error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
var initializeFeedDatabases = async ({ output, responses }) => {
|
|
295
|
+
const initialized = await output.batch([
|
|
296
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_cards (
|
|
297
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
298
|
+
card_id TEXT NOT NULL, revision INTEGER NOT NULL CHECK (revision > 0), issuance_id TEXT NOT NULL,
|
|
299
|
+
card_json TEXT NOT NULL, UNIQUE (card_id, revision, issuance_id)
|
|
300
|
+
)` },
|
|
301
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_handling (
|
|
302
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT, handling_id TEXT NOT NULL UNIQUE,
|
|
303
|
+
response_id TEXT NOT NULL, card_id TEXT NOT NULL, revision INTEGER NOT NULL, issuance_id TEXT NOT NULL,
|
|
304
|
+
phase TEXT NOT NULL CHECK (phase IN ('acknowledged', 'succeeded', 'failed')),
|
|
305
|
+
handling_json TEXT NOT NULL
|
|
306
|
+
)` },
|
|
307
|
+
{ sql: "CREATE INDEX IF NOT EXISTS feed_handling_response ON feed_handling (response_id, sequence)" }
|
|
308
|
+
]);
|
|
309
|
+
if (!initialized.ok) return initialized;
|
|
310
|
+
const responseSchema = await responses.batch([
|
|
311
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_responses (
|
|
312
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT, response_id TEXT NOT NULL UNIQUE,
|
|
313
|
+
card_id TEXT NOT NULL, revision INTEGER NOT NULL, issuance_id TEXT NOT NULL,
|
|
314
|
+
response_json TEXT NOT NULL
|
|
315
|
+
)` },
|
|
316
|
+
{ sql: "CREATE INDEX IF NOT EXISTS feed_responses_card ON feed_responses (card_id, revision, issuance_id, sequence)" },
|
|
317
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_response_payloads (
|
|
318
|
+
response_id TEXT PRIMARY KEY, payload_type TEXT NOT NULL, payload_version INTEGER NOT NULL, payload_json TEXT NOT NULL
|
|
319
|
+
)` },
|
|
320
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_response_imports (
|
|
321
|
+
response_id TEXT PRIMARY KEY, card_id TEXT NOT NULL, revision INTEGER NOT NULL, issuance_id TEXT NOT NULL,
|
|
322
|
+
content_sha256 TEXT NOT NULL
|
|
323
|
+
)` },
|
|
324
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_response_origins (
|
|
325
|
+
response_id TEXT NOT NULL, source_json TEXT NOT NULL, PRIMARY KEY (response_id, source_json)
|
|
326
|
+
)` },
|
|
327
|
+
{ sql: `CREATE TABLE IF NOT EXISTS feed_response_delivery_policies (
|
|
328
|
+
response_id TEXT NOT NULL, recipient TEXT NOT NULL, policy_json TEXT NOT NULL,
|
|
329
|
+
PRIMARY KEY (response_id, recipient, policy_json)
|
|
330
|
+
)` }
|
|
331
|
+
]);
|
|
332
|
+
return responseSchema.ok ? ok(void 0) : responseSchema;
|
|
333
|
+
};
|
|
334
|
+
var publishFeedCard = (output, card) => storeFeedCard(output, card);
|
|
335
|
+
async function storeFeedCard(output, card, options) {
|
|
336
|
+
if (!validIdentity(card)) return feedError("INVALID_INPUT", "A card requires its full ID, positive revision and issuance ID");
|
|
337
|
+
const encoded = feedJson(card);
|
|
338
|
+
const inserted = await output.execute(
|
|
339
|
+
`INSERT INTO feed_cards (card_id, revision, issuance_id, card_json) VALUES (?, ?, ?, ?)
|
|
340
|
+
ON CONFLICT (card_id, revision, issuance_id) DO NOTHING`,
|
|
341
|
+
[card.cardId, card.revision, card.issuanceId, encoded],
|
|
342
|
+
options
|
|
343
|
+
);
|
|
344
|
+
if (!inserted.ok) return inserted;
|
|
345
|
+
const stored = await jsonRows(
|
|
346
|
+
output,
|
|
347
|
+
"SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
|
|
348
|
+
[card.cardId, card.revision, card.issuanceId],
|
|
349
|
+
options
|
|
350
|
+
);
|
|
351
|
+
if (!stored.ok) return stored;
|
|
352
|
+
if (!stored.data[0] || feedJson(stored.data[0]) !== encoded) {
|
|
353
|
+
return feedError("INVALID_INPUT", "A published card identity cannot be silently changed");
|
|
354
|
+
}
|
|
355
|
+
return ok(stored.data[0]);
|
|
356
|
+
}
|
|
357
|
+
var listFeedCards = async (output) => {
|
|
358
|
+
return jsonRows(output, `SELECT card_json FROM feed_cards
|
|
359
|
+
WHERE sequence IN (SELECT MAX(sequence) FROM feed_cards GROUP BY card_id) ORDER BY sequence DESC`);
|
|
360
|
+
};
|
|
361
|
+
var appendFeedResponse = async (output, responses, input, payload) => {
|
|
362
|
+
try {
|
|
363
|
+
const header = newFeedResponseSchema.safeParse(input);
|
|
364
|
+
if (!header.success) return feedError("INVALID_INPUT", "Invalid feed response header");
|
|
365
|
+
return appendResponse(output, responses, header.data, payload);
|
|
366
|
+
} catch {
|
|
367
|
+
return feedError("INVALID_INPUT", "Invalid feed response header");
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var appendFeedMessage = async (output, responses, input, payload) => {
|
|
371
|
+
try {
|
|
372
|
+
const header = newFeedMessageSchema.safeParse(input);
|
|
373
|
+
if (!header.success) return feedError("INVALID_INPUT", "Invalid feed message header");
|
|
374
|
+
return appendResponse(output, responses, header.data, payload);
|
|
375
|
+
} catch {
|
|
376
|
+
return feedError("INVALID_INPUT", "Invalid feed message header");
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
async function appendResponse(output, responses, response, inputPayload) {
|
|
380
|
+
let payload;
|
|
381
|
+
try {
|
|
382
|
+
if (inputPayload !== void 0) {
|
|
383
|
+
const parsed = feedResponsePayloadSchema.safeParse(inputPayload);
|
|
384
|
+
if (!parsed.success) return feedError("INVALID_INPUT", "Invalid private feed response payload");
|
|
385
|
+
payload = parsed.data;
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
return feedError("INVALID_INPUT", "Invalid feed response or private payload");
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const cards = await jsonRows(
|
|
392
|
+
output,
|
|
393
|
+
"SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
|
|
394
|
+
[response.cardId, response.revision, response.issuanceId],
|
|
395
|
+
PRIVATE_RESPONSE_OPTIONS
|
|
396
|
+
);
|
|
397
|
+
if (!cards.ok) return responseDataFailure("Could not verify the response card issuance", cards.error);
|
|
398
|
+
const card = cards.data[0];
|
|
399
|
+
if (!card || !sameFeedIdentity(card, response)) {
|
|
400
|
+
return feedError("INVALID_INPUT", "The response does not identify a published card issuance");
|
|
401
|
+
}
|
|
402
|
+
if (response.kind !== "message") {
|
|
403
|
+
if (!card.needsResponse) {
|
|
404
|
+
return feedError("INVALID_INPUT", "The response does not identify a published actionable card issuance");
|
|
405
|
+
}
|
|
406
|
+
if (response.choiceId !== void 0 && !card.choices?.some((choice) => choice.id === response.choiceId)) {
|
|
407
|
+
return feedError("INVALID_INPUT", "The response choice is not part of this card issuance");
|
|
408
|
+
}
|
|
409
|
+
if (response.supersedesResponseId) {
|
|
410
|
+
const previous = await jsonRows(
|
|
411
|
+
responses,
|
|
412
|
+
"SELECT response_json FROM feed_responses WHERE response_id = ?",
|
|
413
|
+
[response.supersedesResponseId],
|
|
414
|
+
PRIVATE_RESPONSE_OPTIONS
|
|
415
|
+
);
|
|
416
|
+
if (!previous.ok) return responseDataFailure("Could not verify the corrected response", previous.error);
|
|
417
|
+
if (!previous.data[0] || !sameFeedIdentity(previous.data[0], response)) {
|
|
418
|
+
return feedError("INVALID_INPUT", "A correction must reference a response to the same card issuance");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const encoded = feedJson(response);
|
|
423
|
+
const match = (stored) => {
|
|
424
|
+
const samePayload = payload === void 0 ? stored.payload === void 0 : stored.payload !== void 0 && feedJson(stored.payload) === feedJson(payload);
|
|
425
|
+
return stored.headerJson === encoded && samePayload ? ok(stored.response) : feedError("INVALID_INPUT", "A response ID cannot be reused for different header or private payload content");
|
|
426
|
+
};
|
|
427
|
+
const existing = await storedResponse(responses, response.responseId);
|
|
428
|
+
if (!existing.ok) return existing;
|
|
429
|
+
if (existing.data) return match(existing.data);
|
|
430
|
+
const statements = [{
|
|
431
|
+
sql: "INSERT INTO feed_responses (response_id, card_id, revision, issuance_id, response_json) VALUES (?, ?, ?, ?, ?)",
|
|
432
|
+
params: [response.responseId, response.cardId, response.revision, response.issuanceId, encoded]
|
|
433
|
+
}];
|
|
434
|
+
if (payload !== void 0) statements.push({
|
|
435
|
+
sql: "INSERT INTO feed_response_payloads (response_id, payload_type, payload_version, payload_json) VALUES (?, ?, ?, ?)",
|
|
436
|
+
params: [response.responseId, payload.type, payload.version, feedJson(payload.data)]
|
|
437
|
+
});
|
|
438
|
+
let failed = false;
|
|
439
|
+
let failure;
|
|
440
|
+
try {
|
|
441
|
+
const saved = await responses.batch(statements, PRIVATE_RESPONSE_OPTIONS);
|
|
442
|
+
if (!saved.ok) {
|
|
443
|
+
failed = true;
|
|
444
|
+
failure = saved.error;
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
failed = true;
|
|
448
|
+
failure = error;
|
|
449
|
+
}
|
|
450
|
+
const committed = await storedResponse(responses, response.responseId);
|
|
451
|
+
if (!committed.ok) return failed ? responseDataFailure("Could not save the feed response and private payload", failure) : committed;
|
|
452
|
+
if (!committed.data) return responseDataFailure("Could not save the feed response and private payload", failure);
|
|
453
|
+
return match(committed.data);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return responseDataFailure("Could not save the feed response and private payload", error);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
var readFeedResponsePayload = async (responses, identity2) => {
|
|
459
|
+
if (!validIdentity(identity2) || typeof identity2.responseId !== "string" || !identity2.responseId) {
|
|
460
|
+
return feedError("INVALID_INPUT", "A private payload read requires the full response and card identity");
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const result = await responses.query(
|
|
464
|
+
`SELECT p.payload_type, p.payload_version, p.payload_json
|
|
465
|
+
FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
|
|
466
|
+
WHERE r.response_id = ? AND r.card_id = ? AND r.revision = ? AND r.issuance_id = ?`,
|
|
467
|
+
[identity2.responseId, identity2.cardId, identity2.revision, identity2.issuanceId],
|
|
468
|
+
PRIVATE_RESPONSE_OPTIONS
|
|
469
|
+
);
|
|
470
|
+
if (!result.ok) return responseDataFailure("Could not read the private feed response payload", result.error);
|
|
471
|
+
const row = result.data.rows[0];
|
|
472
|
+
if (!row) return feedError("NOT_FOUND", "The response identity does not match a saved response");
|
|
473
|
+
if (row[0] === null || row[0] === void 0) return ok(void 0);
|
|
474
|
+
const parsed = feedResponsePayloadSchema.safeParse({ type: row[0], version: Number(row[1]), data: JSON.parse(String(row[2])) });
|
|
475
|
+
return parsed.success ? ok(parsed.data) : feedError("INVALID_INPUT", "The saved private response payload is invalid");
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return responseDataFailure("Could not read the private feed response payload", error);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var listFeedResponses = async (responses, options = {}) => {
|
|
481
|
+
const after = options.afterSequence ?? 0;
|
|
482
|
+
const limit = options.limit ?? 100;
|
|
483
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
|
|
484
|
+
return feedError("INVALID_INPUT", "Response pagination requires a nonnegative sequence and limit between 1 and 1000");
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const result = await responses.query(
|
|
488
|
+
`${RESPONSE_HEADER_SELECT} WHERE r.sequence > ? ORDER BY r.sequence LIMIT ?`,
|
|
489
|
+
[after, limit],
|
|
490
|
+
{ ...PRIVATE_RESPONSE_OPTIONS, signal: options.signal }
|
|
491
|
+
);
|
|
492
|
+
if (!result.ok) return responseDataFailure("Could not list feed responses", result.error);
|
|
493
|
+
return ok(result.data.rows.map(responseFromRow));
|
|
494
|
+
} catch (error) {
|
|
495
|
+
return responseDataFailure("Could not list feed responses", error);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
var recordFeedHandling = async (output, handling) => {
|
|
499
|
+
if (!validIdentity(handling) || !handling.handlingId || !handling.responseId || !["acknowledged", "succeeded", "failed"].includes(handling.phase)) {
|
|
500
|
+
return feedError("INVALID_INPUT", "Handling requires full response and card identities and a known phase");
|
|
501
|
+
}
|
|
502
|
+
const previous = await jsonRows(
|
|
503
|
+
output,
|
|
504
|
+
"SELECT handling_json FROM feed_handling WHERE response_id = ? ORDER BY sequence",
|
|
505
|
+
[handling.responseId]
|
|
506
|
+
);
|
|
507
|
+
if (!previous.ok) return previous;
|
|
508
|
+
if (previous.data.some((item) => !sameFeedIdentity(item, handling))) {
|
|
509
|
+
return feedError("INVALID_INPUT", "Handling cannot move a response to another card issuance");
|
|
510
|
+
}
|
|
511
|
+
const encoded = feedJson(handling);
|
|
512
|
+
const duplicate = previous.data.find((item) => item.handlingId === handling.handlingId);
|
|
513
|
+
if (duplicate) {
|
|
514
|
+
return feedJson(duplicate) === encoded ? ok(duplicate) : feedError("INVALID_INPUT", "A handling ID cannot change content");
|
|
515
|
+
}
|
|
516
|
+
const knownOutcome = previous.data.find((item) => item.phase !== "acknowledged");
|
|
517
|
+
if (knownOutcome && handling.phase !== "acknowledged") {
|
|
518
|
+
return feedError("INVALID_INPUT", "An existing outcome requires an explicit follow-up response, not a replacement");
|
|
519
|
+
}
|
|
520
|
+
const inserted = await output.execute(
|
|
521
|
+
`INSERT INTO feed_handling (handling_id, response_id, card_id, revision, issuance_id, phase, handling_json)
|
|
522
|
+
SELECT ?, ?, ?, ?, ?, ?, ?
|
|
523
|
+
WHERE NOT EXISTS (
|
|
524
|
+
SELECT 1 FROM feed_handling WHERE response_id = ? AND (card_id <> ? OR revision <> ? OR issuance_id <> ?)
|
|
525
|
+
) AND (? = 'acknowledged' OR NOT EXISTS (
|
|
526
|
+
SELECT 1 FROM feed_handling WHERE response_id = ? AND phase IN ('succeeded', 'failed')
|
|
527
|
+
)) ON CONFLICT (handling_id) DO NOTHING`,
|
|
528
|
+
[
|
|
529
|
+
handling.handlingId,
|
|
530
|
+
handling.responseId,
|
|
531
|
+
handling.cardId,
|
|
532
|
+
handling.revision,
|
|
533
|
+
handling.issuanceId,
|
|
534
|
+
handling.phase,
|
|
535
|
+
encoded,
|
|
536
|
+
handling.responseId,
|
|
537
|
+
handling.cardId,
|
|
538
|
+
handling.revision,
|
|
539
|
+
handling.issuanceId,
|
|
540
|
+
handling.phase,
|
|
541
|
+
handling.responseId
|
|
542
|
+
]
|
|
543
|
+
);
|
|
544
|
+
if (!inserted.ok) return inserted;
|
|
545
|
+
const stored = await jsonRows(
|
|
546
|
+
output,
|
|
547
|
+
"SELECT handling_json FROM feed_handling WHERE handling_id = ?",
|
|
548
|
+
[handling.handlingId]
|
|
549
|
+
);
|
|
550
|
+
if (!stored.ok) return stored;
|
|
551
|
+
if (!stored.data[0]) return feedError("INVALID_INPUT", "Handling conflicts with an existing outcome or response issuance");
|
|
552
|
+
return feedJson(stored.data[0]) === encoded ? ok(stored.data[0]) : feedError("INVALID_INPUT", "A handling ID cannot change content");
|
|
553
|
+
};
|
|
554
|
+
async function selectResponseHandling(output, identity2) {
|
|
555
|
+
const records = await jsonRows(
|
|
556
|
+
output,
|
|
557
|
+
`SELECT handling_json FROM feed_handling WHERE response_id = ? AND card_id = ? AND revision = ? AND issuance_id = ?
|
|
558
|
+
ORDER BY sequence`,
|
|
559
|
+
[identity2.responseId, identity2.cardId, identity2.revision, identity2.issuanceId]
|
|
560
|
+
);
|
|
561
|
+
if (!records.ok) return records;
|
|
562
|
+
if (records.data.length === 0) return ok(void 0);
|
|
563
|
+
const handling = { acknowledgement: null, outcome: null };
|
|
564
|
+
for (const item of records.data) {
|
|
565
|
+
if (item.responseId !== identity2.responseId || !sameFeedIdentity(item, identity2)) {
|
|
566
|
+
return feedError("INVALID_INPUT", "Stored handling does not match the response identity");
|
|
567
|
+
}
|
|
568
|
+
if (item.phase === "acknowledged") handling.acknowledgement = item;
|
|
569
|
+
else if (item.phase === "succeeded" || item.phase === "failed") handling.outcome = item;
|
|
570
|
+
else return feedError("INVALID_INPUT", "Stored handling has an unknown phase");
|
|
571
|
+
}
|
|
572
|
+
return ok(handling);
|
|
573
|
+
}
|
|
574
|
+
var readFeedResponseHandling = async (output, identity2) => {
|
|
575
|
+
if (!validIdentity(identity2) || typeof identity2.responseId !== "string" || !identity2.responseId) {
|
|
576
|
+
return feedError("INVALID_INPUT", "Handling reads require full response and card identities");
|
|
577
|
+
}
|
|
578
|
+
const handling = await selectResponseHandling(output, identity2);
|
|
579
|
+
if (!handling.ok) return handling;
|
|
580
|
+
return handling.data ? ok(handling.data) : feedError("NOT_FOUND", "Matching response handling not found");
|
|
581
|
+
};
|
|
582
|
+
var getFeedCardState = async (output, responses, identity2) => {
|
|
583
|
+
const params = [identity2.cardId, identity2.revision, identity2.issuanceId];
|
|
584
|
+
const cards = await jsonRows(
|
|
585
|
+
output,
|
|
586
|
+
"SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
|
|
587
|
+
params
|
|
588
|
+
);
|
|
589
|
+
if (!cards.ok) return cards;
|
|
590
|
+
if (!cards.data[0]) return feedError("NOT_FOUND", "Card issuance not found");
|
|
591
|
+
const saved = await responses.query(
|
|
592
|
+
`${RESPONSE_HEADER_SELECT} WHERE r.card_id = ? AND r.revision = ? AND r.issuance_id = ?
|
|
593
|
+
AND json_extract(r.response_json, '$.kind') IN ('answer', 'followup')
|
|
594
|
+
ORDER BY r.sequence DESC LIMIT 1`,
|
|
595
|
+
params,
|
|
596
|
+
PRIVATE_RESPONSE_OPTIONS
|
|
597
|
+
);
|
|
598
|
+
if (!saved.ok) return responseDataFailure("Could not read the card's saved response", saved.error);
|
|
599
|
+
const row = saved.data.rows[0];
|
|
600
|
+
let response;
|
|
601
|
+
try {
|
|
602
|
+
response = row ? responseFromRow(row) : null;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
return responseDataFailure("Could not read the card's saved response", error);
|
|
605
|
+
}
|
|
606
|
+
const handling = response ? await selectResponseHandling(output, response) : ok(void 0);
|
|
607
|
+
if (!handling.ok) return handling;
|
|
608
|
+
return ok({
|
|
609
|
+
card: cards.data[0],
|
|
610
|
+
response,
|
|
611
|
+
acknowledgement: handling.data?.acknowledgement ?? null,
|
|
612
|
+
outcome: handling.data?.outcome ?? null
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
var resolveFeedCard = async (output, reference) => {
|
|
616
|
+
if (!reference) return feedError("INVALID_INPUT", "A card reference is required");
|
|
617
|
+
const rows = await output.query(
|
|
618
|
+
`SELECT card_id, revision, issuance_id FROM feed_cards
|
|
619
|
+
WHERE card_id = ? ORDER BY sequence DESC LIMIT 1`,
|
|
620
|
+
[reference]
|
|
621
|
+
);
|
|
622
|
+
if (!rows.ok) return rows;
|
|
623
|
+
let row = rows.data.rows[0];
|
|
624
|
+
if (!row) {
|
|
625
|
+
const matches = await output.query(
|
|
626
|
+
`SELECT card_id, revision, issuance_id FROM feed_cards
|
|
627
|
+
WHERE substr(card_id, 1, length(?)) = ? AND sequence IN
|
|
628
|
+
(SELECT MAX(sequence) FROM feed_cards GROUP BY card_id) LIMIT 2`,
|
|
629
|
+
[reference, reference]
|
|
630
|
+
);
|
|
631
|
+
if (!matches.ok) return matches;
|
|
632
|
+
if (matches.data.rows.length !== 1) return feedError("NOT_FOUND", "Card reference is absent or ambiguous; use its full ID");
|
|
633
|
+
row = matches.data.rows[0];
|
|
634
|
+
}
|
|
635
|
+
return ok({ cardId: String(row[0]), revision: Number(row[1]), issuanceId: String(row[2]) });
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/feed/handoff.ts
|
|
639
|
+
var initializeFeedHandoff = async (handoff) => {
|
|
640
|
+
const result = await handoff.execute(`CREATE TABLE IF NOT EXISTS feed_handoffs (
|
|
641
|
+
response_id TEXT NOT NULL, recipient TEXT NOT NULL, response_json TEXT NOT NULL,
|
|
642
|
+
state TEXT NOT NULL CHECK (state IN ('prepared', 'unconfirmed', 'delivered')),
|
|
643
|
+
PRIMARY KEY (response_id, recipient)
|
|
644
|
+
)`);
|
|
645
|
+
return result.ok ? ok(void 0) : result;
|
|
646
|
+
};
|
|
647
|
+
var getFeedHandoff = async (handoff, responseId, recipient) => {
|
|
648
|
+
const result = await handoff.query(
|
|
649
|
+
"SELECT response_json, state FROM feed_handoffs WHERE response_id = ? AND recipient = ?",
|
|
650
|
+
[responseId, recipient]
|
|
651
|
+
);
|
|
652
|
+
if (!result.ok) return result;
|
|
653
|
+
const row = result.data.rows[0];
|
|
654
|
+
if (!row) return ok(null);
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(row[0]);
|
|
657
|
+
if (parsed.dispatchBefore !== void 0 && !validDispatchBefore(parsed.dispatchBefore)) {
|
|
658
|
+
return feedError("INVALID_INPUT", "Handoff contains invalid dispatch metadata");
|
|
659
|
+
}
|
|
660
|
+
return ok({ responseId, recipient, response: projectFeedResponseNotification(parsed), state: row[1] });
|
|
661
|
+
} catch {
|
|
662
|
+
return feedError("INVALID_INPUT", "Handoff contains invalid response JSON");
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver }) => {
|
|
666
|
+
if (dispatchDeadlineDenied(input)) return dispatchDeadlineFailure();
|
|
667
|
+
const response = projectFeedResponseNotification(input);
|
|
668
|
+
if (!recipient || !response.responseId) return feedError("INVALID_INPUT", "Handoff requires a recipient and full response ID");
|
|
669
|
+
if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
|
|
670
|
+
const inserted = await handoff.execute(
|
|
671
|
+
`INSERT INTO feed_handoffs (response_id, recipient, response_json, state) VALUES (?, ?, ?, 'prepared')
|
|
672
|
+
ON CONFLICT (response_id, recipient) DO NOTHING`,
|
|
673
|
+
[response.responseId, recipient, feedJson(response)]
|
|
674
|
+
);
|
|
675
|
+
if (!inserted.ok) return inserted;
|
|
676
|
+
const existing = await getFeedHandoff(handoff, response.responseId, recipient);
|
|
677
|
+
if (!existing.ok) return existing;
|
|
678
|
+
if (!existing.data || feedJson(existing.data.response) !== feedJson(response)) {
|
|
679
|
+
return feedError("INVALID_INPUT", "A response handoff identity cannot change its payload");
|
|
680
|
+
}
|
|
681
|
+
if (existing.data.state !== "prepared") return ok(existing.data);
|
|
682
|
+
if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
|
|
683
|
+
const claimed = await handoff.execute(
|
|
684
|
+
"UPDATE feed_handoffs SET state = 'unconfirmed' WHERE response_id = ? AND recipient = ? AND state = 'prepared'",
|
|
685
|
+
[response.responseId, recipient]
|
|
686
|
+
);
|
|
687
|
+
if (!claimed.ok) return claimed;
|
|
688
|
+
if (claimed.data.changes === 0) {
|
|
689
|
+
const current = await getFeedHandoff(handoff, response.responseId, recipient);
|
|
690
|
+
return current.ok && current.data ? ok(current.data) : current.ok ? feedError("NOT_FOUND", "Handoff disappeared before delivery") : current;
|
|
691
|
+
}
|
|
692
|
+
if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
|
|
693
|
+
let state = "unconfirmed";
|
|
694
|
+
try {
|
|
695
|
+
const delivered = await deliver(response);
|
|
696
|
+
if (delivered === "delivered") state = "delivered";
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
if (state === "delivered") {
|
|
700
|
+
const saved = await handoff.execute(
|
|
701
|
+
"UPDATE feed_handoffs SET state = 'delivered' WHERE response_id = ? AND recipient = ? AND state = 'unconfirmed'",
|
|
702
|
+
[response.responseId, recipient]
|
|
703
|
+
);
|
|
704
|
+
if (!saved.ok) return saved;
|
|
705
|
+
}
|
|
706
|
+
return ok({ ...existing.data, state });
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/feed/restore-metadata.ts
|
|
710
|
+
var import_zod3 = require("zod");
|
|
711
|
+
var restoreOriginSchema = import_zod3.z.object({
|
|
712
|
+
sourceSpaceId: import_zod3.z.string().min(1),
|
|
713
|
+
namespace: import_zod3.z.string().min(1),
|
|
714
|
+
snapshotId: import_zod3.z.string().min(1),
|
|
715
|
+
sourceKey: import_zod3.z.string().min(1),
|
|
716
|
+
eventKey: import_zod3.z.string().min(1).optional()
|
|
717
|
+
}).strict();
|
|
718
|
+
var restorePolicySchema = import_zod3.z.object({
|
|
719
|
+
recipient: import_zod3.z.string().min(1),
|
|
720
|
+
state: import_zod3.z.enum(["pending", "held", "delivered", "unconfirmed", "refused", "unknown"]),
|
|
721
|
+
dispatch: import_zod3.z.enum(["never", "resolved-pending", "held"]),
|
|
722
|
+
resolvedOwnerMapping: import_zod3.z.boolean(),
|
|
723
|
+
dispatchBefore: import_zod3.z.custom(validDispatchBefore),
|
|
724
|
+
source: restoreOriginSchema
|
|
725
|
+
}).strict();
|
|
726
|
+
function restoreContent(input, inputPayload) {
|
|
727
|
+
const response = feedResponseHeaderSchema.parse(input);
|
|
728
|
+
const headerJson = feedJson(response);
|
|
729
|
+
if (feedJson(input) !== headerJson) throw new Error("Invalid restored response header fields");
|
|
730
|
+
const payload = inputPayload === void 0 ? void 0 : feedResponsePayloadSchema.parse(inputPayload);
|
|
731
|
+
const payloadJson = payload === void 0 ? void 0 : feedJson(payload.data);
|
|
732
|
+
const envelope = payload === void 0 ? "null" : `{"data":${payloadJson},"type":${JSON.stringify(payload.type)},"version":${payload.version}}`;
|
|
733
|
+
return { response, headerJson, payload, payloadJson, canonical: `{"payload":${envelope},"response":${headerJson}}` };
|
|
734
|
+
}
|
|
735
|
+
async function contentSha256(content) {
|
|
736
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)));
|
|
737
|
+
let hex = "";
|
|
738
|
+
for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
|
|
739
|
+
return hex;
|
|
740
|
+
}
|
|
741
|
+
function sameOriginBinding(a, b) {
|
|
742
|
+
return a.sourceSpaceId === b.sourceSpaceId && a.namespace === b.namespace && a.sourceKey === b.sourceKey && a.eventKey === b.eventKey;
|
|
743
|
+
}
|
|
744
|
+
function restoreCancelled(signal) {
|
|
745
|
+
return signal?.aborted ? feedError(ErrorCodes.ABORTED, "Feed restore or policy read was aborted") : void 0;
|
|
746
|
+
}
|
|
747
|
+
function baselinePolicy(source) {
|
|
748
|
+
return { recipient: "*", state: "unknown", dispatch: "never", resolvedOwnerMapping: false, dispatchBefore: null, source };
|
|
749
|
+
}
|
|
750
|
+
async function readRestoreMetadata(responses, responseIds, options) {
|
|
751
|
+
const cancelled = restoreCancelled(options.signal);
|
|
752
|
+
if (cancelled) return cancelled;
|
|
753
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
754
|
+
for (const id of responseIds) metadata.set(id, { origins: /* @__PURE__ */ new Map(), policies: [] });
|
|
755
|
+
const selected = responseIds.length ? `VALUES ${responseIds.map(() => "(?)").join(",")}` : "SELECT CAST(NULL AS TEXT) WHERE 0";
|
|
756
|
+
try {
|
|
757
|
+
const result = await responses.query(`WITH selected(response_id) AS (${selected})
|
|
758
|
+
SELECT response_id, 'import', card_id, revision, issuance_id, content_sha256
|
|
759
|
+
FROM feed_response_imports WHERE response_id IN (SELECT response_id FROM selected)
|
|
760
|
+
UNION ALL SELECT response_id, 'origin', source_json, NULL, NULL, NULL
|
|
761
|
+
FROM feed_response_origins WHERE response_id IN (SELECT response_id FROM selected)
|
|
762
|
+
UNION ALL SELECT response_id, 'policy', recipient, policy_json, NULL, NULL
|
|
763
|
+
FROM feed_response_delivery_policies WHERE response_id IN (SELECT response_id FROM selected)`, responseIds, options);
|
|
764
|
+
if (!result.ok) return responseDataFailure("Could not read feed restore policy metadata", result.error);
|
|
765
|
+
const aborted = restoreCancelled(options.signal);
|
|
766
|
+
if (aborted) return aborted;
|
|
767
|
+
for (const row of result.data.rows) {
|
|
768
|
+
const target = metadata.get(String(row[0]));
|
|
769
|
+
if (!target) throw new Error("Policy metadata does not belong to its response page");
|
|
770
|
+
if (row[1] === "import") {
|
|
771
|
+
if (target.marker || typeof row[2] !== "string" || !row[2] || !Number.isSafeInteger(row[3]) || Number(row[3]) < 1 || typeof row[4] !== "string" || !row[4] || typeof row[5] !== "string" || !/^[a-f0-9]{64}$/.test(row[5])) {
|
|
772
|
+
throw new Error("Invalid import marker");
|
|
773
|
+
}
|
|
774
|
+
target.marker = { cardId: row[2], revision: Number(row[3]), issuanceId: row[4], contentSha256: row[5] };
|
|
775
|
+
} else if (row[1] === "origin") {
|
|
776
|
+
const source = restoreOriginSchema.parse(JSON.parse(String(row[2])));
|
|
777
|
+
target.origins.set(feedJson(source), source);
|
|
778
|
+
} else if (row[1] === "policy") {
|
|
779
|
+
const policy = restorePolicySchema.parse(JSON.parse(String(row[3])));
|
|
780
|
+
if (row[2] !== policy.recipient) throw new Error("Policy recipient does not match its stored key");
|
|
781
|
+
target.policies.push(policy);
|
|
782
|
+
} else throw new Error("Unknown restore metadata row");
|
|
783
|
+
}
|
|
784
|
+
return ok(metadata);
|
|
785
|
+
} catch (error) {
|
|
786
|
+
return responseDataFailure("Could not verify feed restore policy metadata", error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function verifyRestoreMetadata(metadata, response, contentHash) {
|
|
790
|
+
if (!metadata.marker) {
|
|
791
|
+
if (metadata.origins.size || metadata.policies.length) throw new Error("Orphan restore metadata");
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
if (!sameFeedIdentity(metadata.marker, response) || contentHash !== void 0 && metadata.marker.contentSha256 !== contentHash || !metadata.origins.size) {
|
|
795
|
+
throw new Error("Import identity, content or source reference is unverified");
|
|
796
|
+
}
|
|
797
|
+
let binding;
|
|
798
|
+
const defaults = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const source of metadata.origins.values()) {
|
|
800
|
+
if (binding && !sameOriginBinding(binding, source)) throw new Error("Conflicting import source bindings");
|
|
801
|
+
binding = source;
|
|
802
|
+
}
|
|
803
|
+
for (const policy of metadata.policies) {
|
|
804
|
+
const source = feedJson(policy.source);
|
|
805
|
+
if (!metadata.origins.has(source)) throw new Error("Policy references an unregistered import origin");
|
|
806
|
+
if (policy.recipient === "*") {
|
|
807
|
+
if (feedJson(policy) !== feedJson(baselinePolicy(policy.source))) throw new Error("Invalid default import policy");
|
|
808
|
+
defaults.add(source);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
for (const source of metadata.origins.keys()) if (!defaults.has(source)) throw new Error("Missing default import policy");
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/feed/delivery-policy.ts
|
|
816
|
+
function permittedDeadline(metadata, recipient) {
|
|
817
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const policy of metadata.policies) {
|
|
819
|
+
if (policy.recipient !== recipient) continue;
|
|
820
|
+
const source = feedJson(policy.source);
|
|
821
|
+
const rows = evidence.get(source);
|
|
822
|
+
if (rows) rows.push(policy);
|
|
823
|
+
else evidence.set(source, [policy]);
|
|
824
|
+
}
|
|
825
|
+
let cutoff;
|
|
826
|
+
for (const source of metadata.origins.keys()) {
|
|
827
|
+
const rows = evidence.get(source);
|
|
828
|
+
if (!rows?.length) return { allowed: false };
|
|
829
|
+
for (const policy of rows) {
|
|
830
|
+
if (policy.state !== "pending" || policy.dispatch !== "resolved-pending" || !policy.resolvedOwnerMapping) return { allowed: false };
|
|
831
|
+
if (cutoff === void 0) cutoff = policy.dispatchBefore;
|
|
832
|
+
else if (cutoff !== policy.dispatchBefore) return { allowed: false };
|
|
833
|
+
if (typeof policy.dispatchBefore === "string" && Date.now() >= Date.parse(policy.dispatchBefore)) return { allowed: false };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return cutoff === void 0 ? { allowed: false } : { allowed: true, cutoff };
|
|
837
|
+
}
|
|
838
|
+
var listFeedResponseDeliveries = async (responses, options) => {
|
|
839
|
+
try {
|
|
840
|
+
const after = options.afterSequence ?? 0;
|
|
841
|
+
const limit = options.limit ?? 100;
|
|
842
|
+
if (!options.recipient || options.recipient === "*" || !Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
|
|
843
|
+
return feedError("INVALID_INPUT", "Delivery pages require an exact recipient and valid sequence pagination");
|
|
844
|
+
}
|
|
845
|
+
const privateOptions = { redactErrors: true, signal: options.signal };
|
|
846
|
+
const cancelled = restoreCancelled(options.signal);
|
|
847
|
+
if (cancelled) return cancelled;
|
|
848
|
+
const result = await responses.query(`SELECT r.sequence, r.response_json, p.payload_type, p.payload_version,
|
|
849
|
+
p.payload_json, r.card_id, r.revision, r.issuance_id, r.response_id
|
|
850
|
+
FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
|
|
851
|
+
WHERE r.sequence > ? ORDER BY r.sequence LIMIT ?`, [after, limit + 1], privateOptions);
|
|
852
|
+
if (!result.ok) return responseDataFailure("Could not read feed response delivery headers", result.error);
|
|
853
|
+
const stopped = restoreCancelled(options.signal);
|
|
854
|
+
if (stopped) return stopped;
|
|
855
|
+
const rows = result.data.rows.slice(0, limit);
|
|
856
|
+
const headers = [];
|
|
857
|
+
const ids = [];
|
|
858
|
+
const seen = /* @__PURE__ */ new Set();
|
|
859
|
+
let nextSequence = after;
|
|
860
|
+
for (const row of rows) {
|
|
861
|
+
const response = responseFromRow(row);
|
|
862
|
+
if (!Number.isSafeInteger(response.sequence) || response.sequence <= nextSequence || seen.has(response.responseId) || row[8] !== response.responseId || !sameFeedIdentity(response, { cardId: String(row[5]), revision: Number(row[6]), issuanceId: String(row[7]) })) {
|
|
863
|
+
throw new Error("Response page identity or ordering is unverified");
|
|
864
|
+
}
|
|
865
|
+
seen.add(response.responseId);
|
|
866
|
+
nextSequence = response.sequence;
|
|
867
|
+
headers.push(response);
|
|
868
|
+
ids.push(response.responseId);
|
|
869
|
+
}
|
|
870
|
+
const metadataResult = await readRestoreMetadata(responses, ids, privateOptions);
|
|
871
|
+
if (!metadataResult.ok) return metadataResult;
|
|
872
|
+
const deliveries = [];
|
|
873
|
+
for (let index = 0; index < headers.length; index += 1) {
|
|
874
|
+
const response = headers[index];
|
|
875
|
+
const metadata = metadataResult.data.get(response.responseId);
|
|
876
|
+
if (!verifyRestoreMetadata(metadata, response)) {
|
|
877
|
+
deliveries.push(response);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const row = rows[index];
|
|
881
|
+
let payload;
|
|
882
|
+
if (row[2] !== null && row[2] !== void 0) {
|
|
883
|
+
payload = feedResponsePayloadSchema.parse({ type: row[2], version: row[3], data: JSON.parse(String(row[4])) });
|
|
884
|
+
} else if (row[3] !== null && row[3] !== void 0 || row[4] !== null && row[4] !== void 0) {
|
|
885
|
+
throw new Error("Imported payload presence is unverified");
|
|
886
|
+
}
|
|
887
|
+
const content = restoreContent(JSON.parse(String(row[1])), payload);
|
|
888
|
+
const hash = await contentSha256(content.canonical);
|
|
889
|
+
const aborted2 = restoreCancelled(options.signal);
|
|
890
|
+
if (aborted2) return aborted2;
|
|
891
|
+
verifyRestoreMetadata(metadata, response, hash);
|
|
892
|
+
const permission = permittedDeadline(metadata, options.recipient);
|
|
893
|
+
if (permission.allowed) {
|
|
894
|
+
deliveries.push(projectFeedResponseNotification({ ...response, dispatchBefore: permission.cutoff }));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const aborted = restoreCancelled(options.signal);
|
|
898
|
+
if (aborted) return aborted;
|
|
899
|
+
return ok({ responses: deliveries, nextSequence, hasMore: result.data.rows.length > limit });
|
|
900
|
+
} catch (error) {
|
|
901
|
+
return responseDataFailure("Could not verify feed response delivery policy", error);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/feed/listener.ts
|
|
906
|
+
var listenFeedResponses = async (options) => {
|
|
907
|
+
const { signal } = options;
|
|
908
|
+
const renewalSeconds = options.renewalSeconds ?? 300;
|
|
909
|
+
if (!Number.isInteger(renewalSeconds) || renewalSeconds < 60 || renewalSeconds > 300) {
|
|
910
|
+
throw new Error("Feed renewal must be between 60 and 300 seconds");
|
|
911
|
+
}
|
|
912
|
+
if (options.subscription.service !== "sql" || !options.subscription.pathPrefix) {
|
|
913
|
+
throw new Error("Feed listener requires the exact response SQL resource subscription");
|
|
914
|
+
}
|
|
915
|
+
if (signal.aborted) return;
|
|
916
|
+
const initialized = await initializeFeedHandoff(options.handoff);
|
|
917
|
+
if (!initialized.ok) throw initialized.error;
|
|
918
|
+
let ready = false;
|
|
919
|
+
let dirty = false;
|
|
920
|
+
let draining;
|
|
921
|
+
let failed = false;
|
|
922
|
+
let failure;
|
|
923
|
+
let current;
|
|
924
|
+
const drain = async () => {
|
|
925
|
+
do {
|
|
926
|
+
dirty = false;
|
|
927
|
+
let afterSequence = 0;
|
|
928
|
+
while (!signal.aborted) {
|
|
929
|
+
const page = await listFeedResponseDeliveries(options.responses, { recipient: options.recipient, afterSequence, limit: 100, signal });
|
|
930
|
+
if (!page.ok) {
|
|
931
|
+
if (signal.aborted && page.error.code === ErrorCodes.ABORTED) return;
|
|
932
|
+
throw page.error;
|
|
933
|
+
}
|
|
934
|
+
for (const response of page.data.responses) {
|
|
935
|
+
if (signal.aborted) return;
|
|
936
|
+
const handoff = await deliverFeedResponse({
|
|
937
|
+
handoff: options.handoff,
|
|
938
|
+
recipient: options.recipient,
|
|
939
|
+
response,
|
|
940
|
+
deliver: options.deliver
|
|
941
|
+
});
|
|
942
|
+
if (!handoff.ok && !isDispatchDeadlineFailure(handoff.error)) throw handoff.error;
|
|
943
|
+
}
|
|
944
|
+
if (page.data.hasMore && page.data.nextSequence <= afterSequence) throw new Error("Delivery page did not advance a verified cursor");
|
|
945
|
+
afterSequence = page.data.nextSequence;
|
|
946
|
+
if (!page.data.hasMore) break;
|
|
947
|
+
}
|
|
948
|
+
} while (dirty && !signal.aborted);
|
|
949
|
+
};
|
|
950
|
+
const schedule = () => {
|
|
951
|
+
if (!ready || signal.aborted || failed) return;
|
|
952
|
+
dirty = true;
|
|
953
|
+
if (draining) return;
|
|
954
|
+
draining = drain().catch((error) => {
|
|
955
|
+
failed = true;
|
|
956
|
+
failure = error;
|
|
957
|
+
current?.abort();
|
|
958
|
+
}).finally(() => {
|
|
959
|
+
draining = void 0;
|
|
960
|
+
if (dirty && !failed) schedule();
|
|
961
|
+
});
|
|
962
|
+
};
|
|
963
|
+
while (!signal.aborted && !failed) {
|
|
964
|
+
const connection = new AbortController();
|
|
965
|
+
current = connection;
|
|
966
|
+
const stop = () => connection.abort();
|
|
967
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
968
|
+
const renewal = setTimeout(stop, renewalSeconds * 1e3);
|
|
969
|
+
renewal.unref?.();
|
|
970
|
+
try {
|
|
971
|
+
for await (const _event of options.hooks.subscribe([options.subscription], {
|
|
972
|
+
signal: connection.signal,
|
|
973
|
+
ttlSeconds: renewalSeconds,
|
|
974
|
+
onReady: () => {
|
|
975
|
+
if (current !== connection || connection.signal.aborted || signal.aborted) return;
|
|
976
|
+
ready = true;
|
|
977
|
+
schedule();
|
|
978
|
+
}
|
|
979
|
+
})) schedule();
|
|
980
|
+
} finally {
|
|
981
|
+
ready = false;
|
|
982
|
+
clearTimeout(renewal);
|
|
983
|
+
signal.removeEventListener("abort", stop);
|
|
984
|
+
await draining;
|
|
985
|
+
}
|
|
986
|
+
if (failed) throw failure;
|
|
987
|
+
if (!connection.signal.aborted) return;
|
|
988
|
+
}
|
|
989
|
+
if (failed) throw failure;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
// src/feed/resources.ts
|
|
993
|
+
var feedResourceNames = (prefix, stableToken) => {
|
|
994
|
+
if (!/^[A-Za-z0-9_-]+$/.test(stableToken) || !prefix || prefix.endsWith("/") || prefix.split("/").some((part) => !part || part === "." || part === ".." || !/^[A-Za-z0-9._-]+$/.test(part))) {
|
|
995
|
+
return feedError("INVALID_INPUT", "Feed resources require a canonical prefix and a stable alphanumeric, underscore or hyphen agent token");
|
|
996
|
+
}
|
|
997
|
+
return ok({
|
|
998
|
+
outputResource: `${prefix}/feed_output_${stableToken}`,
|
|
999
|
+
responseResource: `${prefix}/feed_responses_${stableToken}`
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// src/feed/restore.ts
|
|
1004
|
+
var import_zod4 = require("zod");
|
|
1005
|
+
var envelopeSchema = import_zod4.z.object({
|
|
1006
|
+
response: import_zod4.z.unknown(),
|
|
1007
|
+
payload: import_zod4.z.unknown().optional(),
|
|
1008
|
+
origin: restoreOriginSchema,
|
|
1009
|
+
deliveries: import_zod4.z.array(restorePolicySchema).optional()
|
|
1010
|
+
}).strict();
|
|
1011
|
+
var recordsSchema = import_zod4.z.object({ cards: import_zod4.z.array(import_zod4.z.unknown()), responses: import_zod4.z.array(envelopeSchema) }).strict();
|
|
1012
|
+
function markerGuard(content, hash) {
|
|
1013
|
+
const response = content.response;
|
|
1014
|
+
return {
|
|
1015
|
+
sql: `EXISTS (SELECT 1 FROM feed_response_imports WHERE response_id = ? AND card_id = ?
|
|
1016
|
+
AND revision = ? AND issuance_id = ? AND content_sha256 = ?)`,
|
|
1017
|
+
params: [response.responseId, response.cardId, response.revision, response.issuanceId, hash]
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
function bindingGuard(responseId, source) {
|
|
1021
|
+
return {
|
|
1022
|
+
sql: `NOT EXISTS (SELECT 1 FROM feed_response_origins WHERE response_id = ? AND (
|
|
1023
|
+
json_extract(source_json, '$.sourceSpaceId') IS NOT ? OR json_extract(source_json, '$.namespace') IS NOT ?
|
|
1024
|
+
OR json_extract(source_json, '$.sourceKey') IS NOT ? OR json_extract(source_json, '$.eventKey') IS NOT ?))`,
|
|
1025
|
+
params: [responseId, source.sourceSpaceId, source.namespace, source.sourceKey, source.eventKey ?? null]
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function headerGuard(content) {
|
|
1029
|
+
const response = content.response;
|
|
1030
|
+
return {
|
|
1031
|
+
sql: `EXISTS (SELECT 1 FROM feed_responses r WHERE r.response_id = ? AND r.card_id = ? AND r.revision = ?
|
|
1032
|
+
AND r.issuance_id = ? AND r.response_json = ?)`,
|
|
1033
|
+
params: [response.responseId, response.cardId, response.revision, response.issuanceId, content.headerJson]
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function payloadGuard(content) {
|
|
1037
|
+
const responseId = content.response.responseId;
|
|
1038
|
+
return content.payload === void 0 ? { sql: "NOT EXISTS (SELECT 1 FROM feed_response_payloads WHERE response_id = ?)", params: [responseId] } : {
|
|
1039
|
+
sql: `EXISTS (SELECT 1 FROM feed_response_payloads WHERE response_id = ? AND payload_type = ?
|
|
1040
|
+
AND payload_version = ? AND payload_json = ?)`,
|
|
1041
|
+
params: [responseId, content.payload.type, content.payload.version, content.payloadJson]
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function combined(...guards) {
|
|
1045
|
+
return { sql: guards.map((guard) => `(${guard.sql})`).join(" AND "), params: guards.flatMap((guard) => guard.params) };
|
|
1046
|
+
}
|
|
1047
|
+
function restoreStatements(group, hash) {
|
|
1048
|
+
const { content } = group;
|
|
1049
|
+
const response = content.response;
|
|
1050
|
+
const first = group.origins.values().next().value;
|
|
1051
|
+
const claim = combined(markerGuard(content, hash), bindingGuard(response.responseId, first.origin));
|
|
1052
|
+
const header = headerGuard(content);
|
|
1053
|
+
const complete = combined(claim, header, payloadGuard(content));
|
|
1054
|
+
const committed = [complete];
|
|
1055
|
+
const statements = [{
|
|
1056
|
+
// Claim precedes the header under the same transaction. A committed live header cannot be reclassified.
|
|
1057
|
+
sql: `INSERT INTO feed_response_imports (response_id, card_id, revision, issuance_id, content_sha256)
|
|
1058
|
+
SELECT ?, ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM feed_responses WHERE response_id = ?)
|
|
1059
|
+
AND NOT EXISTS (SELECT 1 FROM feed_response_payloads WHERE response_id = ?)
|
|
1060
|
+
AND NOT EXISTS (SELECT 1 FROM feed_response_origins WHERE response_id = ?)
|
|
1061
|
+
AND NOT EXISTS (SELECT 1 FROM feed_response_delivery_policies WHERE response_id = ?)
|
|
1062
|
+
ON CONFLICT (response_id) DO NOTHING`,
|
|
1063
|
+
params: [
|
|
1064
|
+
response.responseId,
|
|
1065
|
+
response.cardId,
|
|
1066
|
+
response.revision,
|
|
1067
|
+
response.issuanceId,
|
|
1068
|
+
hash,
|
|
1069
|
+
response.responseId,
|
|
1070
|
+
response.responseId,
|
|
1071
|
+
response.responseId,
|
|
1072
|
+
response.responseId
|
|
1073
|
+
]
|
|
1074
|
+
}, {
|
|
1075
|
+
sql: `INSERT INTO feed_responses (response_id, card_id, revision, issuance_id, response_json)
|
|
1076
|
+
SELECT ?, ?, ?, ?, ? WHERE ${claim.sql} ON CONFLICT (response_id) DO NOTHING`,
|
|
1077
|
+
params: [response.responseId, response.cardId, response.revision, response.issuanceId, content.headerJson, ...claim.params]
|
|
1078
|
+
}];
|
|
1079
|
+
if (content.payload !== void 0) statements.push({
|
|
1080
|
+
sql: `INSERT INTO feed_response_payloads (response_id, payload_type, payload_version, payload_json)
|
|
1081
|
+
SELECT ?, ?, ?, ? WHERE ${combined(claim, header).sql} ON CONFLICT (response_id) DO NOTHING`,
|
|
1082
|
+
params: [response.responseId, content.payload.type, content.payload.version, content.payloadJson, ...claim.params, ...header.params]
|
|
1083
|
+
});
|
|
1084
|
+
for (const [sourceJson, evidence] of group.origins) {
|
|
1085
|
+
statements.push({
|
|
1086
|
+
sql: `INSERT INTO feed_response_origins (response_id, source_json)
|
|
1087
|
+
SELECT ?, ? WHERE ${complete.sql} ON CONFLICT (response_id, source_json) DO NOTHING`,
|
|
1088
|
+
params: [response.responseId, sourceJson, ...complete.params]
|
|
1089
|
+
});
|
|
1090
|
+
committed.push({
|
|
1091
|
+
sql: "EXISTS (SELECT 1 FROM feed_response_origins WHERE response_id = ? AND source_json = ?)",
|
|
1092
|
+
params: [response.responseId, sourceJson]
|
|
1093
|
+
});
|
|
1094
|
+
for (const policy of [baselinePolicy(evidence.origin), ...evidence.policies.values()]) {
|
|
1095
|
+
statements.push({
|
|
1096
|
+
sql: `INSERT INTO feed_response_delivery_policies (response_id, recipient, policy_json)
|
|
1097
|
+
SELECT ?, ?, ? WHERE ${complete.sql} AND EXISTS (
|
|
1098
|
+
SELECT 1 FROM feed_response_origins WHERE response_id = ? AND source_json = ?)
|
|
1099
|
+
ON CONFLICT (response_id, recipient, policy_json) DO NOTHING`,
|
|
1100
|
+
params: [response.responseId, policy.recipient, feedJson(policy), ...complete.params, response.responseId, sourceJson]
|
|
1101
|
+
});
|
|
1102
|
+
committed.push({
|
|
1103
|
+
sql: "EXISTS (SELECT 1 FROM feed_response_delivery_policies WHERE response_id = ? AND recipient = ? AND policy_json = ?)",
|
|
1104
|
+
params: [response.responseId, policy.recipient, feedJson(policy)]
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const proof = combined(...committed);
|
|
1109
|
+
statements.push({
|
|
1110
|
+
sql: `INSERT INTO feed_response_imports (response_id, card_id, revision, issuance_id, content_sha256)
|
|
1111
|
+
SELECT ?, NULL, NULL, NULL, NULL WHERE NOT (${proof.sql})`,
|
|
1112
|
+
params: [response.responseId, ...proof.params]
|
|
1113
|
+
});
|
|
1114
|
+
return statements;
|
|
1115
|
+
}
|
|
1116
|
+
async function restoreGroup(output, responses, group, options) {
|
|
1117
|
+
const { content } = group;
|
|
1118
|
+
const response = content.response;
|
|
1119
|
+
const card = await output.query(
|
|
1120
|
+
"SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
|
|
1121
|
+
[response.cardId, response.revision, response.issuanceId],
|
|
1122
|
+
options
|
|
1123
|
+
);
|
|
1124
|
+
if (!card.ok) return responseDataFailure("Could not verify a restored response card issuance", card.error);
|
|
1125
|
+
const cardReadCancelled = restoreCancelled(options.signal);
|
|
1126
|
+
if (cardReadCancelled) return cardReadCancelled;
|
|
1127
|
+
if (!card.data.rows[0] || !sameFeedIdentity(feedCardSchema.parse(JSON.parse(card.data.rows[0][0])), response)) {
|
|
1128
|
+
return feedError("INVALID_INPUT", "Restored response requires a published full card issuance");
|
|
1129
|
+
}
|
|
1130
|
+
const hash = await contentSha256(content.canonical);
|
|
1131
|
+
const cancelled = restoreCancelled(options.signal);
|
|
1132
|
+
if (cancelled) return cancelled;
|
|
1133
|
+
const previous = await storedResponse(responses, response.responseId, options);
|
|
1134
|
+
if (!previous.ok) return previous;
|
|
1135
|
+
const previousMetadata = await readRestoreMetadata(responses, [response.responseId], options);
|
|
1136
|
+
if (!previousMetadata.ok) return previousMetadata;
|
|
1137
|
+
const before = previousMetadata.data.get(response.responseId);
|
|
1138
|
+
if (previous.data) {
|
|
1139
|
+
if (!verifyRestoreMetadata(before, response, hash)) return feedError("INVALID_INPUT", "A live response ID cannot be restored or reclassified");
|
|
1140
|
+
if (restoreContent(JSON.parse(previous.data.rawHeaderJson), previous.data.payload).canonical !== content.canonical) {
|
|
1141
|
+
return feedError("INVALID_INPUT", "A restored response ID cannot change header or private payload");
|
|
1142
|
+
}
|
|
1143
|
+
const known = before.origins.values().next().value;
|
|
1144
|
+
for (const evidence of group.origins.values()) if (!sameOriginBinding(known, evidence.origin)) {
|
|
1145
|
+
return feedError("INVALID_INPUT", "A restored response cannot alias its source binding");
|
|
1146
|
+
}
|
|
1147
|
+
} else if (before.marker || before.origins.size || before.policies.length) {
|
|
1148
|
+
return feedError("INVALID_INPUT", "Orphan restore metadata cannot authorize a new header");
|
|
1149
|
+
}
|
|
1150
|
+
const aborted = restoreCancelled(options.signal);
|
|
1151
|
+
if (aborted) return aborted;
|
|
1152
|
+
let failure;
|
|
1153
|
+
let failed = false;
|
|
1154
|
+
try {
|
|
1155
|
+
const saved = await responses.batch(restoreStatements(group, hash), options);
|
|
1156
|
+
if (!saved.ok) {
|
|
1157
|
+
failed = true;
|
|
1158
|
+
failure = saved.error;
|
|
1159
|
+
}
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
failed = true;
|
|
1162
|
+
failure = error;
|
|
1163
|
+
}
|
|
1164
|
+
const stopped = restoreCancelled(options.signal);
|
|
1165
|
+
if (stopped) return failed ? responseDataFailure("Could not restore response and atomic import policy", failure) : stopped;
|
|
1166
|
+
const stored = await storedResponse(responses, response.responseId, options);
|
|
1167
|
+
if (!stored.ok) return failed && options.signal?.aborted && stored.error.code === ErrorCodes.ABORTED ? responseDataFailure("Could not restore response and atomic import policy", failure) : stored;
|
|
1168
|
+
const metadata = await readRestoreMetadata(responses, [response.responseId], options);
|
|
1169
|
+
if (!metadata.ok) return failed && options.signal?.aborted && metadata.error.code === ErrorCodes.ABORTED ? responseDataFailure("Could not restore response and atomic import policy", failure) : metadata;
|
|
1170
|
+
const actual = metadata.data.get(response.responseId);
|
|
1171
|
+
if (!stored.data) return responseDataFailure("Could not restore response and atomic import policy", failure);
|
|
1172
|
+
if (!verifyRestoreMetadata(actual, response, hash) || restoreContent(JSON.parse(stored.data.rawHeaderJson), stored.data.payload).canonical !== content.canonical) {
|
|
1173
|
+
return feedError("INVALID_INPUT", "Restore conflicts with an existing live or different response");
|
|
1174
|
+
}
|
|
1175
|
+
const policies = new Set(actual.policies.map(feedJson));
|
|
1176
|
+
for (const [sourceJson, evidence] of group.origins) {
|
|
1177
|
+
if (!actual.origins.has(sourceJson) || !policies.has(feedJson(baselinePolicy(evidence.origin)))) {
|
|
1178
|
+
return failed ? responseDataFailure("Could not restore atomic provenance and default policy", failure) : feedError("INVALID_INPUT", "Restore source binding conflicts with existing provenance");
|
|
1179
|
+
}
|
|
1180
|
+
for (const policy of evidence.policies.keys()) if (!policies.has(policy)) {
|
|
1181
|
+
return responseDataFailure("Could not retain requested immutable delivery evidence", failure);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const readbackCancelled = restoreCancelled(options.signal);
|
|
1185
|
+
if (readbackCancelled) return failed ? responseDataFailure("Could not restore response and atomic import policy", failure) : readbackCancelled;
|
|
1186
|
+
return ok(void 0);
|
|
1187
|
+
}
|
|
1188
|
+
var restoreFeedRecords = async (ownerPair, input, options = {}) => {
|
|
1189
|
+
const privateOptions = { redactErrors: true, signal: options.signal };
|
|
1190
|
+
try {
|
|
1191
|
+
const cancelled = restoreCancelled(options.signal);
|
|
1192
|
+
if (cancelled) return cancelled;
|
|
1193
|
+
const parsed = recordsSchema.parse(input);
|
|
1194
|
+
const cards = parsed.cards.map((card) => {
|
|
1195
|
+
const valid = feedCardSchema.parse(card);
|
|
1196
|
+
if (valid.payload !== void 0) valid.payload = card.payload;
|
|
1197
|
+
if (feedJson(valid) !== feedJson(card)) throw new Error("Invalid restored card fields");
|
|
1198
|
+
return valid;
|
|
1199
|
+
});
|
|
1200
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1201
|
+
for (const record of parsed.responses) {
|
|
1202
|
+
const content = restoreContent(record.response, record.payload);
|
|
1203
|
+
const id = content.response.responseId;
|
|
1204
|
+
let group = groups.get(id);
|
|
1205
|
+
if (group && group.content.canonical !== content.canonical) throw new Error("Conflicting restored response content");
|
|
1206
|
+
if (!group) {
|
|
1207
|
+
group = { content, origins: /* @__PURE__ */ new Map() };
|
|
1208
|
+
groups.set(id, group);
|
|
1209
|
+
}
|
|
1210
|
+
const first = group.origins.values().next().value;
|
|
1211
|
+
if (first && !sameOriginBinding(first.origin, record.origin)) throw new Error("Conflicting restored response binding");
|
|
1212
|
+
const sourceJson = feedJson(record.origin);
|
|
1213
|
+
let evidence = group.origins.get(sourceJson);
|
|
1214
|
+
if (!evidence) {
|
|
1215
|
+
evidence = { origin: record.origin, policies: /* @__PURE__ */ new Map() };
|
|
1216
|
+
group.origins.set(sourceJson, evidence);
|
|
1217
|
+
}
|
|
1218
|
+
for (const policy of record.deliveries ?? []) {
|
|
1219
|
+
if (policy.recipient === "*" || feedJson(policy.source) !== sourceJson) throw new Error("Policy must belong to its exact origin and recipient");
|
|
1220
|
+
evidence.policies.set(feedJson(policy), policy);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
for (const card of cards) {
|
|
1224
|
+
const stopped = restoreCancelled(options.signal);
|
|
1225
|
+
if (stopped) return stopped;
|
|
1226
|
+
const saved = await storeFeedCard(ownerPair.output, card, privateOptions);
|
|
1227
|
+
if (!saved.ok) return responseDataFailure("Could not publish a restored card", saved.error);
|
|
1228
|
+
}
|
|
1229
|
+
for (const group of groups.values()) {
|
|
1230
|
+
const stopped = restoreCancelled(options.signal);
|
|
1231
|
+
if (stopped) return stopped;
|
|
1232
|
+
const saved = await restoreGroup(ownerPair.output, ownerPair.responses, group, privateOptions);
|
|
1233
|
+
if (!saved.ok) return saved;
|
|
1234
|
+
}
|
|
1235
|
+
return restoreCancelled(options.signal) ?? ok(void 0);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
return responseDataFailure("Could not validate or restore generic feed records", error);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
// src/feed/history.ts
|
|
1242
|
+
function validIdentity2(value) {
|
|
1243
|
+
return value !== null && typeof value === "object" && typeof value.cardId === "string" && value.cardId.length > 0 && Number.isSafeInteger(value.revision) && value.revision > 0 && typeof value.issuanceId === "string" && value.issuanceId.length > 0;
|
|
1244
|
+
}
|
|
1245
|
+
var listFeedCardHistory = async (output, options = {}) => {
|
|
1246
|
+
try {
|
|
1247
|
+
const { afterIdentity, signal } = options;
|
|
1248
|
+
const limit = options.limit === void 0 ? 100 : options.limit;
|
|
1249
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3 || afterIdentity !== void 0 && !validIdentity2(afterIdentity)) {
|
|
1250
|
+
return feedError(ErrorCodes.INVALID_INPUT, "Card history requires a full identity cursor and valid page limit");
|
|
1251
|
+
}
|
|
1252
|
+
if (signal?.aborted) return feedError(ErrorCodes.ABORTED, "Card history read was cancelled");
|
|
1253
|
+
const params = afterIdentity ? [afterIdentity.cardId, afterIdentity.revision, afterIdentity.issuanceId, limit + 1] : [limit + 1];
|
|
1254
|
+
const result = await output.query(`SELECT card_id, revision, issuance_id, card_json FROM feed_cards
|
|
1255
|
+
${afterIdentity ? "WHERE (card_id COLLATE BINARY, revision, issuance_id COLLATE BINARY) > (?, ?, ?)" : ""}
|
|
1256
|
+
ORDER BY card_id COLLATE BINARY, revision, issuance_id COLLATE BINARY LIMIT ?`, params, { redactErrors: true, signal });
|
|
1257
|
+
if (!result.ok) return responseDataFailure("Could not read feed card history", result.error);
|
|
1258
|
+
if (signal?.aborted) return feedError(ErrorCodes.ABORTED, "Card history read was cancelled");
|
|
1259
|
+
const cards = [];
|
|
1260
|
+
const count = Math.min(limit, result.data.rows.length);
|
|
1261
|
+
for (let index = 0; index < count; index += 1) {
|
|
1262
|
+
const row = result.data.rows[index];
|
|
1263
|
+
if (typeof row[3] !== "string") throw new Error("Stored card envelope is invalid");
|
|
1264
|
+
const card = JSON.parse(row[3]);
|
|
1265
|
+
feedCardSchema.parse(card);
|
|
1266
|
+
if (!validIdentity2(card) || row[0] !== card.cardId || row[1] !== card.revision || row[2] !== card.issuanceId) {
|
|
1267
|
+
throw new Error("Stored card identity does not match its key");
|
|
1268
|
+
}
|
|
1269
|
+
cards.push(card);
|
|
1270
|
+
}
|
|
1271
|
+
const last = cards[cards.length - 1];
|
|
1272
|
+
return ok({
|
|
1273
|
+
cards,
|
|
1274
|
+
nextIdentity: last ? { cardId: last.cardId, revision: last.revision, issuanceId: last.issuanceId } : null,
|
|
1275
|
+
hasMore: result.data.rows.length > limit
|
|
1276
|
+
});
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
return responseDataFailure("Could not verify feed card history", error);
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1282
|
+
0 && (module.exports = {
|
|
1283
|
+
appendFeedMessage,
|
|
1284
|
+
appendFeedResponse,
|
|
1285
|
+
deliverFeedResponse,
|
|
1286
|
+
feedCardSchema,
|
|
1287
|
+
feedHandlingSchema,
|
|
1288
|
+
feedResourceNames,
|
|
1289
|
+
feedResponsePayloadSchema,
|
|
1290
|
+
getFeedCardState,
|
|
1291
|
+
getFeedHandoff,
|
|
1292
|
+
initializeFeedDatabases,
|
|
1293
|
+
initializeFeedHandoff,
|
|
1294
|
+
listFeedCardHistory,
|
|
1295
|
+
listFeedCards,
|
|
1296
|
+
listFeedResponseDeliveries,
|
|
1297
|
+
listFeedResponses,
|
|
1298
|
+
listenFeedResponses,
|
|
1299
|
+
newFeedMessageSchema,
|
|
1300
|
+
newFeedResponseSchema,
|
|
1301
|
+
projectFeedResponseNotification,
|
|
1302
|
+
publishFeedCard,
|
|
1303
|
+
readFeedResponseHandling,
|
|
1304
|
+
readFeedResponsePayload,
|
|
1305
|
+
recordFeedHandling,
|
|
1306
|
+
resolveFeedCard,
|
|
1307
|
+
restoreFeedRecords
|
|
1308
|
+
});
|
|
1309
|
+
//# sourceMappingURL=index.cjs.map
|