or3-provider-sqlite 0.0.1 → 0.0.3

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.
Files changed (40) hide show
  1. package/README.md +20 -4
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/server/admin/adapters/sync-sqlite.js +12 -34
  4. package/dist/runtime/server/admin/stores/sqlite-store.js +64 -9
  5. package/dist/runtime/server/auth/sqlite-auth-workspace-store.d.ts +16 -1
  6. package/dist/runtime/server/auth/sqlite-auth-workspace-store.js +114 -3
  7. package/dist/runtime/server/connect/sqlite-connect-store.d.ts +2 -0
  8. package/dist/runtime/server/connect/sqlite-connect-store.js +885 -0
  9. package/dist/runtime/server/db/migrate.js +62 -1
  10. package/dist/runtime/server/db/migrations/006_sync_snapshots.d.ts +10 -0
  11. package/dist/runtime/server/db/migrations/006_sync_snapshots.js +106 -0
  12. package/dist/runtime/server/db/migrations/007_device_cursor_ownership.d.ts +7 -0
  13. package/dist/runtime/server/db/migrations/007_device_cursor_ownership.js +5 -0
  14. package/dist/runtime/server/db/migrations/008_upload_intents.d.ts +4 -0
  15. package/dist/runtime/server/db/migrations/008_upload_intents.js +26 -0
  16. package/dist/runtime/server/db/migrations/009_or3_connect.d.ts +4 -0
  17. package/dist/runtime/server/db/migrations/009_or3_connect.js +91 -0
  18. package/dist/runtime/server/db/migrations/010_connect_user_code_hmac.d.ts +10 -0
  19. package/dist/runtime/server/db/migrations/010_connect_user_code_hmac.js +97 -0
  20. package/dist/runtime/server/db/migrations/011_connect_environment_scope_indexes.d.ts +4 -0
  21. package/dist/runtime/server/db/migrations/011_connect_environment_scope_indexes.js +26 -0
  22. package/dist/runtime/server/db/migrations/012_connect_credential_redelivery.d.ts +4 -0
  23. package/dist/runtime/server/db/migrations/012_connect_credential_redelivery.js +124 -0
  24. package/dist/runtime/server/db/migrations/013_connect_environment_lifecycle.d.ts +10 -0
  25. package/dist/runtime/server/db/migrations/013_connect_environment_lifecycle.js +261 -0
  26. package/dist/runtime/server/db/migrations/014_connect_retention_activation.d.ts +4 -0
  27. package/dist/runtime/server/db/migrations/014_connect_retention_activation.js +44 -0
  28. package/dist/runtime/server/db/migrations/015_rate_limits.d.ts +4 -0
  29. package/dist/runtime/server/db/migrations/015_rate_limits.js +18 -0
  30. package/dist/runtime/server/db/schema.d.ts +97 -0
  31. package/dist/runtime/server/plugins/register.js +27 -5
  32. package/dist/runtime/server/rate-limit/sqlite-provider.d.ts +7 -0
  33. package/dist/runtime/server/rate-limit/sqlite-provider.js +70 -0
  34. package/dist/runtime/server/sync/history-gc-policy.d.ts +13 -0
  35. package/dist/runtime/server/sync/history-gc-policy.js +8 -0
  36. package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.d.ts +14 -2
  37. package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.js +901 -67
  38. package/dist/runtime/server/webhooks/sqlite-webhook-store.d.ts +9 -0
  39. package/dist/runtime/server/webhooks/sqlite-webhook-store.js +521 -0
  40. package/package.json +9 -1
