or3-provider-sqlite 0.0.9 → 0.0.11

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.
@@ -16,6 +16,9 @@ import * as m014 from "./migrations/014_connect_retention_activation.js";
16
16
  import * as m015 from "./migrations/015_rate_limits.js";
17
17
  import * as m016 from "./migrations/016_connect_runtime_binding.js";
18
18
  import * as m017 from "./migrations/017_background_jobs.js";
19
+ import * as m018 from "./migrations/018_background_admission_cancels.js";
20
+ import * as m019 from "./migrations/019_background_generation_history.js";
21
+ import * as m020 from "./migrations/020_background_generation_receipts.js";
19
22
  const REQUIRED_SCHEMA_TABLES = [
20
23
  "users",
21
24
  "auth_accounts",
@@ -41,7 +44,9 @@ const REQUIRED_SCHEMA_TABLES = [
41
44
  "connect_device_authorizations",
42
45
  "connect_environments",
43
46
  "rate_limits",
44
- "background_jobs"
47
+ "background_jobs",
48
+ "background_admission_cancels",
49
+ "background_generation_receipts"
45
50
  ];
46
51
  const migrations = {
47
52
  "001_init": m001,
@@ -60,7 +65,10 @@ const migrations = {
60
65
  "014_connect_retention_activation": m014,
61
66
  "015_rate_limits": m015,
62
67
  "016_connect_runtime_binding": m016,
63
- "017_background_jobs": m017
68
+ "017_background_jobs": m017,
69
+ "018_background_admission_cancels": m018,
70
+ "019_background_generation_history": m019,
71
+ "020_background_generation_receipts": m020
64
72
  };
65
73
  class StaticMigrationProvider {
66
74
  async getMigrations() {
@@ -0,0 +1,7 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Durable admission cancellations. Creation and cancellation both check this
4
+ * table so a Stop that arrives before the job row commits cannot launch work.
5
+ */
6
+ export declare function up(db: Kysely<unknown>): Promise<void>;
7
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ CREATE TABLE IF NOT EXISTS background_admission_cancels (
5
+ user_id TEXT NOT NULL,
6
+ admission_id TEXT NOT NULL,
7
+ expires_at INTEGER NOT NULL,
8
+ PRIMARY KEY (user_id, admission_id)
9
+ )
10
+ `.execute(db);
11
+ await sql`
12
+ CREATE INDEX IF NOT EXISTS background_admission_cancels_expiry
13
+ ON background_admission_cancels(expires_at)
14
+ `.execute(db);
15
+ }
16
+ export async function down(db) {
17
+ await sql`DROP TABLE IF EXISTS background_admission_cancels`.execute(db);
18
+ }
@@ -0,0 +1,7 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Generation identity, durable history phase, and accumulated reasoning on
4
+ * background jobs. Reasoning is kept distinct from request reasoning config.
5
+ */
6
+ export declare function up(db: Kysely<unknown>): Promise<void>;
7
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ ALTER TABLE background_jobs
5
+ ADD COLUMN reasoning TEXT NOT NULL DEFAULT ''
6
+ `.execute(db);
7
+ await sql`
8
+ ALTER TABLE background_jobs
9
+ ADD COLUMN generation_id TEXT
10
+ `.execute(db);
11
+ await sql`
12
+ ALTER TABLE background_jobs
13
+ ADD COLUMN history_phase TEXT
14
+ `.execute(db);
15
+ await sql`
16
+ ALTER TABLE background_jobs
17
+ ADD COLUMN sync_provider_id TEXT
18
+ `.execute(db);
19
+ }
20
+ export async function down(db) {
21
+ await sql`ALTER TABLE background_jobs DROP COLUMN sync_provider_id`.execute(db);
22
+ await sql`ALTER TABLE background_jobs DROP COLUMN history_phase`.execute(db);
23
+ await sql`ALTER TABLE background_jobs DROP COLUMN generation_id`.execute(db);
24
+ await sql`ALTER TABLE background_jobs DROP COLUMN reasoning`.execute(db);
25
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Durable replay receipts for canonical background-history transactions. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ CREATE TABLE background_generation_receipts (
5
+ id TEXT PRIMARY KEY,
6
+ workspace_id TEXT NOT NULL,
7
+ generation_id TEXT NOT NULL,
8
+ stage TEXT NOT NULL CHECK(stage IN ('admission', 'finalization')),
9
+ fingerprint TEXT NOT NULL,
10
+ outcome TEXT NOT NULL,
11
+ server_version INTEGER,
12
+ created_at INTEGER NOT NULL,
13
+ UNIQUE(workspace_id, generation_id, stage)
14
+ )
15
+ `.execute(db);
16
+ }
17
+ export async function down(db) {
18
+ await sql`DROP TABLE IF EXISTS background_generation_receipts`.execute(db);
19
+ }
@@ -197,6 +197,10 @@ export interface BackgroundJobsTable {
197
197
  kind: string | null;
198
198
  status: 'streaming' | 'complete' | 'error' | 'aborted';
199
199
  content: string;
200
+ reasoning: string;
201
+ generation_id: string | null;
202
+ history_phase: string | null;
203
+ sync_provider_id: string | null;
200
204
  chunks_received: number;
201
205
  started_at: number;
202
206
  last_activity_at: number;
@@ -210,6 +214,16 @@ export interface BackgroundJobsTable {
210
214
  lease_expires_at: number | null;
211
215
  attempts: number;
212
216
  }
217
+ export interface BackgroundGenerationReceiptsTable {
218
+ id: string;
219
+ workspace_id: string;
220
+ generation_id: string;
221
+ stage: 'admission' | 'finalization';
222
+ fingerprint: string;
223
+ outcome: string;
224
+ server_version: number | null;
225
+ created_at: number;
226
+ }
213
227
  export interface AdminUsersTable {
214
228
  user_id: string;
215
229
  created_at: number;
@@ -239,6 +253,7 @@ export interface Or3SqliteDb {
239
253
  connect_environments: ConnectEnvironmentsTable;
240
254
  rate_limits: RateLimitsTable;
241
255
  background_jobs: BackgroundJobsTable;
256
+ background_generation_receipts: BackgroundGenerationReceiptsTable;
242
257
  s_threads: SyncedEntityTable;
243
258
  s_messages: SyncedEntityTable;
244
259
  s_projects: SyncedEntityTable;
@@ -0,0 +1,7 @@
1
+ import type { PluginConnectionStore } from '~~/server/utils/plugins/connections/store/registry';
2
+ import { type SqliteRawDatabase } from '../db/kysely.js';
3
+ type SqliteDatabase = SqliteRawDatabase;
4
+ export declare function createSqlitePluginConnectionStore(options?: {
5
+ database?: SqliteDatabase;
6
+ }): PluginConnectionStore;
7
+ export {};
@@ -0,0 +1,209 @@
1
+ import {
2
+ getRawDb,
3
+ getSqliteDriver
4
+ } from "../db/kysely.js";
5
+ function parseStringArray(raw) {
6
+ try {
7
+ const parsed = JSON.parse(raw);
8
+ if (!Array.isArray(parsed)) return [];
9
+ return parsed.filter((value) => typeof value === "string");
10
+ } catch {
11
+ return [];
12
+ }
13
+ }
14
+ function toConnection(row) {
15
+ return {
16
+ id: row.id,
17
+ ownerUserId: row.owner_user_id,
18
+ workspaceId: row.workspace_id,
19
+ pluginId: row.plugin_id,
20
+ providerId: row.provider_id,
21
+ ...row.slot_id === null ? {} : { slotId: row.slot_id },
22
+ label: row.label,
23
+ scopes: parseStringArray(row.scopes),
24
+ revision: row.revision,
25
+ secretCiphertext: row.secret_ciphertext,
26
+ createdAt: row.created_at,
27
+ updatedAt: row.updated_at
28
+ };
29
+ }
30
+ function toEvidence(row) {
31
+ return {
32
+ connectionId: row.connection_id,
33
+ revision: row.revision,
34
+ operationId: row.operation_id,
35
+ ok: row.ok === 1,
36
+ ...row.code === null ? {} : { code: row.code },
37
+ checkedAt: row.checked_at,
38
+ ...row.detail === null ? {} : { detail: row.detail }
39
+ };
40
+ }
41
+ class SqlitePluginConnectionStore {
42
+ constructor(db) {
43
+ this.db = db;
44
+ this.#initialize();
45
+ }
46
+ #initialize() {
47
+ if (getSqliteDriver() !== "turso") {
48
+ this.db.pragma?.("journal_mode = WAL");
49
+ this.db.pragma?.("synchronous = NORMAL");
50
+ this.db.pragma?.("foreign_keys = ON");
51
+ }
52
+ this.db.exec?.(`
53
+ CREATE TABLE IF NOT EXISTS plugin_connections (
54
+ id TEXT PRIMARY KEY,
55
+ owner_user_id TEXT NOT NULL,
56
+ workspace_id TEXT NOT NULL,
57
+ plugin_id TEXT NOT NULL,
58
+ provider_id TEXT NOT NULL,
59
+ slot_id TEXT,
60
+ label TEXT NOT NULL DEFAULT '',
61
+ scopes TEXT NOT NULL DEFAULT '[]',
62
+ revision INTEGER NOT NULL DEFAULT 1,
63
+ secret_ciphertext TEXT NOT NULL,
64
+ created_at INTEGER NOT NULL,
65
+ updated_at INTEGER NOT NULL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS plugin_connection_tests (
69
+ connection_id TEXT PRIMARY KEY,
70
+ revision INTEGER NOT NULL,
71
+ operation_id TEXT NOT NULL,
72
+ ok INTEGER NOT NULL,
73
+ code TEXT,
74
+ checked_at INTEGER NOT NULL,
75
+ detail TEXT,
76
+ FOREIGN KEY (connection_id) REFERENCES plugin_connections(id) ON DELETE CASCADE
77
+ );
78
+
79
+ CREATE INDEX IF NOT EXISTS idx_plugin_connections_workspace_plugin
80
+ ON plugin_connections (workspace_id, plugin_id);
81
+ CREATE INDEX IF NOT EXISTS idx_plugin_connections_owner
82
+ ON plugin_connections (owner_user_id);
83
+ `);
84
+ try {
85
+ this.db.prepare(`ALTER TABLE plugin_connections ADD COLUMN slot_id TEXT`).run();
86
+ } catch {
87
+ }
88
+ }
89
+ /**
90
+ * Owner-scoped listing. Ownership is part of the SQL predicate so a workspace
91
+ * peer cannot discover another user's connections or their references.
92
+ */
93
+ async list(input) {
94
+ const rows = input.pluginId === void 0 ? this.db.prepare(
95
+ `SELECT * FROM plugin_connections WHERE owner_user_id = ? AND workspace_id = ? ORDER BY created_at ASC`
96
+ ).all(input.ownerUserId, input.workspaceId) : this.db.prepare(
97
+ `SELECT * FROM plugin_connections WHERE owner_user_id = ? AND workspace_id = ? AND plugin_id = ? ORDER BY created_at ASC`
98
+ ).all(input.ownerUserId, input.workspaceId, input.pluginId);
99
+ return rows.map(toConnection);
100
+ }
101
+ async get(id) {
102
+ const row = this.db.prepare(`SELECT * FROM plugin_connections WHERE id = ?`).get(id);
103
+ return row ? toConnection(row) : null;
104
+ }
105
+ /**
106
+ * Insert-only creation. `DO NOTHING` is deliberate: an upsert here could
107
+ * reassign an existing record's owner, workspace, plugin or provider when two
108
+ * processes happen to generate the same id.
109
+ */
110
+ async insert(connection) {
111
+ const result = this.db.prepare(
112
+ `INSERT INTO plugin_connections (
113
+ id, owner_user_id, workspace_id, plugin_id, provider_id, slot_id,
114
+ label, scopes, revision, secret_ciphertext, created_at, updated_at
115
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
116
+ ON CONFLICT(id) DO NOTHING`
117
+ ).run(
118
+ connection.id,
119
+ connection.ownerUserId,
120
+ connection.workspaceId,
121
+ connection.pluginId,
122
+ connection.providerId,
123
+ connection.slotId ?? null,
124
+ connection.label,
125
+ JSON.stringify(connection.scopes),
126
+ connection.revision,
127
+ connection.secretCiphertext,
128
+ connection.createdAt,
129
+ connection.updatedAt
130
+ );
131
+ return result.changes > 0;
132
+ }
133
+ /**
134
+ * Compare-and-swap update. Identity columns are not in the SET list, so a
135
+ * retry cannot move the record; the revision predicate is the whole
136
+ * concurrency control.
137
+ */
138
+ async update(update) {
139
+ const result = this.db.prepare(
140
+ `UPDATE plugin_connections
141
+ SET revision = ?,
142
+ secret_ciphertext = ?,
143
+ updated_at = ?,
144
+ scopes = COALESCE(?, scopes),
145
+ label = COALESCE(?, label)
146
+ WHERE id = ? AND revision = ?`
147
+ ).run(
148
+ update.revision,
149
+ update.secretCiphertext,
150
+ update.updatedAt,
151
+ update.scopes === void 0 ? null : JSON.stringify(update.scopes),
152
+ update.label ?? null,
153
+ update.id,
154
+ update.expectedRevision
155
+ );
156
+ return result.changes > 0;
157
+ }
158
+ async delete(id) {
159
+ this.db.prepare(`DELETE FROM plugin_connections WHERE id = ?`).run(id);
160
+ this.db.prepare(`DELETE FROM plugin_connection_tests WHERE connection_id = ?`).run(id);
161
+ }
162
+ async getTestEvidence(connectionId) {
163
+ const row = this.db.prepare(`SELECT * FROM plugin_connection_tests WHERE connection_id = ?`).get(connectionId);
164
+ return row ? toEvidence(row) : null;
165
+ }
166
+ /**
167
+ * Stores evidence only when it belongs to the connection's current revision
168
+ * and is not older than what is already stored. A slow test that finishes
169
+ * after a credential rotation therefore cannot reinstate itself as current
170
+ * evidence, and an older completion cannot replace a newer result.
171
+ */
172
+ async setTestEvidence(evidence) {
173
+ const result = this.db.prepare(
174
+ `INSERT INTO plugin_connection_tests (
175
+ connection_id, revision, operation_id, ok, code, checked_at, detail
176
+ )
177
+ SELECT ?, ?, ?, ?, ?, ?, ?
178
+ WHERE EXISTS (
179
+ SELECT 1 FROM plugin_connections
180
+ WHERE id = ? AND revision = ?
181
+ )
182
+ ON CONFLICT(connection_id) DO UPDATE SET
183
+ revision = excluded.revision,
184
+ operation_id = excluded.operation_id,
185
+ ok = excluded.ok,
186
+ code = excluded.code,
187
+ checked_at = excluded.checked_at,
188
+ detail = excluded.detail
189
+ WHERE excluded.revision > plugin_connection_tests.revision
190
+ OR (excluded.revision = plugin_connection_tests.revision
191
+ AND excluded.checked_at >= plugin_connection_tests.checked_at)`
192
+ ).run(
193
+ evidence.connectionId,
194
+ evidence.revision,
195
+ evidence.operationId,
196
+ evidence.ok ? 1 : 0,
197
+ evidence.code ?? null,
198
+ evidence.checkedAt,
199
+ evidence.detail ?? null,
200
+ evidence.connectionId,
201
+ evidence.revision
202
+ );
203
+ return result.changes > 0;
204
+ }
205
+ }
206
+ export function createSqlitePluginConnectionStore(options = {}) {
207
+ const db = options.database ?? getRawDb();
208
+ return new SqlitePluginConnectionStore(db);
209
+ }
@@ -5,6 +5,7 @@ import { registerAdminStoreProvider } from "~~/server/admin/stores/registry";
5
5
  import { registerSyncGatewayAdapter } from "~~/server/sync/gateway/registry";
6
6
  import { registerWebhookStore } from "~~/server/utils/webhooks/store/registry";
7
7
  import { registerConnectStore } from "~~/server/connect/store/registry";
8
+ import { registerPluginConnectionStore } from "~~/server/utils/plugins/connections/store/registry";
8
9
  import { registerRateLimitProvider } from "~~/server/utils/rate-limit/registry";
9
10
  import { registerBackgroundJobProvider } from "~~/server/utils/background-jobs/registry";
10
11
  import { createSqliteAuthWorkspaceStore } from "../auth/sqlite-auth-workspace-store.js";
@@ -17,6 +18,7 @@ import {
17
18
  } from "../admin/stores/sqlite-store.js";
18
19
  import { sqliteSyncAdminAdapter } from "../admin/adapters/sync-sqlite.js";
19
20
  import { createSqliteConnectStore } from "../connect/sqlite-connect-store.js";
21
+ import { createSqlitePluginConnectionStore } from "../plugin-connections/sqlite-connection-store.js";
20
22
  import { getSqliteDriver, initializeSqliteDb } from "../db/kysely.js";
21
23
  import { runMigrations } from "../db/migrate.js";
22
24
  import { sqliteRateLimitProvider } from "../rate-limit/sqlite-provider.js";
@@ -72,6 +74,17 @@ export default defineNitroPlugin(async (nitroApp) => {
72
74
  "[or3-sqlite] Webhook persistence is unavailable with Cloudflare D1."
73
75
  );
74
76
  }
77
+ if (driver !== "d1") {
78
+ registerPluginConnectionStore({
79
+ id: SQLITE_PROVIDER_ID,
80
+ order: 100,
81
+ create: createSqlitePluginConnectionStore
82
+ });
83
+ } else {
84
+ console.warn(
85
+ "[or3-sqlite] Plugin connection persistence is unavailable with Cloudflare D1."
86
+ );
87
+ }
75
88
  registerAdminStoreProvider({
76
89
  id: SQLITE_PROVIDER_ID,
77
90
  createWorkspaceAccessStore: createSqliteWorkspaceAccessStore,
@@ -11,3 +11,11 @@ export declare const SYNC_HISTORY_GC_POLICY: Readonly<{
11
11
  reason: "SQLite history GC is guarded by the verified snapshot-v1 retention contract.";
12
12
  }>;
13
13
  export declare function canRunSyncHistoryGc(): boolean;
14
+ export declare function computePullRetention(input: {
15
+ cursor: number;
16
+ oldestLogVersion: number | null;
17
+ highWatermark: number;
18
+ }): {
19
+ oldestRetainedVersion: number;
20
+ requiresSnapshot: boolean;
21
+ };
@@ -6,3 +6,10 @@ export const SYNC_HISTORY_GC_POLICY = Object.freeze({
6
6
  export function canRunSyncHistoryGc() {
7
7
  return SYNC_HISTORY_GC_POLICY.enabled && SYNC_HISTORY_GC_POLICY.snapshotBootstrapVerified;
8
8
  }
9
+ export function computePullRetention(input) {
10
+ const oldestRetainedVersion = input.oldestLogVersion ?? Math.max(0, input.highWatermark) + 1;
11
+ return {
12
+ oldestRetainedVersion,
13
+ requiresSnapshot: oldestRetainedVersion > 0 && input.cursor < Math.max(0, oldestRetainedVersion - 1)
14
+ };
15
+ }
@@ -7,14 +7,26 @@
7
7
  */
8
8
  import type { H3Event } from 'h3';
9
9
  import type { CanonicalStorageQueryRequest, CanonicalStorageQueryResponse, SyncGatewayAdapter, SyncMaintenanceState, UploadIntentConsumptionRequest, UploadIntentReservationRequest } from '~~/server/sync/gateway/types';
10
+ import type { AdmitChatGenerationResult, CanonicalGenerationSnapshot, CanonicalHistoryActor, ChatGenerationAdmissionEnvelope, FinalizeChatGenerationResult } from '~~/shared/chat/background-history';
10
11
  import type { PullRequest, PullResponse, PushBatch, PushResult, SnapshotRequest, SnapshotResponse } from '~~/shared/sync/types';
11
12
  export declare class SqliteSyncGatewayAdapter implements SyncGatewayAdapter {
12
13
  id: string;
13
- readonly capabilities: {
14
+ get capabilities(): {
14
15
  readonly snapshotBootstrap: "snapshot-v1";
15
16
  readonly historyRetention: "snapshot-v1";
17
+ readonly backgroundGenerationHistory?: undefined;
18
+ } | {
19
+ readonly snapshotBootstrap: "snapshot-v1";
20
+ readonly historyRetention: "snapshot-v1";
21
+ readonly backgroundGenerationHistory: "v1";
16
22
  };
17
23
  private get db();
24
+ private assertCanonicalActor;
25
+ admitChatGeneration(actor: CanonicalHistoryActor, input: ChatGenerationAdmissionEnvelope): Promise<AdmitChatGenerationResult>;
26
+ finalizeChatGeneration(actor: CanonicalHistoryActor, input: {
27
+ admission: ChatGenerationAdmissionEnvelope;
28
+ snapshot: CanonicalGenerationSnapshot;
29
+ }): Promise<FinalizeChatGenerationResult>;
18
30
  reserveUploadIntent(event: H3Event, input: UploadIntentReservationRequest): Promise<void>;
19
31
  consumeUploadIntent(event: H3Event, input: UploadIntentConsumptionRequest): Promise<void>;
20
32
  cancelUploadIntent(event: H3Event, input: {