@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.
@@ -137,6 +137,23 @@ var feedHandlingSchema = z.object({
137
137
  detail: z.record(z.unknown()).optional()
138
138
  });
139
139
 
140
+ // src/feed/dispatch.ts
141
+ import { z as z2 } from "zod";
142
+ var timestamp = z2.string().datetime({ offset: true });
143
+ function validDispatchBefore(value) {
144
+ return value === null || typeof value === "string" && timestamp.safeParse(value).success && Number.isFinite(Date.parse(value));
145
+ }
146
+ function dispatchDeadlineDenied(response) {
147
+ const cutoff = response.dispatchBefore;
148
+ return cutoff !== void 0 && (!validDispatchBefore(cutoff) || typeof cutoff === "string" && Date.now() >= Date.parse(cutoff));
149
+ }
150
+ function dispatchDeadlineFailure() {
151
+ return err({ code: ErrorCodes.PERMISSION_DENIED, service: "feed", message: "The response dispatch deadline does not permit a new handoff.", meta: { reason: "dispatch_deadline" } });
152
+ }
153
+ function isDispatchDeadlineFailure(error) {
154
+ return error.service === "feed" && error.code === ErrorCodes.PERMISSION_DENIED && error.meta?.reason === "dispatch_deadline";
155
+ }
156
+
140
157
  // src/feed/notification.ts
