@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.
@@ -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], PRIVATE_RESPONSE_OPTIONS);
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 = async (output, card) => {
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
- return ok({ responseId, recipient, response: projectFeedResponseNotification(JSON.parse(row[0])), state: row[1] });
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 listFeedResponses(options.responses, { afterSequence, limit: 100, signal });
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.length < 100) break;
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