@tinycloud/sdk-services 3.8.0-beta.18 → 3.8.0-beta.22
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/feed/index.cjs +537 -15
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.d.cts +63 -1
- package/dist/feed/index.d.ts +63 -1
- package/dist/feed/index.js +533 -14
- package/dist/feed/index.js.map +1 -1
- package/package.json +1 -1
package/dist/feed/index.cjs
CHANGED
|
@@ -31,7 +31,9 @@ __export(feed_exports, {
|
|
|
31
31
|
getFeedHandoff: () => getFeedHandoff,
|
|
32
32
|
initializeFeedDatabases: () => initializeFeedDatabases,
|
|
33
33
|
initializeFeedHandoff: () => initializeFeedHandoff,
|
|
34
|
+
listFeedCardHistory: () => listFeedCardHistory,
|
|
34
35
|
listFeedCards: () => listFeedCards,
|
|
36
|
+
listFeedResponseDeliveries: () => listFeedResponseDeliveries,
|
|
35
37
|
listFeedResponses: () => listFeedResponses,
|
|
36
38
|
listenFeedResponses: () => listenFeedResponses,
|
|
37
39
|
newFeedMessageSchema: () => newFeedMessageSchema,
|
|
@@ -41,7 +43,8 @@ __export(feed_exports, {
|
|
|
41
43
|
readFeedResponseHandling: () => readFeedResponseHandling,
|
|
42
44
|
readFeedResponsePayload: () => readFeedResponsePayload,
|
|
43
45
|
recordFeedHandling: () => recordFeedHandling,
|
|
44
|
-
resolveFeedCard: () => resolveFeedCard
|
|
46
|
+
resolveFeedCard: () => resolveFeedCard,
|
|
47
|
+
restoreFeedRecords: () => restoreFeedRecords
|
|
45
48
|
});
|
|
46
49
|
module.exports = __toCommonJS(feed_exports);
|
|
47
50
|
|
|
@@ -184,6 +187,23 @@ var feedHandlingSchema = import_zod.z.object({
|
|
|
184
187
|
detail: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
185
188
|
});
|
|
186
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
|
+
|
|
187
207
|
// src/feed/notification.ts
|
|
188
208
|
var projectFeedResponseNotification = (response) => {
|
|
189
209
|
const notification = {
|
|
@@ -200,6 +220,7 @@ var projectFeedResponseNotification = (response) => {
|
|
|
200
220
|
if (response.kind !== "message" && response.supersedesResponseId !== void 0) notification.supersedesResponseId = response.supersedesResponseId;
|
|
201
221
|
if (response.payloadType !== void 0) notification.payloadType = response.payloadType;
|
|
202
222
|
if (response.payloadVersion !== void 0) notification.payloadVersion = response.payloadVersion;
|
|
223
|
+
if (validDispatchBefore(response.dispatchBefore)) notification.dispatchBefore = response.dispatchBefore;
|
|
203
224
|
return notification;
|
|
204
225
|
};
|
|
205
226
|
|
|
@@ -254,18 +275,18 @@ function responseFromHeader(row, header) {
|
|
|
254
275
|
}
|
|
255
276
|
return projectFeedResponseNotification(response);
|
|
256
277
|
}
|
|
257
|
-
async function storedResponse(db, responseId) {
|
|
278
|
+
async function storedResponse(db, responseId, options = PRIVATE_RESPONSE_OPTIONS) {
|
|
258
279
|
try {
|
|
259
280
|
const result = await db.query(`SELECT r.sequence, r.response_json, p.payload_type, p.payload_version, p.payload_json
|
|
260
281
|
FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
|
|
261
|
-
WHERE r.response_id = ?`, [responseId],
|
|
282
|
+
WHERE r.response_id = ?`, [responseId], options);
|
|
262
283
|
if (!result.ok) return responseDataFailure("Could not read the saved feed response", result.error);
|
|
263
284
|
const row = result.data.rows[0];
|
|
264
285
|
if (!row) return ok(void 0);
|
|
265
286
|
const header = feedResponseHeaderSchema.parse(JSON.parse(String(row[1])));
|
|
266
287
|
const response = responseFromHeader(row, header);
|
|
267
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])) });
|
|
268
|
-
return ok({ response, headerJson: feedJson(header), payload });
|
|
289
|
+
return ok({ response, rawHeaderJson: String(row[1]), headerJson: feedJson(header), payload });
|
|
269
290
|
} catch (error) {
|
|
270
291
|
return responseDataFailure("Could not read the saved feed response", error);
|
|
271
292
|
}
|
|
@@ -295,30 +316,44 @@ var initializeFeedDatabases = async ({ output, responses }) => {
|
|
|
295
316
|
{ sql: "CREATE INDEX IF NOT EXISTS feed_responses_card ON feed_responses (card_id, revision, issuance_id, sequence)" },
|
|
296
317
|
{ sql: `CREATE TABLE IF NOT EXISTS feed_response_payloads (
|
|
297
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)
|
|
298
330
|
)` }
|
|
299
331
|
]);
|
|
300
332
|
return responseSchema.ok ? ok(void 0) : responseSchema;
|
|
301
333
|
};
|
|
302
|
-
var publishFeedCard =
|
|
334
|
+
var publishFeedCard = (output, card) => storeFeedCard(output, card);
|
|
335
|
+
async function storeFeedCard(output, card, options) {
|
|
303
336
|
if (!validIdentity(card)) return feedError("INVALID_INPUT", "A card requires its full ID, positive revision and issuance ID");
|
|
304
337
|
const encoded = feedJson(card);
|
|
305
338
|
const inserted = await output.execute(
|
|
306
339
|
`INSERT INTO feed_cards (card_id, revision, issuance_id, card_json) VALUES (?, ?, ?, ?)
|
|
307
340
|
ON CONFLICT (card_id, revision, issuance_id) DO NOTHING`,
|
|
308
|
-
[card.cardId, card.revision, card.issuanceId, encoded]
|
|
341
|
+
[card.cardId, card.revision, card.issuanceId, encoded],
|
|
342
|
+
options
|
|
309
343
|
);
|
|
310
344
|
if (!inserted.ok) return inserted;
|
|
311
345
|
const stored = await jsonRows(
|
|
312
346
|
output,
|
|
313
347
|
"SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
|
|
314
|
-
[card.cardId, card.revision, card.issuanceId]
|
|
348
|
+
[card.cardId, card.revision, card.issuanceId],
|
|
349
|
+
options
|
|
315
350
|
);
|
|
316
351
|
if (!stored.ok) return stored;
|
|
317
352
|
if (!stored.data[0] || feedJson(stored.data[0]) !== encoded) {
|
|
318
353
|
return feedError("INVALID_INPUT", "A published card identity cannot be silently changed");
|
|
319
354
|
}
|
|
320
355
|
return ok(stored.data[0]);
|
|
321
|
-
}
|
|
356
|
+
}
|
|
322
357
|
var listFeedCards = async (output) => {
|
|
323
358
|
return jsonRows(output, `SELECT card_json FROM feed_cards
|
|
324
359
|
WHERE sequence IN (SELECT MAX(sequence) FROM feed_cards GROUP BY card_id) ORDER BY sequence DESC`);
|
|
@@ -618,14 +653,20 @@ var getFeedHandoff = async (handoff, responseId, recipient) => {
|
|
|
618
653
|
const row = result.data.rows[0];
|
|
619
654
|
if (!row) return ok(null);
|
|
620
655
|
try {
|
|
621
|
-
|
|
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] });
|
|
622
661
|
} catch {
|
|
623
662
|
return feedError("INVALID_INPUT", "Handoff contains invalid response JSON");
|
|
624
663
|
}
|
|
625
664
|
};
|
|
626
665
|
var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver }) => {
|
|
666
|
+
if (dispatchDeadlineDenied(input)) return dispatchDeadlineFailure();
|
|
627
667
|
const response = projectFeedResponseNotification(input);
|
|
628
668
|
if (!recipient || !response.responseId) return feedError("INVALID_INPUT", "Handoff requires a recipient and full response ID");
|
|
669
|
+
if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
|
|
629
670
|
const inserted = await handoff.execute(
|
|
630
671
|
`INSERT INTO feed_handoffs (response_id, recipient, response_json, state) VALUES (?, ?, ?, 'prepared')
|
|
631
672
|
ON CONFLICT (response_id, recipient) DO NOTHING`,
|
|
@@ -638,6 +679,7 @@ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver
|
|
|
638
679
|
return feedError("INVALID_INPUT", "A response handoff identity cannot change its payload");
|
|
639
680
|
}
|
|
640
681
|
if (existing.data.state !== "prepared") return ok(existing.data);
|
|
682
|
+
if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
|
|
641
683
|
const claimed = await handoff.execute(
|
|
642
684
|
"UPDATE feed_handoffs SET state = 'unconfirmed' WHERE response_id = ? AND recipient = ? AND state = 'prepared'",
|
|
643
685
|
[response.responseId, recipient]
|
|
@@ -647,6 +689,7 @@ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver
|
|
|
647
689
|
const current = await getFeedHandoff(handoff, response.responseId, recipient);
|
|
648
690
|
return current.ok && current.data ? ok(current.data) : current.ok ? feedError("NOT_FOUND", "Handoff disappeared before delivery") : current;
|
|
649
691
|
}
|
|
692
|
+
if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
|
|
650
693
|
let state = "unconfirmed";
|
|
651
694
|
try {
|
|
652
695
|
const delivered = await deliver(response);
|
|
@@ -663,6 +706,202 @@ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver
|
|
|
663
706
|
return ok({ ...existing.data, state });
|
|
664
707
|
};
|
|
665
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
|
+
|
|
666
905
|
// src/feed/listener.ts
|
|
667
906
|
var listenFeedResponses = async (options) => {
|
|
668
907
|
const { signal } = options;
|
|
@@ -687,12 +926,12 @@ var listenFeedResponses = async (options) => {
|
|
|
687
926
|
dirty = false;
|
|
688
927
|
let afterSequence = 0;
|
|
689
928
|
while (!signal.aborted) {
|
|
690
|
-
const page = await
|
|
929
|
+
const page = await listFeedResponseDeliveries(options.responses, { recipient: options.recipient, afterSequence, limit: 100, signal });
|
|
691
930
|
if (!page.ok) {
|
|
692
931
|
if (signal.aborted && page.error.code === ErrorCodes.ABORTED) return;
|
|
693
932
|
throw page.error;
|
|
694
933
|
}
|
|
695
|
-
for (const response of page.data) {
|
|
934
|
+
for (const response of page.data.responses) {
|
|
696
935
|
if (signal.aborted) return;
|
|
697
936
|
const handoff = await deliverFeedResponse({
|
|
698
937
|
handoff: options.handoff,
|
|
@@ -700,10 +939,11 @@ var listenFeedResponses = async (options) => {
|
|
|
700
939
|
response,
|
|
701
940
|
deliver: options.deliver
|
|
702
941
|
});
|
|
703
|
-
if (!handoff.ok) throw handoff.error;
|
|
704
|
-
afterSequence = response.sequence;
|
|
942
|
+
if (!handoff.ok && !isDispatchDeadlineFailure(handoff.error)) throw handoff.error;
|
|
705
943
|
}
|
|
706
|
-
if (page.data.
|
|
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;
|
|
707
947
|
}
|
|
708
948
|
} while (dirty && !signal.aborted);
|
|
709
949
|
};
|
|
@@ -759,6 +999,285 @@ var feedResourceNames = (prefix, stableToken) => {
|
|
|
759
999
|
responseResource: `${prefix}/feed_responses_${stableToken}`
|
|
760
1000
|
});
|
|
761
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
|
+
};
|
|
762
1281
|
// Annotate the CommonJS export names for ESM import in node:
|
|
763
1282
|
0 && (module.exports = {
|
|
764
1283
|
appendFeedMessage,
|
|
@@ -772,7 +1291,9 @@ var feedResourceNames = (prefix, stableToken) => {
|
|
|
772
1291
|
getFeedHandoff,
|
|
773
1292
|
initializeFeedDatabases,
|
|
774
1293
|
initializeFeedHandoff,
|
|
1294
|
+
listFeedCardHistory,
|
|
775
1295
|
listFeedCards,
|
|
1296
|
+
listFeedResponseDeliveries,
|
|
776
1297
|
listFeedResponses,
|
|
777
1298
|
listenFeedResponses,
|
|
778
1299
|
newFeedMessageSchema,
|
|
@@ -782,6 +1303,7 @@ var feedResourceNames = (prefix, stableToken) => {
|
|
|
782
1303
|
readFeedResponseHandling,
|
|
783
1304
|
readFeedResponsePayload,
|
|
784
1305
|
recordFeedHandling,
|
|
785
|
-
resolveFeedCard
|
|
1306
|
+
resolveFeedCard,
|
|
1307
|
+
restoreFeedRecords
|
|
786
1308
|
});
|
|
787
1309
|
//# sourceMappingURL=index.cjs.map
|