@gmickel/gno 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/assets/skill/SKILL.md +46 -15
- package/package.json +2 -1
- package/spec/cli.md +144 -0
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +48 -0
- package/spec/mcp.md +22 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +195 -0
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/ingestion/sync.ts +214 -165
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/index.ts +29 -0
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +42 -0
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +13 -0
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/** SQLite persistence for metadata-only saved Context Capsules. */
|
|
2
|
+
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
SavedCapsuleEvidenceReference,
|
|
7
|
+
SavedCapsuleRegistration,
|
|
8
|
+
SavedCapsuleRegistrationInput,
|
|
9
|
+
SavedCapsuleRegistrationRecord,
|
|
10
|
+
SavedCapsuleRegistrationSnapshot,
|
|
11
|
+
SavedCapsuleReverificationState,
|
|
12
|
+
SavedCapsuleVerificationExpectation,
|
|
13
|
+
SavedCapsuleVerificationRecord,
|
|
14
|
+
StoreResult,
|
|
15
|
+
} from "../types";
|
|
16
|
+
|
|
17
|
+
import { err, ok } from "../types";
|
|
18
|
+
|
|
19
|
+
const MAX_REGISTRATIONS = 10_000;
|
|
20
|
+
|
|
21
|
+
interface DbRegistration {
|
|
22
|
+
registration_id: string;
|
|
23
|
+
file_path: string;
|
|
24
|
+
file_hash: string;
|
|
25
|
+
capsule_id: string;
|
|
26
|
+
index_name: string;
|
|
27
|
+
question: string | null;
|
|
28
|
+
label: string | null;
|
|
29
|
+
notification_preference: "none" | "local";
|
|
30
|
+
registered_at_ms: number;
|
|
31
|
+
updated_at_ms: number;
|
|
32
|
+
last_attempted_sequence: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DbEvidence {
|
|
36
|
+
registration_id: string;
|
|
37
|
+
evidence_id: string;
|
|
38
|
+
canonical_uri: string;
|
|
39
|
+
collection: string;
|
|
40
|
+
source_hash: string;
|
|
41
|
+
mirror_hash: string;
|
|
42
|
+
passage_hash: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DbVerification {
|
|
46
|
+
registration_id: string;
|
|
47
|
+
trigger_kind: "manual" | "journal";
|
|
48
|
+
from_sequence: number;
|
|
49
|
+
through_sequence: number;
|
|
50
|
+
operation_status: "completed" | "failed";
|
|
51
|
+
affected_question_state: "unaffected" | "affected" | "unknown";
|
|
52
|
+
affected_reasons_json: string;
|
|
53
|
+
receipt_json: string | null;
|
|
54
|
+
receipt_hash: string | null;
|
|
55
|
+
error_code: string | null;
|
|
56
|
+
error_message: string | null;
|
|
57
|
+
verified_at_ms: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const mapRegistration = (row: DbRegistration): SavedCapsuleRegistration => ({
|
|
61
|
+
registrationId: row.registration_id,
|
|
62
|
+
filePath: row.file_path,
|
|
63
|
+
fileHash: row.file_hash,
|
|
64
|
+
capsuleId: row.capsule_id,
|
|
65
|
+
indexName: row.index_name,
|
|
66
|
+
question: row.question,
|
|
67
|
+
label: row.label,
|
|
68
|
+
notificationPreference: row.notification_preference,
|
|
69
|
+
registeredAtMs: row.registered_at_ms,
|
|
70
|
+
updatedAtMs: row.updated_at_ms,
|
|
71
|
+
lastAttemptedSequence: row.last_attempted_sequence,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const mapEvidence = (row: DbEvidence): SavedCapsuleEvidenceReference => ({
|
|
75
|
+
evidenceId: row.evidence_id,
|
|
76
|
+
canonicalUri: row.canonical_uri,
|
|
77
|
+
collection: row.collection,
|
|
78
|
+
sourceHash: row.source_hash,
|
|
79
|
+
mirrorHash: row.mirror_hash,
|
|
80
|
+
passageHash: row.passage_hash,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const mapVerification = (
|
|
84
|
+
row: DbVerification
|
|
85
|
+
): SavedCapsuleVerificationRecord => ({
|
|
86
|
+
registrationId: row.registration_id,
|
|
87
|
+
triggerKind: row.trigger_kind,
|
|
88
|
+
fromSequence: row.from_sequence,
|
|
89
|
+
throughSequence: row.through_sequence,
|
|
90
|
+
operationStatus: row.operation_status,
|
|
91
|
+
affectedQuestionState: row.affected_question_state,
|
|
92
|
+
affectedReasons: JSON.parse(row.affected_reasons_json) as string[],
|
|
93
|
+
receiptJson: row.receipt_json,
|
|
94
|
+
receiptHash: row.receipt_hash,
|
|
95
|
+
errorCode: row.error_code,
|
|
96
|
+
errorMessage: row.error_message,
|
|
97
|
+
verifiedAtMs: row.verified_at_ms,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const loadRecords = (
|
|
101
|
+
db: Database,
|
|
102
|
+
registrationId?: string
|
|
103
|
+
): SavedCapsuleRegistrationRecord[] => {
|
|
104
|
+
const registrations = registrationId
|
|
105
|
+
? db
|
|
106
|
+
.query<DbRegistration, [string]>(
|
|
107
|
+
`SELECT * FROM saved_capsule_registrations
|
|
108
|
+
WHERE registration_id = ?`
|
|
109
|
+
)
|
|
110
|
+
.all(registrationId)
|
|
111
|
+
: db
|
|
112
|
+
.query<DbRegistration, []>(
|
|
113
|
+
`SELECT * FROM saved_capsule_registrations
|
|
114
|
+
ORDER BY registration_id ASC`
|
|
115
|
+
)
|
|
116
|
+
.all();
|
|
117
|
+
if (registrations.length === 0) return [];
|
|
118
|
+
const evidence = registrationId
|
|
119
|
+
? db
|
|
120
|
+
.query<DbEvidence, [string]>(
|
|
121
|
+
`SELECT * FROM saved_capsule_evidence
|
|
122
|
+
WHERE registration_id = ?
|
|
123
|
+
ORDER BY evidence_id ASC`
|
|
124
|
+
)
|
|
125
|
+
.all(registrationId)
|
|
126
|
+
: db
|
|
127
|
+
.query<DbEvidence, []>(
|
|
128
|
+
`SELECT * FROM saved_capsule_evidence
|
|
129
|
+
ORDER BY registration_id ASC, evidence_id ASC`
|
|
130
|
+
)
|
|
131
|
+
.all();
|
|
132
|
+
const verifications = registrationId
|
|
133
|
+
? db
|
|
134
|
+
.query<DbVerification, [string]>(
|
|
135
|
+
`SELECT * FROM saved_capsule_verifications
|
|
136
|
+
WHERE registration_id = ?`
|
|
137
|
+
)
|
|
138
|
+
.all(registrationId)
|
|
139
|
+
: db
|
|
140
|
+
.query<DbVerification, []>(
|
|
141
|
+
`SELECT * FROM saved_capsule_verifications
|
|
142
|
+
ORDER BY registration_id ASC`
|
|
143
|
+
)
|
|
144
|
+
.all();
|
|
145
|
+
const evidenceByRegistration = new Map<
|
|
146
|
+
string,
|
|
147
|
+
SavedCapsuleEvidenceReference[]
|
|
148
|
+
>();
|
|
149
|
+
for (const row of evidence) {
|
|
150
|
+
const values = evidenceByRegistration.get(row.registration_id) ?? [];
|
|
151
|
+
values.push(mapEvidence(row));
|
|
152
|
+
evidenceByRegistration.set(row.registration_id, values);
|
|
153
|
+
}
|
|
154
|
+
const verificationByRegistration = new Map(
|
|
155
|
+
verifications.map((row) => [row.registration_id, mapVerification(row)])
|
|
156
|
+
);
|
|
157
|
+
return registrations.map((row) => ({
|
|
158
|
+
...mapRegistration(row),
|
|
159
|
+
evidence: evidenceByRegistration.get(row.registration_id) ?? [],
|
|
160
|
+
verification: verificationByRegistration.get(row.registration_id) ?? null,
|
|
161
|
+
}));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const upsertSavedCapsuleRegistration = (
|
|
165
|
+
db: Database,
|
|
166
|
+
input: SavedCapsuleRegistrationInput
|
|
167
|
+
): StoreResult<SavedCapsuleRegistrationRecord> => {
|
|
168
|
+
try {
|
|
169
|
+
const transaction = db.transaction(() => {
|
|
170
|
+
const count = db
|
|
171
|
+
.query<{ count: number }, []>(
|
|
172
|
+
"SELECT COUNT(*) AS count FROM saved_capsule_registrations"
|
|
173
|
+
)
|
|
174
|
+
.get()?.count;
|
|
175
|
+
const exists = db
|
|
176
|
+
.query<{ found: number }, [string]>(
|
|
177
|
+
`SELECT 1 AS found FROM saved_capsule_registrations
|
|
178
|
+
WHERE registration_id = ?`
|
|
179
|
+
)
|
|
180
|
+
.get(input.registrationId);
|
|
181
|
+
if (!exists && (count ?? 0) >= MAX_REGISTRATIONS) {
|
|
182
|
+
throw new RangeError("Saved Capsule registration limit reached");
|
|
183
|
+
}
|
|
184
|
+
db.run(
|
|
185
|
+
`UPDATE saved_capsule_reverification_state
|
|
186
|
+
SET last_processed_sequence = MIN(last_processed_sequence, ?),
|
|
187
|
+
registration_epoch = registration_epoch + 1
|
|
188
|
+
WHERE singleton_id = 1`,
|
|
189
|
+
[input.lastAttemptedSequence]
|
|
190
|
+
);
|
|
191
|
+
const registrationGeneration = db
|
|
192
|
+
.query<{ registration_epoch: number }, []>(
|
|
193
|
+
`SELECT registration_epoch
|
|
194
|
+
FROM saved_capsule_reverification_state
|
|
195
|
+
WHERE singleton_id = 1`
|
|
196
|
+
)
|
|
197
|
+
.get()?.registration_epoch;
|
|
198
|
+
if (registrationGeneration === undefined) {
|
|
199
|
+
throw new Error("Saved Capsule reverification state is missing");
|
|
200
|
+
}
|
|
201
|
+
db.run(
|
|
202
|
+
`INSERT INTO saved_capsule_registrations (
|
|
203
|
+
registration_id, file_path, file_hash, capsule_id, index_name,
|
|
204
|
+
question, label, notification_preference, registered_at_ms,
|
|
205
|
+
updated_at_ms, last_attempted_sequence, registration_generation
|
|
206
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
207
|
+
ON CONFLICT(registration_id) DO UPDATE SET
|
|
208
|
+
file_path = excluded.file_path,
|
|
209
|
+
file_hash = excluded.file_hash,
|
|
210
|
+
capsule_id = excluded.capsule_id,
|
|
211
|
+
index_name = excluded.index_name,
|
|
212
|
+
question = excluded.question,
|
|
213
|
+
label = excluded.label,
|
|
214
|
+
notification_preference = excluded.notification_preference,
|
|
215
|
+
updated_at_ms = excluded.updated_at_ms,
|
|
216
|
+
last_attempted_sequence = excluded.last_attempted_sequence,
|
|
217
|
+
registration_generation = excluded.registration_generation`,
|
|
218
|
+
[
|
|
219
|
+
input.registrationId,
|
|
220
|
+
input.filePath,
|
|
221
|
+
input.fileHash,
|
|
222
|
+
input.capsuleId,
|
|
223
|
+
input.indexName,
|
|
224
|
+
input.question,
|
|
225
|
+
input.label,
|
|
226
|
+
input.notificationPreference,
|
|
227
|
+
input.registeredAtMs,
|
|
228
|
+
input.updatedAtMs,
|
|
229
|
+
input.lastAttemptedSequence,
|
|
230
|
+
registrationGeneration,
|
|
231
|
+
]
|
|
232
|
+
);
|
|
233
|
+
db.run("DELETE FROM saved_capsule_evidence WHERE registration_id = ?", [
|
|
234
|
+
input.registrationId,
|
|
235
|
+
]);
|
|
236
|
+
const insertEvidence = db.prepare(
|
|
237
|
+
`INSERT INTO saved_capsule_evidence (
|
|
238
|
+
registration_id, evidence_id, canonical_uri, collection,
|
|
239
|
+
source_hash, mirror_hash, passage_hash
|
|
240
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
241
|
+
);
|
|
242
|
+
for (const evidence of input.evidence) {
|
|
243
|
+
insertEvidence.run(
|
|
244
|
+
input.registrationId,
|
|
245
|
+
evidence.evidenceId,
|
|
246
|
+
evidence.canonicalUri,
|
|
247
|
+
evidence.collection,
|
|
248
|
+
evidence.sourceHash,
|
|
249
|
+
evidence.mirrorHash,
|
|
250
|
+
evidence.passageHash
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
db.run(
|
|
254
|
+
"DELETE FROM saved_capsule_verifications WHERE registration_id = ?",
|
|
255
|
+
[input.registrationId]
|
|
256
|
+
);
|
|
257
|
+
return loadRecords(db, input.registrationId)[0]!;
|
|
258
|
+
});
|
|
259
|
+
return ok(transaction());
|
|
260
|
+
} catch (cause) {
|
|
261
|
+
return err(
|
|
262
|
+
cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
|
|
263
|
+
cause instanceof Error
|
|
264
|
+
? cause.message
|
|
265
|
+
: "Failed to register saved Context Capsule",
|
|
266
|
+
cause
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const listSavedCapsuleRegistrations = (
|
|
272
|
+
db: Database
|
|
273
|
+
): StoreResult<SavedCapsuleRegistrationRecord[]> => {
|
|
274
|
+
try {
|
|
275
|
+
return ok(loadRecords(db));
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
return err("QUERY_FAILED", "Failed to list saved Context Capsules", cause);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const getSavedCapsuleRegistration = (
|
|
282
|
+
db: Database,
|
|
283
|
+
registrationId: string
|
|
284
|
+
): StoreResult<SavedCapsuleRegistrationRecord | null> => {
|
|
285
|
+
try {
|
|
286
|
+
return ok(loadRecords(db, registrationId)[0] ?? null);
|
|
287
|
+
} catch (cause) {
|
|
288
|
+
return err("QUERY_FAILED", "Failed to read saved Context Capsule", cause);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const getSavedCapsuleRegistrationSnapshot = (
|
|
293
|
+
db: Database,
|
|
294
|
+
registrationId: string
|
|
295
|
+
): StoreResult<SavedCapsuleRegistrationSnapshot | null> => {
|
|
296
|
+
try {
|
|
297
|
+
const transaction = db.transaction(() => {
|
|
298
|
+
const registration = loadRecords(db, registrationId)[0];
|
|
299
|
+
if (!registration) return null;
|
|
300
|
+
const registrationGeneration = db
|
|
301
|
+
.query<{ registration_generation: number }, [string]>(
|
|
302
|
+
`SELECT registration_generation
|
|
303
|
+
FROM saved_capsule_registrations
|
|
304
|
+
WHERE registration_id = ?`
|
|
305
|
+
)
|
|
306
|
+
.get(registrationId)?.registration_generation;
|
|
307
|
+
if (registrationGeneration === undefined) return null;
|
|
308
|
+
return { registration, registrationGeneration };
|
|
309
|
+
});
|
|
310
|
+
return ok(transaction());
|
|
311
|
+
} catch (cause) {
|
|
312
|
+
return err(
|
|
313
|
+
"QUERY_FAILED",
|
|
314
|
+
"Failed to read saved Context Capsule verification snapshot",
|
|
315
|
+
cause
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const deleteSavedCapsuleRegistration = (
|
|
321
|
+
db: Database,
|
|
322
|
+
registrationId: string
|
|
323
|
+
): StoreResult<boolean> => {
|
|
324
|
+
try {
|
|
325
|
+
return ok(
|
|
326
|
+
db.run(
|
|
327
|
+
"DELETE FROM saved_capsule_registrations WHERE registration_id = ?",
|
|
328
|
+
[registrationId]
|
|
329
|
+
).changes > 0
|
|
330
|
+
);
|
|
331
|
+
} catch (cause) {
|
|
332
|
+
return err("QUERY_FAILED", "Failed to remove saved Context Capsule", cause);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export const listSavedCapsuleIdsAffectedByChanges = (
|
|
337
|
+
db: Database,
|
|
338
|
+
afterSequence: number,
|
|
339
|
+
throughSequence: number,
|
|
340
|
+
limit: number
|
|
341
|
+
): StoreResult<{ registrationIds: string[]; truncated: boolean }> => {
|
|
342
|
+
try {
|
|
343
|
+
if (
|
|
344
|
+
!Number.isSafeInteger(afterSequence) ||
|
|
345
|
+
!Number.isSafeInteger(throughSequence) ||
|
|
346
|
+
afterSequence < 0 ||
|
|
347
|
+
throughSequence < afterSequence ||
|
|
348
|
+
!Number.isSafeInteger(limit) ||
|
|
349
|
+
limit < 1 ||
|
|
350
|
+
limit > MAX_REGISTRATIONS
|
|
351
|
+
) {
|
|
352
|
+
return err("INVALID_INPUT", "Invalid saved Capsule change range");
|
|
353
|
+
}
|
|
354
|
+
const rows = db
|
|
355
|
+
.query<{ registration_id: string }, [number, number, number]>(
|
|
356
|
+
`SELECT DISTINCT evidence.registration_id
|
|
357
|
+
FROM saved_capsule_evidence evidence
|
|
358
|
+
INNER JOIN saved_capsule_registrations registration
|
|
359
|
+
ON registration.registration_id = evidence.registration_id
|
|
360
|
+
INNER JOIN document_changes change
|
|
361
|
+
ON (
|
|
362
|
+
evidence.canonical_uri = change.old_uri
|
|
363
|
+
OR evidence.canonical_uri = change.new_uri
|
|
364
|
+
OR evidence.source_hash = change.old_source_hash
|
|
365
|
+
OR evidence.source_hash = change.new_source_hash
|
|
366
|
+
OR evidence.mirror_hash = change.old_mirror_hash
|
|
367
|
+
OR evidence.mirror_hash = change.new_mirror_hash
|
|
368
|
+
)
|
|
369
|
+
WHERE change.sequence > ?
|
|
370
|
+
AND change.sequence > registration.last_attempted_sequence
|
|
371
|
+
AND change.sequence <= ?
|
|
372
|
+
ORDER BY evidence.registration_id ASC
|
|
373
|
+
LIMIT ?`
|
|
374
|
+
)
|
|
375
|
+
.all(afterSequence, throughSequence, limit + 1);
|
|
376
|
+
return ok({
|
|
377
|
+
registrationIds: rows.slice(0, limit).map((row) => row.registration_id),
|
|
378
|
+
truncated: rows.length > limit,
|
|
379
|
+
});
|
|
380
|
+
} catch (cause) {
|
|
381
|
+
return err(
|
|
382
|
+
"QUERY_FAILED",
|
|
383
|
+
"Failed to resolve affected saved Context Capsules",
|
|
384
|
+
cause
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const upsertSavedCapsuleVerification = (
|
|
390
|
+
db: Database,
|
|
391
|
+
verification: SavedCapsuleVerificationRecord,
|
|
392
|
+
expectedRegistration: SavedCapsuleVerificationExpectation
|
|
393
|
+
): StoreResult<boolean> => {
|
|
394
|
+
try {
|
|
395
|
+
const transaction = db.transaction(() => {
|
|
396
|
+
const registrationMatches = db
|
|
397
|
+
.query<{ found: number }, [string, number]>(
|
|
398
|
+
`SELECT 1 AS found
|
|
399
|
+
FROM saved_capsule_registrations
|
|
400
|
+
WHERE registration_id = ?
|
|
401
|
+
AND registration_generation = ?`
|
|
402
|
+
)
|
|
403
|
+
.get(
|
|
404
|
+
verification.registrationId,
|
|
405
|
+
expectedRegistration.registrationGeneration
|
|
406
|
+
);
|
|
407
|
+
if (!registrationMatches) return false;
|
|
408
|
+
|
|
409
|
+
db.run(
|
|
410
|
+
`INSERT INTO saved_capsule_verifications (
|
|
411
|
+
registration_id, trigger_kind, from_sequence, through_sequence,
|
|
412
|
+
operation_status, affected_question_state, affected_reasons_json,
|
|
413
|
+
receipt_json, receipt_hash, error_code, error_message, verified_at_ms
|
|
414
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
415
|
+
ON CONFLICT(registration_id) DO UPDATE SET
|
|
416
|
+
trigger_kind = excluded.trigger_kind,
|
|
417
|
+
from_sequence = excluded.from_sequence,
|
|
418
|
+
through_sequence = excluded.through_sequence,
|
|
419
|
+
operation_status = excluded.operation_status,
|
|
420
|
+
affected_question_state = excluded.affected_question_state,
|
|
421
|
+
affected_reasons_json = excluded.affected_reasons_json,
|
|
422
|
+
receipt_json = excluded.receipt_json,
|
|
423
|
+
receipt_hash = excluded.receipt_hash,
|
|
424
|
+
error_code = excluded.error_code,
|
|
425
|
+
error_message = excluded.error_message,
|
|
426
|
+
verified_at_ms = excluded.verified_at_ms`,
|
|
427
|
+
[
|
|
428
|
+
verification.registrationId,
|
|
429
|
+
verification.triggerKind,
|
|
430
|
+
verification.fromSequence,
|
|
431
|
+
verification.throughSequence,
|
|
432
|
+
verification.operationStatus,
|
|
433
|
+
verification.affectedQuestionState,
|
|
434
|
+
JSON.stringify(verification.affectedReasons),
|
|
435
|
+
verification.receiptJson,
|
|
436
|
+
verification.receiptHash,
|
|
437
|
+
verification.errorCode,
|
|
438
|
+
verification.errorMessage,
|
|
439
|
+
verification.verifiedAtMs,
|
|
440
|
+
]
|
|
441
|
+
);
|
|
442
|
+
db.run(
|
|
443
|
+
`UPDATE saved_capsule_registrations
|
|
444
|
+
SET last_attempted_sequence = MAX(last_attempted_sequence, ?),
|
|
445
|
+
updated_at_ms = MAX(updated_at_ms, ?)
|
|
446
|
+
WHERE registration_id = ?
|
|
447
|
+
AND registration_generation = ?`,
|
|
448
|
+
[
|
|
449
|
+
verification.throughSequence,
|
|
450
|
+
verification.verifiedAtMs,
|
|
451
|
+
verification.registrationId,
|
|
452
|
+
expectedRegistration.registrationGeneration,
|
|
453
|
+
]
|
|
454
|
+
);
|
|
455
|
+
return true;
|
|
456
|
+
});
|
|
457
|
+
return ok(transaction());
|
|
458
|
+
} catch (cause) {
|
|
459
|
+
return err(
|
|
460
|
+
"QUERY_FAILED",
|
|
461
|
+
"Failed to persist saved Context Capsule verification",
|
|
462
|
+
cause
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
export const getSavedCapsuleReverificationSequence = (
|
|
468
|
+
db: Database
|
|
469
|
+
): StoreResult<number> => {
|
|
470
|
+
try {
|
|
471
|
+
return ok(
|
|
472
|
+
db
|
|
473
|
+
.query<{ last_processed_sequence: number }, []>(
|
|
474
|
+
`SELECT last_processed_sequence
|
|
475
|
+
FROM saved_capsule_reverification_state WHERE singleton_id = 1`
|
|
476
|
+
)
|
|
477
|
+
.get()?.last_processed_sequence ?? 0
|
|
478
|
+
);
|
|
479
|
+
} catch (cause) {
|
|
480
|
+
return err("QUERY_FAILED", "Failed to read reverification state", cause);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
export const getSavedCapsuleReverificationState = (
|
|
485
|
+
db: Database
|
|
486
|
+
): StoreResult<SavedCapsuleReverificationState> => {
|
|
487
|
+
try {
|
|
488
|
+
const row = db
|
|
489
|
+
.query<
|
|
490
|
+
{
|
|
491
|
+
last_processed_sequence: number;
|
|
492
|
+
registration_epoch: number;
|
|
493
|
+
},
|
|
494
|
+
[]
|
|
495
|
+
>(
|
|
496
|
+
`SELECT last_processed_sequence, registration_epoch
|
|
497
|
+
FROM saved_capsule_reverification_state WHERE singleton_id = 1`
|
|
498
|
+
)
|
|
499
|
+
.get();
|
|
500
|
+
return ok({
|
|
501
|
+
lastProcessedSequence: row?.last_processed_sequence ?? 0,
|
|
502
|
+
registrationEpoch: row?.registration_epoch ?? 0,
|
|
503
|
+
});
|
|
504
|
+
} catch (cause) {
|
|
505
|
+
return err("QUERY_FAILED", "Failed to read reverification state", cause);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export const setSavedCapsuleReverificationSequence = (
|
|
510
|
+
db: Database,
|
|
511
|
+
sequence: number,
|
|
512
|
+
expectedRegistrationEpoch: number
|
|
513
|
+
): StoreResult<boolean> => {
|
|
514
|
+
try {
|
|
515
|
+
if (
|
|
516
|
+
!Number.isSafeInteger(sequence) ||
|
|
517
|
+
sequence < 0 ||
|
|
518
|
+
!Number.isSafeInteger(expectedRegistrationEpoch) ||
|
|
519
|
+
expectedRegistrationEpoch < 0
|
|
520
|
+
) {
|
|
521
|
+
return err("INVALID_INPUT", "Invalid reverification sequence");
|
|
522
|
+
}
|
|
523
|
+
const updated = db.run(
|
|
524
|
+
`UPDATE saved_capsule_reverification_state
|
|
525
|
+
SET last_processed_sequence = MAX(last_processed_sequence, ?)
|
|
526
|
+
WHERE singleton_id = 1
|
|
527
|
+
AND registration_epoch = ?`,
|
|
528
|
+
[sequence, expectedRegistrationEpoch]
|
|
529
|
+
).changes;
|
|
530
|
+
return ok(updated > 0);
|
|
531
|
+
} catch (cause) {
|
|
532
|
+
return err("QUERY_FAILED", "Failed to update reverification state", cause);
|
|
533
|
+
}
|
|
534
|
+
};
|