141
158
  var projectFeedResponseNotification = (response) => {
142
159
  const notification = {
@@ -153,6 +170,7 @@ var projectFeedResponseNotification = (response) => {
153
170
  if (response.kind !== "message" && response.supersedesResponseId !== void 0) notification.supersedesResponseId = response.supersedesResponseId;
154
171
  if (response.payloadType !== void 0) notification.payloadType = response.payloadType;
155
172
  if (response.payloadVersion !== void 0) notification.payloadVersion = response.payloadVersion;
173
+ if (validDispatchBefore(response.dispatchBefore)) notification.dispatchBefore = response.dispatchBefore;
156
174
  return notification;
157
175
  };
158
176
 
@@ -207,18 +225,18 @@ function responseFromHeader(row, header) {
207
225
  }
208
226
  return projectFeedResponseNotification(response);
209
227
  }
210
- async function storedResponse(db, responseId) {
228
+ async function storedResponse(db, responseId, options = PRIVATE_RESPONSE_OPTIONS) {
211
229
  try {
212
230
  const result = await db.query(`SELECT r.sequence, r.response_json, p.payload_type, p.payload_version, p.payload_json
213
231
  FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
214
- WHERE r.response_id = ?`, [responseId], PRIVATE_RESPONSE_OPTIONS);
232
+ WHERE r.response_id = ?`, [responseId], options);
215
233
  if (!result.ok) return responseDataFailure("Could not read the saved feed response", result.error);
216
234
  const row = result.data.rows[0];
217
235
  if (!row) return ok(void 0);
218
236
  const header = feedResponseHeaderSchema.parse(JSON.parse(String(row[1])));
219
237
  const response = responseFromHeader(row, header);
220
238
  const payload = row[2] === null || row[2] === void 0 ? void 0 : feedResponsePayloadSchema.parse({ type: row[2], version: Number(row[3]), data: JSON.parse(String(row[4])) });
221
- return ok({ response, headerJson: feedJson(header), payload });
239
+ return ok({ response, rawHeaderJson: String(row[1]), headerJson: feedJson(header), payload });
222
240
  } catch (error) {
223
241
  return responseDataFailure("Could not read the saved feed response", error);
224
242
  }
@@ -248,30 +266,44 @@ var initializeFeedDatabases = async ({ output, responses }) => {
248
266
  { sql: "CREATE INDEX IF NOT EXISTS feed_responses_card ON feed_responses (card_id, revision, issuance_id, sequence)" },
249
267
  { sql: `CREATE TABLE IF NOT EXISTS feed_response_payloads (
250
268
  response_id TEXT PRIMARY KEY, payload_type TEXT NOT NULL, payload_version INTEGER NOT NULL, payload_json TEXT NOT NULL
269
+ )` },
270
+ { sql: `CREATE TABLE IF NOT EXISTS feed_response_imports (
271
+ response_id TEXT PRIMARY KEY, card_id TEXT NOT NULL, revision INTEGER NOT NULL, issuance_id TEXT NOT NULL,
272
+ content_sha256 TEXT NOT NULL
273
+ )` },
274
+ { sql: `CREATE TABLE IF NOT EXISTS feed_response_origins (
275
+ response_id TEXT NOT NULL, source_json TEXT NOT NULL, PRIMARY KEY (response_id, source_json)
276
+ )` },
277
+ { sql: `CREATE TABLE IF NOT EXISTS feed_response_delivery_policies (
278
+ response_id TEXT NOT NULL, recipient TEXT NOT NULL, policy_json TEXT NOT NULL,
279
+ PRIMARY KEY (response_id, recipient, policy_json)
251
280
  )` }
252
281
  ]);
253
282
  return responseSchema.ok ? ok(void 0) : responseSchema;
254
283
  };
255
- var publishFeedCard = async (output, card) => {
284
+ var publishFeedCard = (output, card) => storeFeedCard(output, card);
285
+ async function storeFeedCard(output, card, options) {
256
286
  if (!validIdentity(card)) return feedError("INVALID_INPUT", "A card requires its full ID, positive revision and issuance ID");
257
287
  const encoded = feedJson(card);
258
288
  const inserted = await output.execute(
259
289
  `INSERT INTO feed_cards (card_id, revision, issuance_id, card_json) VALUES (?, ?, ?, ?)
260
290
  ON CONFLICT (card_id, revision, issuance_id) DO NOTHING`,
261
- [card.cardId, card.revision, card.issuanceId, encoded]
291
+ [card.cardId, card.revision, card.issuanceId, encoded],
292
+ options
262
293
  );
263
294
  if (!inserted.ok) return inserted;
264
295
  const stored = await jsonRows(
265
296
  output,
266
297
  "SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
267
- [card.cardId, card.revision, card.issuanceId]
298
+ [card.cardId, card.revision, card.issuanceId],
299
+ options
268
300
  );
269
301
  if (!stored.ok) return stored;
270
302
  if (!stored.data[0] || feedJson(stored.data[0]) !== encoded) {
271
303
  return feedError("INVALID_INPUT", "A published card identity cannot be silently changed");
272
304
  }
273
305
  return ok(stored.data[0]);
274
- };
306
+ }
275
307
  var listFeedCards = async (output) => {
276
308
  return jsonRows(output, `SELECT card_json FROM feed_cards
277
309
  WHERE sequence IN (SELECT MAX(sequence) FROM feed_cards GROUP BY card_id) ORDER BY sequence DESC`);
@@ -571,14 +603,20 @@ var getFeedHandoff = async (handoff, responseId, recipient) => {
571
603
  const row = result.data.rows[0];
572
604
  if (!row) return ok(null);
573
605
  try {
574
- return ok({ responseId, recipient, response: projectFeedResponseNotification(JSON.parse(row[0])), state: row[1] });
606
+ const parsed = JSON.parse(row[0]);
607
+ if (parsed.dispatchBefore !== void 0 && !validDispatchBefore(parsed.dispatchBefore)) {
608
+ return feedError("INVALID_INPUT", "Handoff contains invalid dispatch metadata");
609
+ }
610
+ return ok({ responseId, recipient, response: projectFeedResponseNotification(parsed), state: row[1] });
575
611
  } catch {
576
612
  return feedError("INVALID_INPUT", "Handoff contains invalid response JSON");
577
613
  }
578
614
  };
579
615
  var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver }) => {
616
+ if (dispatchDeadlineDenied(input)) return dispatchDeadlineFailure();
580
617
  const response = projectFeedResponseNotification(input);
581
618
  if (!recipient || !response.responseId) return feedError("INVALID_INPUT", "Handoff requires a recipient and full response ID");
619
+ if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
582
620
  const inserted = await handoff.execute(
583
621
  `INSERT INTO feed_handoffs (response_id, recipient, response_json, state) VALUES (?, ?, ?, 'prepared')
584
622
  ON CONFLICT (response_id, recipient) DO NOTHING`,
@@ -591,6 +629,7 @@ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver
591
629
  return feedError("INVALID_INPUT", "A response handoff identity cannot change its payload");
592
630
  }
593
631
  if (existing.data.state !== "prepared") return ok(existing.data);
632
+ if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
594
633
  const claimed = await handoff.execute(
595
634
  "UPDATE feed_handoffs SET state = 'unconfirmed' WHERE response_id = ? AND recipient = ? AND state = 'prepared'",
596
635
  [response.responseId, recipient]
@@ -600,6 +639,7 @@ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver
600
639
  const current = await getFeedHandoff(handoff, response.responseId, recipient);
601
640
  return current.ok && current.data ? ok(current.data) : current.ok ? feedError("NOT_FOUND", "Handoff disappeared before delivery") : current;
602
641
  }
642
+ if (dispatchDeadlineDenied(response)) return dispatchDeadlineFailure();
603
643
  let state = "unconfirmed";
604
644
  try {
605
645
  const delivered = await deliver(response);
@@ -616,6 +656,202 @@ var deliverFeedResponse = async ({ handoff, recipient, response: input, deliver
616
656
  return ok({ ...existing.data, state });
617
657
  };
618
658
 
659
+ // src/feed/restore-metadata.ts
660
+ import { z as z3 } from "zod";
661
+ var restoreOriginSchema = z3.object({
662
+ sourceSpaceId: z3.string().min(1),
663
+ namespace: z3.string().min(1),
664
+ snapshotId: z3.string().min(1),
665
+ sourceKey: z3.string().min(1),
666
+ eventKey: z3.string().min(1).optional()
667
+ }).strict();
668
+ var restorePolicySchema = z3.object({
669
+ recipient: z3.string().min(1),
670
+ state: z3.enum(["pending", "held", "delivered", "unconfirmed", "refused", "unknown"]),
671
+ dispatch: z3.enum(["never", "resolved-pending", "held"]),
672
+ resolvedOwnerMapping: z3.boolean(),
673
+ dispatchBefore: z3.custom(validDispatchBefore),
674
+ source: restoreOriginSchema
675
+ }).strict();
676
+ function restoreContent(input, inputPayload) {
677
+ const response = feedResponseHeaderSchema.parse(input);
678
+ const headerJson = feedJson(response);
679
+ if (feedJson(input) !== headerJson) throw new Error("Invalid restored response header fields");
680
+ const payload = inputPayload === void 0 ? void 0 : feedResponsePayloadSchema.parse(inputPayload);
681
+ const payloadJson = payload === void 0 ? void 0 : feedJson(payload.data);
682
+ const envelope = payload === void 0 ? "null" : `{"data":${payloadJson},"type":${JSON.stringify(payload.type)},"version":${payload.version}}`;
683
+ return { response, headerJson, payload, payloadJson, canonical: `{"payload":${envelope},"response":${headerJson}}` };
684
+ }
685
+ async function contentSha256(content) {
686
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)));
687
+ let hex = "";
688
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
689
+ return hex;
690
+ }
691
+ function sameOriginBinding(a, b) {
692
+ return a.sourceSpaceId === b.sourceSpaceId && a.namespace === b.namespace && a.sourceKey === b.sourceKey && a.eventKey === b.eventKey;
693
+ }
694
+ function restoreCancelled(signal) {
695
+ return signal?.aborted ? feedError(ErrorCodes.ABORTED, "Feed restore or policy read was aborted") : void 0;
696
+ }
697
+ function baselinePolicy(source) {
698
+ return { recipient: "*", state: "unknown", dispatch: "never", resolvedOwnerMapping: false, dispatchBefore: null, source };
699
+ }
700
+ async function readRestoreMetadata(responses, responseIds, options) {
701
+ const cancelled = restoreCancelled(options.signal);
702
+ if (cancelled) return cancelled;
703
+ const metadata = /* @__PURE__ */ new Map();
704
+ for (const id of responseIds) metadata.set(id, { origins: /* @__PURE__ */ new Map(), policies: [] });
705
+ const selected = responseIds.length ? `VALUES ${responseIds.map(() => "(?)").join(",")}` : "SELECT CAST(NULL AS TEXT) WHERE 0";
706
+ try {
707
+ const result = await responses.query(`WITH selected(response_id) AS (${selected})
708
+ SELECT response_id, 'import', card_id, revision, issuance_id, content_sha256
709
+ FROM feed_response_imports WHERE response_id IN (SELECT response_id FROM selected)
710
+ UNION ALL SELECT response_id, 'origin', source_json, NULL, NULL, NULL
711
+ FROM feed_response_origins WHERE response_id IN (SELECT response_id FROM selected)
712
+ UNION ALL SELECT response_id, 'policy', recipient, policy_json, NULL, NULL
713
+ FROM feed_response_delivery_policies WHERE response_id IN (SELECT response_id FROM selected)`, responseIds, options);
714
+ if (!result.ok) return responseDataFailure("Could not read feed restore policy metadata", result.error);
715
+ const aborted = restoreCancelled(options.signal);
716
+ if (aborted) return aborted;
717
+ for (const row of result.data.rows) {
718
+ const target = metadata.get(String(row[0]));
719
+ if (!target) throw new Error("Policy metadata does not belong to its response page");
720
+ if (row[1] === "import") {
721
+ 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])) {
722
+ throw new Error("Invalid import marker");
723
+ }
724
+ target.marker = { cardId: row[2], revision: Number(row[3]), issuanceId: row[4], contentSha256: row[5] };
725
+ } else if (row[1] === "origin") {
726
+ const source = restoreOriginSchema.parse(JSON.parse(String(row[2])));
727
+ target.origins.set(feedJson(source), source);
728
+ } else if (row[1] === "policy") {
729
+ const policy = restorePolicySchema.parse(JSON.parse(String(row[3])));
730
+ if (row[2] !== policy.recipient) throw new Error("Policy recipient does not match its stored key");
731
+ target.policies.push(policy);
732
+ } else throw new Error("Unknown restore metadata row");
733
+ }
734
+ return ok(metadata);
735
+ } catch (error) {
736
+ return responseDataFailure("Could not verify feed restore policy metadata", error);
737
+ }
738
+ }
739
+ function verifyRestoreMetadata(metadata, response, contentHash) {
740
+ if (!metadata.marker) {
741
+ if (metadata.origins.size || metadata.policies.length) throw new Error("Orphan restore metadata");
742
+ return false;
743
+ }
744
+ if (!sameFeedIdentity(metadata.marker, response) || contentHash !== void 0 && metadata.marker.contentSha256 !== contentHash || !metadata.origins.size) {
745
+ throw new Error("Import identity, content or source reference is unverified");
746
+ }
747
+ let binding;
748
+ const defaults = /* @__PURE__ */ new Set();
749
+ for (const source of metadata.origins.values()) {
750
+ if (binding && !sameOriginBinding(binding, source)) throw new Error("Conflicting import source bindings");
751
+ binding = source;
752
+ }
753
+ for (const policy of metadata.policies) {
754
+ const source = feedJson(policy.source);
755
+ if (!metadata.origins.has(source)) throw new Error("Policy references an unregistered import origin");
756
+ if (policy.recipient === "*") {
757
+ if (feedJson(policy) !== feedJson(baselinePolicy(policy.source))) throw new Error("Invalid default import policy");
758
+ defaults.add(source);
759
+ }
760
+ }
761
+ for (const source of metadata.origins.keys()) if (!defaults.has(source)) throw new Error("Missing default import policy");
762
+ return true;
763
+ }
764
+
765
+ // src/feed/delivery-policy.ts
766
+ function permittedDeadline(metadata, recipient) {
767
+ const evidence = /* @__PURE__ */ new Map();
768
+ for (const policy of metadata.policies) {
769
+ if (policy.recipient !== recipient) continue;
770
+ const source = feedJson(policy.source);
771
+ const rows = evidence.get(source);
772
+ if (rows) rows.push(policy);
773
+ else evidence.set(source, [policy]);
774
+ }
775
+ let cutoff;
776
+ for (const source of metadata.origins.keys()) {
777
+ const rows = evidence.get(source);
778
+ if (!rows?.length) return { allowed: false };
779
+ for (const policy of rows) {
780
+ if (policy.state !== "pending" || policy.dispatch !== "resolved-pending" || !policy.resolvedOwnerMapping) return { allowed: false };
781
+ if (cutoff === void 0) cutoff = policy.dispatchBefore;
782
+ else if (cutoff !== policy.dispatchBefore) return { allowed: false };
783
+ if (typeof policy.dispatchBefore === "string" && Date.now() >= Date.parse(policy.dispatchBefore)) return { allowed: false };
784
+ }
785
+ }
786
+ return cutoff === void 0 ? { allowed: false } : { allowed: true, cutoff };
787
+ }
788
+ var listFeedResponseDeliveries = async (responses, options) => {
789
+ try {
790
+ const after = options.afterSequence ?? 0;
791
+ const limit = options.limit ?? 100;
792
+ if (!options.recipient || options.recipient === "*" || !Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) {
793
+ return feedError("INVALID_INPUT", "Delivery pages require an exact recipient and valid sequence pagination");
794
+ }
795
+ const privateOptions = { redactErrors: true, signal: options.signal };
796
+ const cancelled = restoreCancelled(options.signal);
797
+ if (cancelled) return cancelled;
798
+ const result = await responses.query(`SELECT r.sequence, r.response_json, p.payload_type, p.payload_version,
799
+ p.payload_json, r.card_id, r.revision, r.issuance_id, r.response_id
800
+ FROM feed_responses r LEFT JOIN feed_response_payloads p ON p.response_id = r.response_id
801
+ WHERE r.sequence > ? ORDER BY r.sequence LIMIT ?`, [after, limit + 1], privateOptions);
802
+ if (!result.ok) return responseDataFailure("Could not read feed response delivery headers", result.error);
803
+ const stopped = restoreCancelled(options.signal);
804
+ if (stopped) return stopped;
805
+ const rows = result.data.rows.slice(0, limit);
806
+ const headers = [];
807
+ const ids = [];
808
+ const seen = /* @__PURE__ */ new Set();
809
+ let nextSequence = after;
810
+ for (const row of rows) {
811
+ const response = responseFromRow(row);
812
+ 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]) })) {
813
+ throw new Error("Response page identity or ordering is unverified");
814
+ }
815
+ seen.add(response.responseId);
816
+ nextSequence = response.sequence;
817
+ headers.push(response);
818
+ ids.push(response.responseId);
819
+ }
820
+ const metadataResult = await readRestoreMetadata(responses, ids, privateOptions);
821
+ if (!metadataResult.ok) return metadataResult;
822
+ const deliveries = [];
823
+ for (let index = 0; index < headers.length; index += 1) {
824
+ const response = headers[index];
825
+ const metadata = metadataResult.data.get(response.responseId);
826
+ if (!verifyRestoreMetadata(metadata, response)) {
827
+ deliveries.push(response);
828
+ continue;
829
+ }
830
+ const row = rows[index];
831
+ let payload;
832
+ if (row[2] !== null && row[2] !== void 0) {
833
+ payload = feedResponsePayloadSchema.parse({ type: row[2], version: row[3], data: JSON.parse(String(row[4])) });
834
+ } else if (row[3] !== null && row[3] !== void 0 || row[4] !== null && row[4] !== void 0) {
835
+ throw new Error("Imported payload presence is unverified");
836
+ }
837
+ const content = restoreContent(JSON.parse(String(row[1])), payload);
838
+ const hash = await contentSha256(content.canonical);
839
+ const aborted2 = restoreCancelled(options.signal);
840
+ if (aborted2) return aborted2;
841
+ verifyRestoreMetadata(metadata, response, hash);
842
+ const permission = permittedDeadline(metadata, options.recipient);
843
+ if (permission.allowed) {
844
+ deliveries.push(projectFeedResponseNotification({ ...response, dispatchBefore: permission.cutoff }));
845
+ }
846
+ }
847
+ const aborted = restoreCancelled(options.signal);
848
+ if (aborted) return aborted;
849
+ return ok({ responses: deliveries, nextSequence, hasMore: result.data.rows.length > limit });
850
+ } catch (error) {
851
+ return responseDataFailure("Could not verify feed response delivery policy", error);
852
+ }
853
+ };
854
+
619
855
  // src/feed/listener.ts
620
856
  var listenFeedResponses = async (options) => {
621
857
  const { signal } = options;
@@ -640,12 +876,12 @@ var listenFeedResponses = async (options) => {
640
876
  dirty = false;
641
877
  let afterSequence = 0;
642
878
  while (!signal.aborted) {
643
- const page = await listFeedResponses(options.responses, { afterSequence, limit: 100, signal });
879
+ const page = await listFeedResponseDeliveries(options.responses, { recipient: options.recipient, afterSequence, limit: 100, signal });
644
880
  if (!page.ok) {
645
881
  if (signal.aborted && page.error.code === ErrorCodes.ABORTED) return;
646
882
  throw page.error;
647
883
  }
648
- for (const response of page.data) {
884
+ for (const response of page.data.responses) {
649
885
  if (signal.aborted) return;
650
886
  const handoff = await deliverFeedResponse({
651
887
  handoff: options.handoff,
@@ -653,10 +889,11 @@ var listenFeedResponses = async (options) => {
653
889
  response,
654
890
  deliver: options.deliver
655
891
  });
656
- if (!handoff.ok) throw handoff.error;
657
- afterSequence = response.sequence;
892
+ if (!handoff.ok && !isDispatchDeadlineFailure(handoff.error)) throw handoff.error;
658
893
  }
659
- if (page.data.length < 100) break;
894
+ if (page.data.hasMore && page.data.nextSequence <= afterSequence) throw new Error("Delivery page did not advance a verified cursor");
895
+ afterSequence = page.data.nextSequence;
896
+ if (!page.data.hasMore) break;
660
897
  }
661
898
  } while (dirty && !signal.aborted);
662
899
  };
@@ -712,6 +949,285 @@ var feedResourceNames = (prefix, stableToken) => {
712
949
  responseResource: `${prefix}/feed_responses_${stableToken}`
713
950
  });
714
951
  };
952
+
953
+ // src/feed/restore.ts
954
+ import { z as z4 } from "zod";
955
+ var envelopeSchema = z4.object({
956
+ response: z4.unknown(),
957
+ payload: z4.unknown().optional(),
958
+ origin: restoreOriginSchema,
959
+ deliveries: z4.array(restorePolicySchema).optional()
960
+ }).strict();
961
+ var recordsSchema = z4.object({ cards: z4.array(z4.unknown()), responses: z4.array(envelopeSchema) }).strict();
962
+ function markerGuard(content, hash) {
963
+ const response = content.response;
964
+ return {
965
+ sql: `EXISTS (SELECT 1 FROM feed_response_imports WHERE response_id = ? AND card_id = ?
966
+ AND revision = ? AND issuance_id = ? AND content_sha256 = ?)`,
967
+ params: [response.responseId, response.cardId, response.revision, response.issuanceId, hash]
968
+ };
969
+ }
970
+ function bindingGuard(responseId, source) {
971
+ return {
972
+ sql: `NOT EXISTS (SELECT 1 FROM feed_response_origins WHERE response_id = ? AND (
973
+ json_extract(source_json, '$.sourceSpaceId') IS NOT ? OR json_extract(source_json, '$.namespace') IS NOT ?
974
+ OR json_extract(source_json, '$.sourceKey') IS NOT ? OR json_extract(source_json, '$.eventKey') IS NOT ?))`,
975
+ params: [responseId, source.sourceSpaceId, source.namespace, source.sourceKey, source.eventKey ?? null]
976
+ };
977
+ }
978
+ function headerGuard(content) {
979
+ const response = content.response;
980
+ return {
981
+ sql: `EXISTS (SELECT 1 FROM feed_responses r WHERE r.response_id = ? AND r.card_id = ? AND r.revision = ?
982
+ AND r.issuance_id = ? AND r.response_json = ?)`,
983
+ params: [response.responseId, response.cardId, response.revision, response.issuanceId, content.headerJson]
984
+ };
985
+ }
986
+ function payloadGuard(content) {
987
+ const responseId = content.response.responseId;
988
+ return content.payload === void 0 ? { sql: "NOT EXISTS (SELECT 1 FROM feed_response_payloads WHERE response_id = ?)", params: [responseId] } : {
989
+ sql: `EXISTS (SELECT 1 FROM feed_response_payloads WHERE response_id = ? AND payload_type = ?
990
+ AND payload_version = ? AND payload_json = ?)`,
991
+ params: [responseId, content.payload.type, content.payload.version, content.payloadJson]
992
+ };
993
+ }
994
+ function combined(...guards) {
995
+ return { sql: guards.map((guard) => `(${guard.sql})`).join(" AND "), params: guards.flatMap((guard) => guard.params) };
996
+ }
997
+ function restoreStatements(group, hash) {
998
+ const { content } = group;
999
+ const response = content.response;
1000
+ const first = group.origins.values().next().value;
1001
+ const claim = combined(markerGuard(content, hash), bindingGuard(response.responseId, first.origin));
1002
+ const header = headerGuard(content);
1003
+ const complete = combined(claim, header, payloadGuard(content));
1004
+ const committed = [complete];
1005
+ const statements = [{
1006
+ // Claim precedes the header under the same transaction. A committed live header cannot be reclassified.
1007
+ sql: `INSERT INTO feed_response_imports (response_id, card_id, revision, issuance_id, content_sha256)
1008
+ SELECT ?, ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM feed_responses WHERE response_id = ?)
1009
+ AND NOT EXISTS (SELECT 1 FROM feed_response_payloads WHERE response_id = ?)
1010
+ AND NOT EXISTS (SELECT 1 FROM feed_response_origins WHERE response_id = ?)
1011
+ AND NOT EXISTS (SELECT 1 FROM feed_response_delivery_policies WHERE response_id = ?)
1012
+ ON CONFLICT (response_id) DO NOTHING`,
1013
+ params: [
1014
+ response.responseId,
1015
+ response.cardId,
1016
+ response.revision,
1017
+ response.issuanceId,
1018
+ hash,
1019
+ response.responseId,
1020
+ response.responseId,
1021
+ response.responseId,
1022
+ response.responseId
1023
+ ]
1024
+ }, {
1025
+ sql: `INSERT INTO feed_responses (response_id, card_id, revision, issuance_id, response_json)
1026
+ SELECT ?, ?, ?, ?, ? WHERE ${claim.sql} ON CONFLICT (response_id) DO NOTHING`,
1027
+ params: [response.responseId, response.cardId, response.revision, response.issuanceId, content.headerJson, ...claim.params]
1028
+ }];
1029
+ if (content.payload !== void 0) statements.push({
1030
+ sql: `INSERT INTO feed_response_payloads (response_id, payload_type, payload_version, payload_json)
1031
+ SELECT ?, ?, ?, ? WHERE ${combined(claim, header).sql} ON CONFLICT (response_id) DO NOTHING`,
1032
+ params: [response.responseId, content.payload.type, content.payload.version, content.payloadJson, ...claim.params, ...header.params]
1033
+ });
1034
+ for (const [sourceJson, evidence] of group.origins) {
1035
+ statements.push({
1036
+ sql: `INSERT INTO feed_response_origins (response_id, source_json)
1037
+ SELECT ?, ? WHERE ${complete.sql} ON CONFLICT (response_id, source_json) DO NOTHING`,
1038
+ params: [response.responseId, sourceJson, ...complete.params]
1039
+ });
1040
+ committed.push({
1041
+ sql: "EXISTS (SELECT 1 FROM feed_response_origins WHERE response_id = ? AND source_json = ?)",
1042
+ params: [response.responseId, sourceJson]
1043
+ });
1044
+ for (const policy of [baselinePolicy(evidence.origin), ...evidence.policies.values()]) {
1045
+ statements.push({
1046
+ sql: `INSERT INTO feed_response_delivery_policies (response_id, recipient, policy_json)
1047
+ SELECT ?, ?, ? WHERE ${complete.sql} AND EXISTS (
1048
+ SELECT 1 FROM feed_response_origins WHERE response_id = ? AND source_json = ?)
1049
+ ON CONFLICT (response_id, recipient, policy_json) DO NOTHING`,
1050
+ params: [response.responseId, policy.recipient, feedJson(policy), ...complete.params, response.responseId, sourceJson]
1051
+ });
1052
+ committed.push({
1053
+ sql: "EXISTS (SELECT 1 FROM feed_response_delivery_policies WHERE response_id = ? AND recipient = ? AND policy_json = ?)",
1054
+ params: [response.responseId, policy.recipient, feedJson(policy)]
1055
+ });
1056
+ }
1057
+ }
1058
+ const proof = combined(...committed);
1059
+ statements.push({
1060
+ sql: `INSERT INTO feed_response_imports (response_id, card_id, revision, issuance_id, content_sha256)
1061
+ SELECT ?, NULL, NULL, NULL, NULL WHERE NOT (${proof.sql})`,
1062
+ params: [response.responseId, ...proof.params]
1063
+ });
1064
+ return statements;
1065
+ }
1066
+ async function restoreGroup(output, responses, group, options) {
1067
+ const { content } = group;
1068
+ const response = content.response;
1069
+ const card = await output.query(
1070
+ "SELECT card_json FROM feed_cards WHERE card_id = ? AND revision = ? AND issuance_id = ?",
1071
+ [response.cardId, response.revision, response.issuanceId],
1072
+ options
1073
+ );
1074
+ if (!card.ok) return responseDataFailure("Could not verify a restored response card issuance", card.error);
1075
+ const cardReadCancelled = restoreCancelled(options.signal);
1076
+ if (cardReadCancelled) return cardReadCancelled;
1077
+ if (!card.data.rows[0] || !sameFeedIdentity(feedCardSchema.parse(JSON.parse(card.data.rows[0][0])), response)) {
1078
+ return feedError("INVALID_INPUT", "Restored response requires a published full card issuance");
1079
+ }
1080
+ const hash = await contentSha256(content.canonical);
1081
+ const cancelled = restoreCancelled(options.signal);
1082
+ if (cancelled) return cancelled;
1083
+ const previous = await storedResponse(responses, response.responseId, options);
1084
+ if (!previous.ok) return previous;
1085
+ const previousMetadata = await readRestoreMetadata(responses, [response.responseId], options);
1086
+ if (!previousMetadata.ok) return previousMetadata;
1087
+ const before = previousMetadata.data.get(response.responseId);
1088
+ if (previous.data) {
1089
+ if (!verifyRestoreMetadata(before, response, hash)) return feedError("INVALID_INPUT", "A live response ID cannot be restored or reclassified");
1090
+ if (restoreContent(JSON.parse(previous.data.rawHeaderJson), previous.data.payload).canonical !== content.canonical) {
1091
+ return feedError("INVALID_INPUT", "A restored response ID cannot change header or private payload");
1092
+ }
1093
+ const known = before.origins.values().next().value;
1094
+ for (const evidence of group.origins.values()) if (!sameOriginBinding(known, evidence.origin)) {
1095
+ return feedError("INVALID_INPUT", "A restored response cannot alias its source binding");
1096
+ }
1097
+ } else if (before.marker || before.origins.size || before.policies.length) {
1098
+ return feedError("INVALID_INPUT", "Orphan restore metadata cannot authorize a new header");
1099
+ }
1100
+ const aborted = restoreCancelled(options.signal);
1101
+ if (aborted) return aborted;
1102
+ let failure;
1103
+ let failed = false;
1104
+ try {
1105
+ const saved = await responses.batch(restoreStatements(group, hash), options);
1106
+ if (!saved.ok) {
1107
+ failed = true;
1108
+ failure = saved.error;
1109
+ }
1110
+ } catch (error) {
1111
+ failed = true;
1112
+ failure = error;
1113
+ }
1114
+ const stopped = restoreCancelled(options.signal);
1115
+ if (stopped) return failed ? responseDataFailure("Could not restore response and atomic import policy", failure) : stopped;
1116
+ const stored = await storedResponse(responses, response.responseId, options);
1117
+ if (!stored.ok) return failed && options.signal?.aborted && stored.error.code === ErrorCodes.ABORTED ? responseDataFailure("Could not restore response and atomic import policy", failure) : stored;
1118
+ const metadata = await readRestoreMetadata(responses, [response.responseId], options);
1119
+ if (!metadata.ok) return failed && options.signal?.aborted && metadata.error.code === ErrorCodes.ABORTED ? responseDataFailure("Could not restore response and atomic import policy", failure) : metadata;
1120
+ const actual = metadata.data.get(response.responseId);
1121
+ if (!stored.data) return responseDataFailure("Could not restore response and atomic import policy", failure);
1122
+ if (!verifyRestoreMetadata(actual, response, hash) || restoreContent(JSON.parse(stored.data.rawHeaderJson), stored.data.payload).canonical !== content.canonical) {
1123
+ return feedError("INVALID_INPUT", "Restore conflicts with an existing live or different response");
1124
+ }
1125
+ const policies = new Set(actual.policies.map(feedJson));
1126
+ for (const [sourceJson, evidence] of group.origins) {
1127
+ if (!actual.origins.has(sourceJson) || !policies.has(feedJson(baselinePolicy(evidence.origin)))) {
1128
+ return failed ? responseDataFailure("Could not restore atomic provenance and default policy", failure) : feedError("INVALID_INPUT", "Restore source binding conflicts with existing provenance");
1129
+ }
1130
+ for (const policy of evidence.policies.keys()) if (!policies.has(policy)) {
1131
+ return responseDataFailure("Could not retain requested immutable delivery evidence", failure);
1132
+ }
1133
+ }
1134
+ const readbackCancelled = restoreCancelled(options.signal);
1135
+ if (readbackCancelled) return failed ? responseDataFailure("Could not restore response and atomic import policy", failure) : readbackCancelled;
1136
+ return ok(void 0);
1137
+ }
1138
+ var restoreFeedRecords = async (ownerPair, input, options = {}) => {
1139
+ const privateOptions = { redactErrors: true, signal: options.signal };
1140
+ try {
1141
+ const cancelled = restoreCancelled(options.signal);
1142
+ if (cancelled) return cancelled;
1143
+ const parsed = recordsSchema.parse(input);
1144
+ const cards = parsed.cards.map((card) => {
1145
+ const valid = feedCardSchema.parse(card);
1146
+ if (valid.payload !== void 0) valid.payload = card.payload;
1147
+ if (feedJson(valid) !== feedJson(card)) throw new Error("Invalid restored card fields");
1148
+ return valid;
1149
+ });
1150
+ const groups = /* @__PURE__ */ new Map();
1151
+ for (const record of parsed.responses) {
1152
+ const content = restoreContent(record.response, record.payload);
1153
+ const id = content.response.responseId;
1154
+ let group = groups.get(id);
1155
+ if (group && group.content.canonical !== content.canonical) throw new Error("Conflicting restored response content");
1156
+ if (!group) {
1157
+ group = { content, origins: /* @__PURE__ */ new Map() };
1158
+ groups.set(id, group);
1159
+ }
1160
+ const first = group.origins.values().next().value;
1161
+ if (first && !sameOriginBinding(first.origin, record.origin)) throw new Error("Conflicting restored response binding");
1162
+ const sourceJson = feedJson(record.origin);
1163
+ let evidence = group.origins.get(sourceJson);
1164
+ if (!evidence) {
1165
+ evidence = { origin: record.origin, policies: /* @__PURE__ */ new Map() };
1166
+ group.origins.set(sourceJson, evidence);
1167
+ }
1168
+ for (const policy of record.deliveries ?? []) {
1169
+ if (policy.recipient === "*" || feedJson(policy.source) !== sourceJson) throw new Error("Policy must belong to its exact origin and recipient");
1170
+ evidence.policies.set(feedJson(policy), policy);
1171
+ }
1172
+ }
1173
+ for (const card of cards) {
1174
+ const stopped = restoreCancelled(options.signal);
1175
+ if (stopped) return stopped;
1176
+ const saved = await storeFeedCard(ownerPair.output, card, privateOptions);
1177
+ if (!saved.ok) return responseDataFailure("Could not publish a restored card", saved.error);
1178
+ }
1179
+ for (const group of groups.values()) {
1180
+ const stopped = restoreCancelled(options.signal);
1181
+ if (stopped) return stopped;
1182
+ const saved = await restoreGroup(ownerPair.output, ownerPair.responses, group, privateOptions);
1183
+ if (!saved.ok) return saved;
1184
+ }
1185
+ return restoreCancelled(options.signal) ?? ok(void 0);
1186
+ } catch (error) {
1187
+ return responseDataFailure("Could not validate or restore generic feed records", error);
1188
+ }
1189
+ };
1190
+
1191
+ // src/feed/history.ts
1192
+ function validIdentity2(value) {
1193
+ 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;
1194
+ }
1195
+ var listFeedCardHistory = async (output, options = {}) => {
1196
+ try {
1197
+ const { afterIdentity, signal } = options;
1198
+ const limit = options.limit === void 0 ? 100 : options.limit;
1199
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3 || afterIdentity !== void 0 && !validIdentity2(afterIdentity)) {
1200
+ return feedError(ErrorCodes.INVALID_INPUT, "Card history requires a full identity cursor and valid page limit");
1201
+ }
1202
+ if (signal?.aborted) return feedError(ErrorCodes.ABORTED, "Card history read was cancelled");
1203
+ const params = afterIdentity ? [afterIdentity.cardId, afterIdentity.revision, afterIdentity.issuanceId, limit + 1] : [limit + 1];
1204
+ const result = await output.query(`SELECT card_id, revision, issuance_id, card_json FROM feed_cards
1205
+ ${afterIdentity ? "WHERE (card_id COLLATE BINARY, revision, issuance_id COLLATE BINARY) > (?, ?, ?)" : ""}
1206
+ ORDER BY card_id COLLATE BINARY, revision, issuance_id COLLATE BINARY LIMIT ?`, params, { redactErrors: true, signal });
1207
+ if (!result.ok) return responseDataFailure("Could not read feed card history", result.error);
1208
+ if (signal?.aborted) return feedError(ErrorCodes.ABORTED, "Card history read was cancelled");
1209
+ const cards = [];
1210
+ const count = Math.min(limit, result.data.rows.length);
1211
+ for (let index = 0; index < count; index += 1) {
1212
+ const row = result.data.rows[index];
1213
+ if (typeof row[3] !== "string") throw new Error("Stored card envelope is invalid");
1214
+ const card = JSON.parse(row[3]);
1215
+ feedCardSchema.parse(card);
1216
+ if (!validIdentity2(card) || row[0] !== card.cardId || row[1] !== card.revision || row[2] !== card.issuanceId) {
1217
+ throw new Error("Stored card identity does not match its key");
1218
+ }
1219
+ cards.push(card);
1220
+ }
1221
+ const last = cards[cards.length - 1];
1222
+ return ok({
1223
+ cards,
1224
+ nextIdentity: last ? { cardId: last.cardId, revision: last.revision, issuanceId: last.issuanceId } : null,
1225
+ hasMore: result.data.rows.length > limit
1226
+ });
1227
+ } catch (error) {
1228
+ return responseDataFailure("Could not verify feed card history", error);
1229
+ }
1230
+ };
715
1231
  export {
716
1232
  appendFeedMessage,
717
1233
  appendFeedResponse,
@@ -724,7 +1240,9 @@ export {
724
1240
  getFeedHandoff,
725
1241
  initializeFeedDatabases,
726
1242
  initializeFeedHandoff,
1243
+ listFeedCardHistory,
727
1244
  listFeedCards,
1245
+ listFeedResponseDeliveries,
728
1246
  listFeedResponses,
729
1247
  listenFeedResponses,
730
1248
  newFeedMessageSchema,
@@ -734,6 +1252,7 @@ export {
734
1252
  readFeedResponseHandling,
735
1253
  readFeedResponsePayload,
736
1254
  recordFeedHandling,
737
- resolveFeedCard
1255
+ resolveFeedCard,
1256
+ restoreFeedRecords
738
1257
  };
739
1258
  //# sourceMappingURL=index.js.map