@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.js
CHANGED
|
@@ -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],
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|