@@ -0,0 +1,9 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { WebhookStore } from '~~/server/utils/webhooks/store/types';
3
+ type SqliteDatabase = InstanceType<typeof Database>;
4
+ export interface SqliteWebhookStoreOptions {
5
+ database?: SqliteDatabase;
6
+ path?: string;
7
+ }
8
+ export declare function createSqliteWebhookStore(options?: SqliteWebhookStoreOptions): WebhookStore;
9
+ export {};
@@ -0,0 +1,521 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import Database from "better-sqlite3";
3
+ function envFlag(value) {
4
+ if (!value) return false;
5
+ const normalized = value.trim().toLowerCase();
6
+ return normalized === "1" || normalized === "true" || normalized === "yes";
7
+ }
8
+ function resolveDatabasePath() {
9
+ const isTestEnv = process.env.NODE_ENV === "test" || envFlag(process.env.VITEST);
10
+ const allowInMemory = envFlag(process.env.OR3_SQLITE_ALLOW_IN_MEMORY);
11
+ const strictMode = envFlag(process.env.OR3_SQLITE_STRICT);
12
+ const configuredPath = process.env.OR3_SQLITE_DB_PATH;
13
+ const path = configuredPath ?? ":memory:";
14
+ if (!configuredPath && !isTestEnv && !allowInMemory) {
15
+ throw new Error(
16
+ "OR3_SQLITE_DB_PATH is required in non-test environments. Set OR3_SQLITE_ALLOW_IN_MEMORY=true only if you intentionally want ephemeral storage."
17
+ );
18
+ }
19
+ if (strictMode && path === ":memory:") {
20
+ throw new Error(
21
+ "OR3_SQLITE_STRICT=true forbids in-memory SQLite. Set OR3_SQLITE_DB_PATH to a persistent file path."
22
+ );
23
+ }
24
+ if (!isTestEnv && path === ":memory:" && allowInMemory) {
25
+ console.warn(
26
+ "[webhooks:sqlite] OR3_SQLITE_ALLOW_IN_MEMORY=true enabled. Data will be lost on process restart."
27
+ );
28
+ }
29
+ return path;
30
+ }
31
+ function parseStringArray(raw) {
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ if (!Array.isArray(parsed)) return [];
35
+ return parsed.filter((value) => typeof value === "string");
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+ function toWebhookRegistration(row) {
41
+ return {
42
+ id: row.id,
43
+ scope: row.scope,
44
+ user_id: row.user_id,
45
+ workspace_id: row.workspace_id,
46
+ url: row.url,
47
+ label: row.label,
48
+ events: parseStringArray(row.events),
49
+ custom_hooks: parseStringArray(row.custom_hooks),
50
+ signing_secret_enc: row.signing_secret_enc,
51
+ enabled: row.enabled === 1,
52
+ health: row.health,
53
+ created_at: row.created_at,
54
+ updated_at: row.updated_at
55
+ };
56
+ }
57
+ function toDeliveryLog(row) {
58
+ return {
59
+ id: row.id,
60
+ webhook_id: row.webhook_id,
61
+ event_id: row.event_id,
62
+ event_type: row.event_type,
63
+ attempt: row.attempt,
64
+ status: row.status,
65
+ claimed_by: row.claimed_by,
66
+ claimed_at: row.claimed_at,
67
+ http_status: row.http_status,
68
+ error_message: row.error_message,
69
+ request_payload: row.request_payload,
70
+ response_body: row.response_body,
71
+ duration_ms: row.duration_ms,
72
+ next_retry_at: row.next_retry_at,
73
+ created_at: row.created_at
74
+ };
75
+ }
76
+ class SqliteWebhookStore {
77
+ constructor(db) {
78
+ this.db = db;
79
+ this.initialize();
80
+ }
81
+ initialize() {
82
+ this.db.pragma("journal_mode = WAL");
83
+ this.db.pragma("synchronous = NORMAL");
84
+ this.db.pragma("foreign_keys = ON");
85
+ this.db.exec(`
86
+ CREATE TABLE IF NOT EXISTS webhook_registrations (
87
+ id TEXT PRIMARY KEY,
88
+ scope TEXT NOT NULL CHECK (scope IN ('user', 'admin')),
89
+ user_id TEXT,
90
+ workspace_id TEXT,
91
+ url TEXT NOT NULL,
92
+ label TEXT NOT NULL DEFAULT '',
93
+ events TEXT NOT NULL,
94
+ custom_hooks TEXT NOT NULL,
95
+ signing_secret_enc TEXT NOT NULL,
96
+ enabled INTEGER NOT NULL DEFAULT 1,
97
+ health TEXT NOT NULL DEFAULT 'unknown' CHECK (health IN ('healthy', 'failing', 'unknown')),
98
+ created_at INTEGER NOT NULL,
99
+ updated_at INTEGER NOT NULL
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS webhook_delivery_logs (
103
+ id TEXT PRIMARY KEY,
104
+ webhook_id TEXT NOT NULL,
105
+ event_id TEXT NOT NULL,
106
+ event_type TEXT NOT NULL,
107
+ attempt INTEGER NOT NULL,
108
+ status TEXT NOT NULL CHECK (status IN ('pending', 'in_flight', 'success', 'failed', 'cancelled')),
109
+ claimed_by TEXT,
110
+ claimed_at INTEGER,
111
+ http_status INTEGER,
112
+ error_message TEXT,
113
+ request_payload TEXT NOT NULL,
114
+ response_body TEXT,
115
+ duration_ms INTEGER,
116
+ next_retry_at INTEGER,
117
+ created_at INTEGER NOT NULL,
118
+ FOREIGN KEY (webhook_id) REFERENCES webhook_registrations(id) ON DELETE CASCADE
119
+ );
120
+
121
+ CREATE TABLE IF NOT EXISTS webhook_custom_hooks (
122
+ webhook_id TEXT NOT NULL,
123
+ hook_name TEXT NOT NULL,
124
+ PRIMARY KEY (webhook_id, hook_name),
125
+ FOREIGN KEY (webhook_id) REFERENCES webhook_registrations(id) ON DELETE CASCADE
126
+ );
127
+
128
+ CREATE INDEX IF NOT EXISTS idx_webhook_registrations_scope_user_workspace
129
+ ON webhook_registrations (scope, user_id, workspace_id);
130
+ CREATE INDEX IF NOT EXISTS idx_webhook_delivery_logs_webhook_created
131
+ ON webhook_delivery_logs (webhook_id, created_at);
132
+ CREATE INDEX IF NOT EXISTS idx_webhook_delivery_logs_status_retry
133
+ ON webhook_delivery_logs (status, next_retry_at);
134
+ CREATE INDEX IF NOT EXISTS idx_webhook_delivery_logs_status_claimed
135
+ ON webhook_delivery_logs (status, claimed_at);
136
+ CREATE INDEX IF NOT EXISTS idx_webhook_custom_hooks_hook_name
137
+ ON webhook_custom_hooks (hook_name);
138
+ `);
139
+ this.db.exec(`
140
+ INSERT OR IGNORE INTO webhook_custom_hooks (webhook_id, hook_name)
141
+ SELECT wr.id, json_each.value
142
+ FROM webhook_registrations AS wr,
143
+ json_each(
144
+ CASE
145
+ WHEN json_valid(wr.custom_hooks) THEN wr.custom_hooks
146
+ ELSE '[]'
147
+ END
148
+ )
149
+ WHERE typeof(json_each.value) = 'text'
150
+ `);
151
+ }
152
+ async createWebhook(webhook) {
153
+ const now = Date.now();
154
+ const row = {
155
+ ...webhook,
156
+ id: randomUUID(),
157
+ health: "unknown",
158
+ created_at: now,
159
+ updated_at: now
160
+ };
161
+ const create = this.db.transaction(() => {
162
+ this.db.prepare(
163
+ `
164
+ INSERT INTO webhook_registrations (
165
+ id, scope, user_id, workspace_id, url, label, events, custom_hooks,
166
+ signing_secret_enc, enabled, health, created_at, updated_at
167
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
168
+ `
169
+ ).run(
170
+ row.id,
171
+ row.scope,
172
+ row.user_id,
173
+ row.workspace_id,
174
+ row.url,
175
+ row.label,
176
+ JSON.stringify(row.events),
177
+ JSON.stringify(row.custom_hooks),
178
+ row.signing_secret_enc,
179
+ row.enabled ? 1 : 0,
180
+ row.health,
181
+ row.created_at,
182
+ row.updated_at
183
+ );
184
+ const insertCustomHook = this.db.prepare(
185
+ `
186
+ INSERT OR IGNORE INTO webhook_custom_hooks (webhook_id, hook_name)
187
+ VALUES (?, ?)
188
+ `
189
+ );
190
+ for (const hookName of row.custom_hooks) {
191
+ insertCustomHook.run(row.id, hookName);
192
+ }
193
+ });
194
+ create();
195
+ return row;
196
+ }
197
+ async updateWebhook(webhookId, patch) {
198
+ const current = await this.getWebhook(webhookId);
199
+ if (!current) {
200
+ throw new Error(`Webhook not found: ${webhookId}`);
201
+ }
202
+ const urlChanged = typeof patch.url === "string" && patch.url !== current.url;
203
+ const next = {
204
+ ...current,
205
+ ...patch,
206
+ events: patch.events ?? current.events,
207
+ custom_hooks: patch.custom_hooks ?? current.custom_hooks,
208
+ health: urlChanged ? "unknown" : current.health,
209
+ updated_at: Date.now()
210
+ };
211
+ const update = this.db.transaction(() => {
212
+ this.db.prepare(
213
+ `
214
+ UPDATE webhook_registrations
215
+ SET url = ?, label = ?, events = ?, custom_hooks = ?, enabled = ?, workspace_id = ?, health = ?, updated_at = ?
216
+ WHERE id = ?
217
+ `
218
+ ).run(
219
+ next.url,
220
+ next.label,
221
+ JSON.stringify(next.events),
222
+ JSON.stringify(next.custom_hooks),
223
+ next.enabled ? 1 : 0,
224
+ next.workspace_id,
225
+ next.health,
226
+ next.updated_at,
227
+ webhookId
228
+ );
229
+ if ("custom_hooks" in patch) {
230
+ this.db.prepare("DELETE FROM webhook_custom_hooks WHERE webhook_id = ?").run(webhookId);
231
+ const insertCustomHook = this.db.prepare(
232
+ `
233
+ INSERT OR IGNORE INTO webhook_custom_hooks (webhook_id, hook_name)
234
+ VALUES (?, ?)
235
+ `
236
+ );
237
+ for (const hookName of next.custom_hooks) {
238
+ insertCustomHook.run(webhookId, hookName);
239
+ }
240
+ }
241
+ });
242
+ update();
243
+ return next;
244
+ }
245
+ async deleteWebhook(webhookId) {
246
+ this.db.prepare("DELETE FROM webhook_registrations WHERE id = ?").run(webhookId);
247
+ }
248
+ async getWebhook(webhookId) {
249
+ const row = this.db.prepare("SELECT * FROM webhook_registrations WHERE id = ?").get(webhookId);
250
+ return row ? toWebhookRegistration(row) : null;
251
+ }
252
+ async listWebhooks(userId, workspaceId) {
253
+ const rows = this.db.prepare(
254
+ `
255
+ SELECT * FROM webhook_registrations
256
+ WHERE scope = 'user' AND user_id = ? AND workspace_id = ?
257
+ ORDER BY created_at DESC
258
+ `
259
+ ).all(userId, workspaceId);
260
+ return rows.map(toWebhookRegistration);
261
+ }
262
+ async listAdminWebhooks() {
263
+ const rows = this.db.prepare(
264
+ `
265
+ SELECT * FROM webhook_registrations
266
+ WHERE scope = 'admin'
267
+ ORDER BY created_at DESC
268
+ `
269
+ ).all();
270
+ return rows.map(toWebhookRegistration);
271
+ }
272
+ async listWebhooksByEvent(eventType, scope, workspaceId) {
273
+ const eventQuery = `
274
+ EXISTS (
275
+ SELECT 1
276
+ FROM json_each(
277
+ CASE
278
+ WHEN json_valid(events) THEN events
279
+ ELSE '[]'
280
+ END
281
+ )
282
+ WHERE json_each.value = ?
283
+ )
284
+ `;
285
+ let rows;
286
+ if (!workspaceId) {
287
+ if (scope === "user") {
288
+ return [];
289
+ }
290
+ rows = this.db.prepare(
291
+ `
292
+ SELECT * FROM webhook_registrations
293
+ WHERE scope = ? AND enabled = 1 AND ${eventQuery}
294
+ AND workspace_id IS NULL
295
+ `
296
+ ).all(scope, eventType);
297
+ } else if (scope === "admin") {
298
+ rows = this.db.prepare(
299
+ `
300
+ SELECT * FROM webhook_registrations
301
+ WHERE scope = ? AND enabled = 1 AND ${eventQuery}
302
+ AND (workspace_id IS NULL OR workspace_id = ?)
303
+ `
304
+ ).all(scope, eventType, workspaceId);
305
+ } else {
306
+ rows = this.db.prepare(
307
+ `
308
+ SELECT * FROM webhook_registrations
309
+ WHERE scope = ? AND enabled = 1 AND ${eventQuery}
310
+ AND workspace_id = ?
311
+ `
312
+ ).all(scope, eventType, workspaceId);
313
+ }
314
+ return rows.map(toWebhookRegistration);
315
+ }
316
+ async listWebhooksByCustomHook(hookName) {
317
+ const rows = this.db.prepare(
318
+ `
319
+ SELECT wr.* FROM webhook_registrations wr
320
+ INNER JOIN webhook_custom_hooks wch ON wch.webhook_id = wr.id
321
+ WHERE wr.scope = 'admin' AND wr.enabled = 1 AND wch.hook_name = ?
322
+ `
323
+ ).all(hookName);
324
+ return rows.map(toWebhookRegistration);
325
+ }
326
+ async listActiveCustomHookNames() {
327
+ const rows = this.db.prepare(
328
+ `
329
+ SELECT DISTINCT wch.hook_name
330
+ FROM webhook_custom_hooks wch
331
+ INNER JOIN webhook_registrations wr ON wr.id = wch.webhook_id
332
+ WHERE wr.scope = 'admin' AND wr.enabled = 1
333
+ ORDER BY wch.hook_name ASC
334
+ `
335
+ ).all();
336
+ return rows.map((row) => row.hook_name);
337
+ }
338
+ async updateWebhookHealth(webhookId, health) {
339
+ this.db.prepare(
340
+ `
341
+ UPDATE webhook_registrations
342
+ SET health = ?, updated_at = ?
343
+ WHERE id = ?
344
+ `
345
+ ).run(health, Date.now(), webhookId);
346
+ }
347
+ async disableAllWebhooks(userId, workspaceId) {
348
+ const result = this.db.prepare(
349
+ `
350
+ UPDATE webhook_registrations
351
+ SET enabled = 0, updated_at = ?
352
+ WHERE scope = 'user' AND user_id = ? AND workspace_id = ? AND enabled = 1
353
+ `
354
+ ).run(Date.now(), userId, workspaceId);
355
+ return result.changes;
356
+ }
357
+ async createDeliveryLog(log) {
358
+ const row = {
359
+ ...log,
360
+ id: randomUUID()
361
+ };
362
+ this.db.prepare(
363
+ `
364
+ INSERT INTO webhook_delivery_logs (
365
+ id, webhook_id, event_id, event_type, attempt, status, claimed_by, claimed_at,
366
+ http_status, error_message, request_payload, response_body, duration_ms,
367
+ next_retry_at, created_at
368
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
369
+ `
370
+ ).run(
371
+ row.id,
372
+ row.webhook_id,
373
+ row.event_id,
374
+ row.event_type,
375
+ row.attempt,
376
+ row.status,
377
+ row.claimed_by,
378
+ row.claimed_at,
379
+ row.http_status,
380
+ row.error_message,
381
+ row.request_payload,
382
+ row.response_body,
383
+ row.duration_ms,
384
+ row.next_retry_at,
385
+ row.created_at
386
+ );
387
+ return row;
388
+ }
389
+ async updateDeliveryLog(logId, patch) {
390
+ const current = this.db.prepare("SELECT * FROM webhook_delivery_logs WHERE id = ?").get(logId);
391
+ if (!current) {
392
+ throw new Error(`Webhook delivery log not found: ${logId}`);
393
+ }
394
+ const status = patch.status ?? current.status;
395
+ const clearClaim = status !== "in_flight";
396
+ const attempt = "attempt" in patch ? patch.attempt : current.attempt;
397
+ const httpStatus = "http_status" in patch ? patch.http_status : current.http_status;
398
+ const errorMessage = "error_message" in patch ? patch.error_message : current.error_message;
399
+ const responseBody = "response_body" in patch ? patch.response_body : current.response_body;
400
+ const durationMs = "duration_ms" in patch ? patch.duration_ms : current.duration_ms;
401
+ const nextRetryAt = "next_retry_at" in patch ? patch.next_retry_at : current.next_retry_at;
402
+ this.db.prepare(
403
+ `
404
+ UPDATE webhook_delivery_logs
405
+ SET status = ?, attempt = ?, http_status = ?, error_message = ?, response_body = ?,
406
+ duration_ms = ?, next_retry_at = ?, claimed_by = ?, claimed_at = ?
407
+ WHERE id = ?
408
+ `
409
+ ).run(
410
+ status,
411
+ attempt,
412
+ httpStatus,
413
+ errorMessage,
414
+ responseBody,
415
+ durationMs,
416
+ nextRetryAt,
417
+ clearClaim ? null : current.claimed_by,
418
+ clearClaim ? null : current.claimed_at,
419
+ logId
420
+ );
421
+ }
422
+ async getDeliveryLogs(webhookId, since) {
423
+ const rows = this.db.prepare(
424
+ `
425
+ SELECT * FROM webhook_delivery_logs
426
+ WHERE webhook_id = ? AND created_at >= ?
427
+ ORDER BY created_at DESC
428
+ `
429
+ ).all(webhookId, since);
430
+ return rows.map(toDeliveryLog);
431
+ }
432
+ async getRecentTerminalDeliveries(webhookId, limit) {
433
+ const safeLimit = Math.max(1, Math.floor(limit));
434
+ const rows = this.db.prepare(
435
+ `
436
+ SELECT * FROM webhook_delivery_logs
437
+ WHERE webhook_id = ? AND status IN ('success', 'failed')
438
+ ORDER BY created_at DESC
439
+ LIMIT ?
440
+ `
441
+ ).all(webhookId, safeLimit);
442
+ return rows.map(toDeliveryLog);
443
+ }
444
+ async claimPendingDeliveries(workerId, limit) {
445
+ const safeLimit = Math.max(0, Math.floor(limit));
446
+ if (safeLimit === 0) {
447
+ return [];
448
+ }
449
+ const now = Date.now();
450
+ const claim = this.db.transaction(() => {
451
+ const rows = this.db.prepare(
452
+ `
453
+ SELECT * FROM webhook_delivery_logs
454
+ WHERE status = 'pending'
455
+ AND (next_retry_at IS NULL OR next_retry_at <= ?)
456
+ ORDER BY COALESCE(next_retry_at, created_at) ASC, created_at ASC
457
+ LIMIT ?
458
+ `
459
+ ).all(now, safeLimit);
460
+ if (rows.length === 0) {
461
+ return [];
462
+ }
463
+ const update = this.db.prepare(
464
+ `
465
+ UPDATE webhook_delivery_logs
466
+ SET status = 'in_flight', claimed_by = ?, claimed_at = ?
467
+ WHERE id = ? AND status = 'pending'
468
+ `
469
+ );
470
+ const claimedRows = [];
471
+ for (const row of rows) {
472
+ const result = update.run(workerId, now, row.id);
473
+ if (result.changes > 0) {
474
+ claimedRows.push(
475
+ toDeliveryLog({
476
+ ...row,
477
+ status: "in_flight",
478
+ claimed_by: workerId,
479
+ claimed_at: now
480
+ })
481
+ );
482
+ }
483
+ }
484
+ return claimedRows;
485
+ });
486
+ return claim();
487
+ }
488
+ async resetStaleInFlightDeliveries(olderThanMs) {
489
+ const cutoff = Date.now() - Math.max(0, Math.floor(olderThanMs));
490
+ const result = this.db.prepare(
491
+ `
492
+ UPDATE webhook_delivery_logs
493
+ SET status = 'pending', claimed_by = NULL, claimed_at = NULL
494
+ WHERE status = 'in_flight' AND claimed_at IS NOT NULL AND claimed_at < ?
495
+ `
496
+ ).run(cutoff);
497
+ return result.changes;
498
+ }
499
+ async cancelDeliveriesByWebhook(webhookId) {
500
+ const result = this.db.prepare(
501
+ `
502
+ UPDATE webhook_delivery_logs
503
+ SET status = 'cancelled', claimed_by = NULL, claimed_at = NULL, next_retry_at = NULL
504
+ WHERE webhook_id = ? AND status IN ('pending', 'in_flight')
505
+ `
506
+ ).run(webhookId);
507
+ return result.changes;
508
+ }
509
+ async deleteDeliveryLogsByWebhook(webhookId) {
510
+ const result = this.db.prepare("DELETE FROM webhook_delivery_logs WHERE webhook_id = ?").run(webhookId);
511
+ return result.changes;
512
+ }
513
+ async purgeExpiredLogs(beforeTimestamp) {
514
+ const result = this.db.prepare("DELETE FROM webhook_delivery_logs WHERE created_at < ?").run(beforeTimestamp);
515
+ return result.changes;
516
+ }
517
+ }
518
+ export function createSqliteWebhookStore(options = {}) {
519
+ const db = options.database ?? new Database(options.path ?? resolveDatabasePath());
520
+ return new SqliteWebhookStore(db);
521
+ }
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/Saluana/or3-provider-sqlite.git"
7
+ },
4
8
  "description": "SQLite sync and workspace store provider for OR3 Chat — lightweight self-hosted backend via Kysely.",
5
9
  "license": "MIT",
6
10
  "type": "module",
@@ -13,6 +17,10 @@
13
17
  "./nuxt": {
14
18
  "types": "./dist/types.d.mts",
15
19
  "import": "./dist/module.mjs"
20
+ },
21
+ "./webhooks/sqlite-store": {
22
+ "types": "./dist/runtime/server/webhooks/sqlite-webhook-store.d.ts",
23
+ "import": "./dist/runtime/server/webhooks/sqlite-webhook-store.js"
16
24
  }
17
25
  },
18
26
  "main": "./dist/module.mjs",