or3-provider-sqlite 0.0.4 → 0.0.6

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 (27) hide show
  1. package/README.md +112 -19
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/server/admin/adapters/sync-sqlite.js +90 -16
  4. package/dist/runtime/server/admin/stores/sqlite-store.js +7 -8
  5. package/dist/runtime/server/auth/sqlite-auth-workspace-store.d.ts +5 -0
  6. package/dist/runtime/server/auth/sqlite-auth-workspace-store.js +368 -40
  7. package/dist/runtime/server/connect/sqlite-connect-store.js +19 -5
  8. package/dist/runtime/server/db/d1-dialect.d.ts +39 -0
  9. package/dist/runtime/server/db/d1-dialect.js +70 -0
  10. package/dist/runtime/server/db/d1.d.ts +13 -0
  11. package/dist/runtime/server/db/d1.js +30 -0
  12. package/dist/runtime/server/db/kysely.d.ts +53 -15
  13. package/dist/runtime/server/db/kysely.js +374 -16
  14. package/dist/runtime/server/db/migrate.js +3 -1
  15. package/dist/runtime/server/db/migrations/016_connect_runtime_binding.d.ts +4 -0
  16. package/dist/runtime/server/db/migrations/016_connect_runtime_binding.js +28 -0
  17. package/dist/runtime/server/db/schema.d.ts +3 -0
  18. package/dist/runtime/server/plugins/register.js +33 -13
  19. package/dist/runtime/server/rate-limit/sqlite-provider.d.ts +2 -0
  20. package/dist/runtime/server/rate-limit/sqlite-provider.js +70 -1
  21. package/dist/runtime/server/sync/maintenance-state.d.ts +26 -0
  22. package/dist/runtime/server/sync/maintenance-state.js +66 -0
  23. package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.d.ts +15 -4
  24. package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.js +780 -21
  25. package/dist/runtime/server/webhooks/sqlite-webhook-store.d.ts +3 -2
  26. package/dist/runtime/server/webhooks/sqlite-webhook-store.js +90 -39
  27. package/package.json +10 -6
@@ -0,0 +1,30 @@
1
+ import { getD1Database } from "./kysely.js";
2
+ function assertSuccess(result) {
3
+ if (result.success === false) {
4
+ throw new Error(result.error ?? "Cloudflare D1 statement failed.");
5
+ }
6
+ if (result.error) throw new Error(result.error);
7
+ }
8
+ export async function d1All(sql, ...parameters) {
9
+ const result = await getD1Database().prepare(sql).bind(...parameters).all();
10
+ assertSuccess(result);
11
+ return result.results ?? [];
12
+ }
13
+ export async function d1Run(sql, ...parameters) {
14
+ const result = await getD1Database().prepare(sql).bind(...parameters).run();
15
+ assertSuccess(result);
16
+ return result;
17
+ }
18
+ export async function d1Batch(statements) {
19
+ const database = getD1Database();
20
+ if (!database.batch) {
21
+ throw new Error("The configured D1 binding does not support batch().");
22
+ }
23
+ const results = await database.batch(
24
+ statements.map(
25
+ ({ sql, parameters = [] }) => database.prepare(sql).bind(...parameters)
26
+ )
27
+ );
28
+ for (const result of results) assertSuccess(result);
29
+ return results;
30
+ }
@@ -1,32 +1,70 @@
1
1
  /**
2
- * Kysely singleton for the SQLite provider.
2
+ * Shared database initialization for the SQLite provider.
3
3
  *
4
- * Creates a single Kysely instance backed by better-sqlite3.
5
- * Applies WAL + NORMAL pragmas for safe concurrent reads.
4
+ * The default remains better-sqlite3. Bun and Turso expose the same
5
+ * synchronous SQLite shape, while Cloudflare D1 uses its native async binding.
6
6
  */
7
7
  import { Kysely } from 'kysely';
8
- import Database from 'better-sqlite3';
8
+ import { type D1Database } from './d1-dialect.js';
9
9
  import type { Or3SqliteDb } from './schema.js';
10
+ export type SqliteDriver = 'better-sqlite3' | 'bun' | 'turso' | 'd1';
11
+ export interface SqliteRawStatement {
12
+ readonly reader?: boolean;
13
+ all(...parameters: unknown[]): unknown[];
14
+ get(...parameters: unknown[]): unknown;
15
+ run(...parameters: unknown[]): {
16
+ changes: number;
17
+ lastInsertRowid?: number | bigint;
18
+ };
19
+ iterate(...parameters: unknown[]): IterableIterator<unknown>;
20
+ }
21
+ type SqliteTransaction<T> = ((...args: unknown[]) => T) & {
22
+ immediate: (...args: unknown[]) => T;
23
+ };
24
+ export interface SqliteRawDatabase {
25
+ close(): void;
26
+ prepare(sql: string): SqliteRawStatement;
27
+ pragma?(source: string): unknown;
28
+ exec(source: string): unknown;
29
+ transaction<T>(fn: (...args: unknown[]) => T): SqliteTransaction<T>;
30
+ }
10
31
  export interface SqliteDbOptions {
11
- path: string;
32
+ /** SQLite file path for better-sqlite3 and Bun. */
33
+ path?: string;
34
+ /** Explicitly selects a native runtime. Defaults to better-sqlite3. */
35
+ driver?: SqliteDriver | string;
12
36
  journalMode?: string;
13
37
  synchronous?: string;
38
+ /** Turso/libSQL database URL. */
39
+ tursoUrl?: string;
40
+ /** Turso/libSQL auth token. */
41
+ tursoAuthToken?: string;
42
+ /** Cloudflare D1 binding name. Defaults to DB. */
43
+ d1Binding?: string;
44
+ /** Allows integrations and tests to provide a D1 binding directly. */
45
+ d1Database?: D1Database;
14
46
  }
15
47
  /**
16
- * Get or create the singleton Kysely DB.
17
- * First call initializes the connection and sets pragmas.
48
+ * Initialize the singleton once during server startup. This is async because
49
+ * all native runtimes are loaded lazily, keeping Workers bundles Node-free.
18
50
  */
19
- export declare function getSqliteDb(options?: SqliteDbOptions): Kysely<Or3SqliteDb>;
51
+ export declare function initializeSqliteDb(options?: SqliteDbOptions): Promise<Kysely<Or3SqliteDb>>;
20
52
  /**
21
- * Get the underlying better-sqlite3 instance for raw transactions.
22
- * Only available after getSqliteDb() has been called.
53
+ * Return the initialized Kysely singleton.
23
54
  */
24
- export declare function getRawDb(): InstanceType<typeof Database>;
55
+ export declare function getSqliteDb(): Kysely<Or3SqliteDb>;
56
+ /** The selected driver, resolving the current environment before initialization. */
57
+ export declare function getSqliteDriver(): SqliteDriver;
58
+ export declare function isD1Driver(): boolean;
25
59
  /**
26
- * Destroy the connection (for tests/cleanup).
60
+ * Get the native local SQLite connection for synchronous compatibility paths.
61
+ * D1 intentionally has no synchronous raw connection.
27
62
  */
63
+ export declare function getRawDb(): SqliteRawDatabase;
64
+ /** Return the initialized D1 binding when the D1 driver is selected. */
65
+ export declare function getD1Database(): D1Database;
66
+ /** Destroy the current connection (primarily for tests). */
28
67
  export declare function destroySqliteDb(): Promise<void>;
29
- /**
30
- * Reset the module-level singleton (for tests only).
31
- */
68
+ /** Reset module state for tests. */
32
69
  export declare function _resetForTest(): void;
70
+ export {};
@@ -1,14 +1,46 @@
1
1
  import { Kysely, SqliteDialect } from "kysely";
2
- import Database from "better-sqlite3";
2
+ import { D1Dialect } from "./d1-dialect.js";
3
3
  let instance = null;
4
4
  let rawDb = null;
5
+ let d1Db = null;
6
+ let selectedDriver = null;
7
+ let initialization = null;
5
8
  function envFlag(value) {
6
9
  if (!value) return false;
7
10
  const normalized = value.trim().toLowerCase();
8
11
  return normalized === "1" || normalized === "true" || normalized === "yes";
9
12
  }
10
- export function getSqliteDb(options) {
11
- if (instance) return instance;
13
+ function normalizeDriver(value) {
14
+ switch (value?.trim().toLowerCase()) {
15
+ case void 0:
16
+ case "":
17
+ case "better-sqlite3":
18
+ case "better":
19
+ case "sqlite":
20
+ return "better-sqlite3";
21
+ case "bun":
22
+ case "bun:sqlite":
23
+ return "bun";
24
+ case "turso":
25
+ case "libsql":
26
+ return "turso";
27
+ case "d1":
28
+ case "cloudflare-d1":
29
+ case "cloudflare":
30
+ return "d1";
31
+ default:
32
+ throw new Error(
33
+ `Unsupported OR3_SQLITE_DRIVER value "${value}". Use better-sqlite3, bun, turso, or d1.`
34
+ );
35
+ }
36
+ }
37
+ function resolveDriver(options) {
38
+ const configured = options?.driver ?? process.env.OR3_SQLITE_DRIVER;
39
+ if (configured) return normalizeDriver(configured);
40
+ if (options?.d1Database) return "d1";
41
+ return "better-sqlite3";
42
+ }
43
+ function resolveLocalPath(options) {
12
44
  const isTestEnv = process.env.NODE_ENV === "test" || envFlag(process.env.VITEST);
13
45
  const allowInMemory = envFlag(process.env.OR3_SQLITE_ALLOW_IN_MEMORY);
14
46
  const strictMode = envFlag(process.env.OR3_SQLITE_STRICT);
@@ -34,29 +66,355 @@ export function getSqliteDb(options) {
34
66
  "[or3-sqlite] OR3_SQLITE_ALLOW_IN_MEMORY=true enabled. Data will be lost on process restart."
35
67
  );
36
68
  }
37
- const journalMode = options?.journalMode ?? process.env.OR3_SQLITE_PRAGMA_JOURNAL_MODE ?? "WAL";
38
- const synchronous = options?.synchronous ?? process.env.OR3_SQLITE_PRAGMA_SYNCHRONOUS ?? "NORMAL";
39
- rawDb = new Database(path);
40
- rawDb.pragma(`journal_mode = ${journalMode}`);
41
- rawDb.pragma(`synchronous = ${synchronous}`);
42
- rawDb.pragma("foreign_keys = ON");
69
+ return path;
70
+ }
71
+ function normalizeParameters(parameters) {
72
+ return parameters.length === 1 && Array.isArray(parameters[0]) ? parameters[0] : parameters;
73
+ }
74
+ function isReaderStatement(sql) {
75
+ const normalized = sql.trim().toLowerCase();
76
+ return /^(select|pragma|explain|with)\b/.test(normalized) || /\breturning\b/.test(normalized);
77
+ }
78
+ class BunStatementAdapter {
79
+ constructor(statement, sql) {
80
+ this.statement = statement;
81
+ this.sql = sql;
82
+ }
83
+ get reader() {
84
+ return this.statement.columnNames ? this.statement.columnNames.length > 0 : isReaderStatement(this.sql);
85
+ }
86
+ all(...parameters) {
87
+ return this.statement.all(...normalizeParameters(parameters));
88
+ }
89
+ get(...parameters) {
90
+ return this.statement.get(...normalizeParameters(parameters));
91
+ }
92
+ run(...parameters) {
93
+ const result = this.statement.run(...normalizeParameters(parameters));
94
+ return {
95
+ changes: result.changes ?? 0,
96
+ lastInsertRowid: result.lastInsertRowid
97
+ };
98
+ }
99
+ iterate(...parameters) {
100
+ const values = normalizeParameters(parameters);
101
+ return this.statement.iterate ? this.statement.iterate(...values) : this.statement.all(...values)[Symbol.iterator]();
102
+ }
103
+ }
104
+ class BunDatabaseAdapter {
105
+ constructor(database) {
106
+ this.database = database;
107
+ }
108
+ close() {
109
+ this.database.close();
110
+ }
111
+ prepare(sql) {
112
+ return new BunStatementAdapter(this.database.prepare(sql), sql);
113
+ }
114
+ pragma(source) {
115
+ if (this.database.run) return this.database.run(`PRAGMA ${source}`);
116
+ return this.database.exec?.(`PRAGMA ${source}`);
117
+ }
118
+ exec(source) {
119
+ return this.database.exec?.(source);
120
+ }
121
+ transaction(fn) {
122
+ if (!this.database.transaction) {
123
+ throw new Error("bun:sqlite does not expose transaction() in this Bun runtime.");
124
+ }
125
+ const transaction = this.database.transaction(fn);
126
+ if (!transaction.immediate) {
127
+ transaction.immediate = (...args) => transaction(...args);
128
+ }
129
+ return transaction;
130
+ }
131
+ }
132
+ function stripLibsqlMetadata(value) {
133
+ if (!isRecord(value) || !("_metadata" in value)) return value;
134
+ const { _metadata: _ignored, ...row } = value;
135
+ return row;
136
+ }
137
+ class TursoStatementAdapter {
138
+ constructor(statement) {
139
+ this.statement = statement;
140
+ }
141
+ get reader() {
142
+ return this.statement.reader;
143
+ }
144
+ all(...parameters) {
145
+ return this.statement.all(...parameters).map(stripLibsqlMetadata);
146
+ }
147
+ get(...parameters) {
148
+ return stripLibsqlMetadata(this.statement.get(...parameters));
149
+ }
150
+ run(...parameters) {
151
+ return stripLibsqlMetadata(
152
+ this.statement.run(...parameters)
153
+ );
154
+ }
155
+ iterate(...parameters) {
156
+ return Array.from(
157
+ this.statement.iterate(...parameters),
158
+ stripLibsqlMetadata
159
+ )[Symbol.iterator]();
160
+ }
161
+ }
162
+ class TursoDatabaseAdapter {
163
+ constructor(database) {
164
+ this.database = database;
165
+ }
166
+ close() {
167
+ this.database.close();
168
+ }
169
+ prepare(sql) {
170
+ return new TursoStatementAdapter(this.database.prepare(sql));
171
+ }
172
+ pragma(source) {
173
+ return this.database.pragma?.(source);
174
+ }
175
+ exec(source) {
176
+ return this.database.exec(source);
177
+ }
178
+ transaction(fn) {
179
+ return this.database.transaction(fn);
180
+ }
181
+ }
182
+ class KyselyStatementAdapter {
183
+ constructor(statement, sql) {
184
+ this.statement = statement;
185
+ this.sql = sql;
186
+ }
187
+ get reader() {
188
+ return this.statement.reader ?? isReaderStatement(this.sql);
189
+ }
190
+ all(parameters) {
191
+ return this.statement.all(parameters);
192
+ }
193
+ run(parameters) {
194
+ const result = this.statement.run(parameters);
195
+ return {
196
+ changes: result.changes,
197
+ lastInsertRowid: result.lastInsertRowid ?? 0
198
+ };
199
+ }
200
+ iterate(parameters) {
201
+ return this.statement.iterate(parameters);
202
+ }
203
+ }
204
+ class KyselyDatabaseAdapter {
205
+ constructor(database) {
206
+ this.database = database;
207
+ }
208
+ close() {
209
+ this.database.close();
210
+ }
211
+ prepare(sql) {
212
+ return new KyselyStatementAdapter(this.database.prepare(sql), sql);
213
+ }
214
+ }
215
+ function validatePragmaValue(name, value) {
216
+ const normalized = value.trim();
217
+ if (!/^[a-z0-9_]+$/i.test(normalized)) {
218
+ throw new Error(`Invalid ${name} pragma value.`);
219
+ }
220
+ return normalized;
221
+ }
222
+ function applyLocalPragmas(db, options) {
223
+ const journalMode = validatePragmaValue(
224
+ "journal_mode",
225
+ options?.journalMode ?? process.env.OR3_SQLITE_PRAGMA_JOURNAL_MODE ?? "WAL"
226
+ );
227
+ const synchronous = validatePragmaValue(
228
+ "synchronous",
229
+ options?.synchronous ?? process.env.OR3_SQLITE_PRAGMA_SYNCHRONOUS ?? "NORMAL"
230
+ );
231
+ db.pragma?.(`journal_mode = ${journalMode}`);
232
+ db.pragma?.(`synchronous = ${synchronous}`);
233
+ db.pragma?.("foreign_keys = ON");
234
+ }
235
+ async function importRuntimeModule(moduleName) {
236
+ const imported = await import(
237
+ /* @vite-ignore */
238
+ moduleName
239
+ );
240
+ if (!isRecord(imported)) {
241
+ throw new Error(`Runtime module "${moduleName}" did not export an object.`);
242
+ }
243
+ return imported;
244
+ }
245
+ async function importCloudflareWorkersModule() {
246
+ const imported = await import(
247
+ /* @vite-ignore */
248
+ "cloudflare:workers"
249
+ );
250
+ if (!isRecord(imported)) {
251
+ throw new Error("cloudflare:workers did not export an object.");
252
+ }
253
+ return imported;
254
+ }
255
+ function isRecord(value) {
256
+ return typeof value === "object" && value !== null;
257
+ }
258
+ function asDatabaseConstructor(value) {
259
+ return typeof value === "function" ? value : null;
260
+ }
261
+ async function createBetterSqliteDatabase(path) {
262
+ let imported;
263
+ try {
264
+ imported = await importRuntimeModule("better-sqlite3");
265
+ } catch (error) {
266
+ throw new Error(
267
+ "Unable to load better-sqlite3. Install it or set OR3_SQLITE_DRIVER to bun, turso, or d1.",
268
+ { cause: error }
269
+ );
270
+ }
271
+ const Database = asDatabaseConstructor(imported.default);
272
+ if (!Database) {
273
+ throw new Error("better-sqlite3 did not provide a Database constructor.");
274
+ }
275
+ return new Database(path);
276
+ }
277
+ async function createBunSqliteDatabase(path) {
278
+ let imported;
279
+ try {
280
+ imported = await import(
281
+ /* @vite-ignore */
282
+ "bun:sqlite"
283
+ );
284
+ } catch (error) {
285
+ throw new Error(
286
+ "OR3_SQLITE_DRIVER=bun requires Bun and its built-in bun:sqlite module.",
287
+ { cause: error }
288
+ );
289
+ }
290
+ const Database = asDatabaseConstructor(
291
+ imported.Database ?? imported.default
292
+ );
293
+ if (!Database) {
294
+ throw new Error("bun:sqlite did not provide a Database constructor.");
295
+ }
296
+ return new BunDatabaseAdapter(new Database(path));
297
+ }
298
+ async function createTursoDatabase(options) {
299
+ const url = options?.tursoUrl ?? process.env.OR3_SQLITE_TURSO_URL;
300
+ const authToken = options?.tursoAuthToken ?? process.env.OR3_SQLITE_TURSO_AUTH_TOKEN;
301
+ if (!url?.trim()) {
302
+ throw new Error("OR3_SQLITE_TURSO_URL is required when OR3_SQLITE_DRIVER=turso.");
303
+ }
304
+ if (!authToken?.trim()) {
305
+ throw new Error(
306
+ "OR3_SQLITE_TURSO_AUTH_TOKEN is required when OR3_SQLITE_DRIVER=turso."
307
+ );
308
+ }
309
+ let imported;
310
+ try {
311
+ imported = await importRuntimeModule("libsql");
312
+ } catch (error) {
313
+ throw new Error(
314
+ "Unable to load libsql for Turso. Install the libsql package and try again.",
315
+ { cause: error }
316
+ );
317
+ }
318
+ const Database = asDatabaseConstructor(
319
+ imported.default ?? imported.Database
320
+ );
321
+ if (!Database) {
322
+ throw new Error("libsql did not provide a Database constructor.");
323
+ }
324
+ return new TursoDatabaseAdapter(new Database(url, { authToken }));
325
+ }
326
+ async function resolveD1Database(options) {
327
+ if (options?.d1Database) return options.d1Database;
328
+ const binding = (options?.d1Binding ?? process.env.OR3_SQLITE_D1_BINDING ?? "DB").trim();
329
+ if (!binding) {
330
+ throw new Error("OR3_SQLITE_D1_BINDING must name a Cloudflare D1 binding.");
331
+ }
332
+ let imported;
333
+ try {
334
+ imported = await importCloudflareWorkersModule();
335
+ } catch (error) {
336
+ throw new Error(
337
+ "OR3_SQLITE_DRIVER=d1 requires a Cloudflare Workers runtime with a D1 binding.",
338
+ { cause: error }
339
+ );
340
+ }
341
+ const env = imported.env;
342
+ const database = isRecord(env) ? env[binding] : void 0;
343
+ if (!isD1Database(database)) {
344
+ throw new Error(
345
+ `Cloudflare D1 binding "${binding}" was not found. Set OR3_SQLITE_D1_BINDING to the binding name from wrangler.jsonc.`
346
+ );
347
+ }
348
+ return database;
349
+ }
350
+ function isD1Database(value) {
351
+ return isRecord(value) && typeof value.prepare === "function";
352
+ }
353
+ async function initialize(options) {
354
+ const driver = resolveDriver(options);
355
+ selectedDriver = driver;
356
+ if (driver === "d1") {
357
+ d1Db = await resolveD1Database(options);
358
+ instance = new Kysely({
359
+ dialect: new D1Dialect(d1Db)
360
+ });
361
+ return instance;
362
+ }
363
+ const path = resolveLocalPath(options);
364
+ rawDb = driver === "bun" ? await createBunSqliteDatabase(path) : driver === "turso" ? await createTursoDatabase(options) : await createBetterSqliteDatabase(path);
365
+ if (driver !== "turso") applyLocalPragmas(rawDb, options);
43
366
  instance = new Kysely({
44
- dialect: new SqliteDialect({ database: rawDb })
367
+ dialect: new SqliteDialect({ database: new KyselyDatabaseAdapter(rawDb) })
45
368
  });
46
369
  return instance;
47
370
  }
371
+ export async function initializeSqliteDb(options) {
372
+ if (instance) return instance;
373
+ if (!initialization) {
374
+ initialization = initialize(options).finally(() => {
375
+ initialization = null;
376
+ });
377
+ }
378
+ return initialization;
379
+ }
380
+ export function getSqliteDb() {
381
+ if (!instance) {
382
+ throw new Error(
383
+ "SQLite DB not initialized \u2014 await initializeSqliteDb() before calling getSqliteDb()."
384
+ );
385
+ }
386
+ return instance;
387
+ }
388
+ export function getSqliteDriver() {
389
+ return selectedDriver ?? resolveDriver(void 0);
390
+ }
391
+ export function isD1Driver() {
392
+ return getSqliteDriver() === "d1";
393
+ }
48
394
  export function getRawDb() {
49
- if (!rawDb) throw new Error("SQLite DB not initialized \u2014 call getSqliteDb() first");
395
+ if (!rawDb) {
396
+ const suffix = isD1Driver() ? " Cloudflare D1 uses an async native binding." : "";
397
+ throw new Error(`SQLite raw DB not initialized.${suffix}`);
398
+ }
50
399
  return rawDb;
51
400
  }
52
- export async function destroySqliteDb() {
53
- if (instance) {
54
- await instance.destroy();
55
- instance = null;
56
- rawDb = null;
401
+ export function getD1Database() {
402
+ if (!d1Db) {
403
+ throw new Error("Cloudflare D1 is not initialized \u2014 call initializeSqliteDb() first.");
57
404
  }
405
+ return d1Db;
406
+ }
407
+ export async function destroySqliteDb() {
408
+ if (instance) await instance.destroy();
409
+ instance = null;
410
+ rawDb = null;
411
+ d1Db = null;
412
+ selectedDriver = null;
58
413
  }
59
414
  export function _resetForTest() {
60
415
  instance = null;
61
416
  rawDb = null;
417
+ d1Db = null;
418
+ selectedDriver = null;
419
+ initialization = null;
62
420
  }
@@ -14,6 +14,7 @@ import * as m012 from "./migrations/012_connect_credential_redelivery.js";
14
14
  import * as m013 from "./migrations/013_connect_environment_lifecycle.js";
15
15
  import * as m014 from "./migrations/014_connect_retention_activation.js";
16
16
  import * as m015 from "./migrations/015_rate_limits.js";
17
+ import * as m016 from "./migrations/016_connect_runtime_binding.js";
17
18
  const REQUIRED_SCHEMA_TABLES = [
18
19
  "users",
19
20
  "auth_accounts",
@@ -55,7 +56,8 @@ const migrations = {
55
56
  "012_connect_credential_redelivery": m012,
56
57
  "013_connect_environment_lifecycle": m013,
57
58
  "014_connect_retention_activation": m014,
58
- "015_rate_limits": m015
59
+ "015_rate_limits": m015,
60
+ "016_connect_runtime_binding": m016
59
61
  };
60
62
  class StaticMigrationProvider {
61
63
  async getMigrations() {
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Adds the additive runtime binding used by Runs-compatible Connect hosts. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ const result = await sql`
4
+ PRAGMA table_info(connect_environments)
5
+ `.execute(db);
6
+ const columns = new Set(result.rows.map((column) => column.name));
7
+ if (!columns.has("driver")) {
8
+ await sql`
9
+ ALTER TABLE connect_environments
10
+ ADD COLUMN driver TEXT
11
+ `.execute(db);
12
+ }
13
+ if (!columns.has("runtime")) {
14
+ await sql`
15
+ ALTER TABLE connect_environments
16
+ ADD COLUMN runtime TEXT
17
+ `.execute(db);
18
+ }
19
+ if (!columns.has("base_path")) {
20
+ await sql`
21
+ ALTER TABLE connect_environments
22
+ ADD COLUMN base_path TEXT
23
+ `.execute(db);
24
+ }
25
+ }
26
+ export async function down(db) {
27
+ void db;
28
+ }
@@ -164,6 +164,9 @@ export interface ConnectEnvironmentsTable {
164
164
  control_token_hash: string;
165
165
  access_credential_ciphertext: string;
166
166
  tunnel_secret_ciphertext: string | null;
167
+ driver: 'intern' | 'runs' | null;
168
+ runtime: 'intern' | 'openclaw' | 'hermes' | null;
169
+ base_path: '/' | '/or3/' | null;
167
170
  status: 'provisioning' | 'active' | 'revoking' | 'revoked' | 'error';
168
171
  lifecycle_attempts: number;
169
172
  lifecycle_next_attempt_at: number;
@@ -16,18 +16,24 @@ import {
16
16
  } from "../admin/stores/sqlite-store.js";
17
17
  import { sqliteSyncAdminAdapter } from "../admin/adapters/sync-sqlite.js";
18
18
  import { createSqliteConnectStore } from "../connect/sqlite-connect-store.js";
19
- import { getSqliteDb } from "../db/kysely.js";
19
+ import { getSqliteDriver, initializeSqliteDb } from "../db/kysely.js";
20
20
  import { runMigrations } from "../db/migrate.js";
21
21
  import { sqliteRateLimitProvider } from "../rate-limit/sqlite-provider.js";
22
22
  import { useRuntimeConfig } from "#imports";
23
23
  const SQLITE_PROVIDER_ID = "sqlite";
24
- export default defineNitroPlugin(async () => {
24
+ let d1RuntimeInitialization;
25
+ export default defineNitroPlugin(async (nitroApp) => {
25
26
  const config = useRuntimeConfig();
26
27
  if (!config.auth?.enabled) return;
27
28
  const syncSelected = config.sync?.enabled === true && config.sync?.provider === SQLITE_PROVIDER_ID;
28
29
  const connectSelected = config.connect?.enabled === true && config.connect?.provider === SQLITE_PROVIDER_ID;
29
30
  if (!syncSelected && !connectSelected) return;
30
- const db = getSqliteDb();
31
+ const driver = getSqliteDriver();
32
+ if (driver === "d1" && connectSelected) {
33
+ throw new Error(
34
+ "[or3-sqlite] Cloudflare D1 supports Auth and Sync, but OR3 Connect still requires a synchronous SQLite runtime. Set OR3_CONNECT_ENABLED=false or choose better-sqlite3, Bun, or Turso."
35
+ );
36
+ }
31
37
  registerAuthWorkspaceStore({
32
38
  id: SQLITE_PROVIDER_ID,
33
39
  order: 100,
@@ -48,24 +54,38 @@ export default defineNitroPlugin(async () => {
48
54
  create: createSqliteConnectStore
49
55
  });
50
56
  }
51
- registerWebhookStore({
52
- id: SQLITE_PROVIDER_ID,
53
- order: 100,
54
- create: createSqliteWebhookStore
55
- });
57
+ if (driver !== "d1") {
58
+ registerWebhookStore({
59
+ id: SQLITE_PROVIDER_ID,
60
+ order: 100,
61
+ create: createSqliteWebhookStore
62
+ });
63
+ } else {
64
+ console.warn(
65
+ "[or3-sqlite] Webhook persistence is unavailable with Cloudflare D1."
66
+ );
67
+ }
56
68
  registerAdminStoreProvider({
57
69
  id: SQLITE_PROVIDER_ID,
58
70
  createWorkspaceAccessStore: createSqliteWorkspaceAccessStore,
59
71
  createWorkspaceSettingsStore: createSqliteWorkspaceSettingsStore,
60
72
  createAdminUserStore: createSqliteAdminUserStore,
61
73
  getCapabilities: () => ({
62
- supportsServerSideAdmin: true,
63
- supportsUserSearch: true,
64
- supportsWorkspaceList: true,
65
- supportsWorkspaceManagement: true,
66
- supportsDeploymentAdminGrants: true
74
+ supportsServerSideAdmin: driver !== "d1",
75
+ supportsUserSearch: driver !== "d1",
76
+ supportsWorkspaceList: driver !== "d1",
77
+ supportsWorkspaceManagement: driver !== "d1",
78
+ supportsDeploymentAdminGrants: driver !== "d1"
67
79
  })
68
80
  });
69
81
  registerProviderAdminAdapter(sqliteSyncAdminAdapter);
82
+ if (driver === "d1") {
83
+ nitroApp.hooks.hook("request", async () => {
84
+ d1RuntimeInitialization ??= initializeSqliteDb().then(runMigrations);
85
+ await d1RuntimeInitialization;
86
+ });
87
+ return;
88
+ }
89
+ const db = await initializeSqliteDb();
70
90
  await runMigrations(db);
71
91
  });
@@ -3,5 +3,7 @@ export declare class SqliteRateLimitProvider implements RateLimitProvider {
3
3
  readonly name = "sqlite";
4
4
  checkAndRecord(key: string, config: RateLimitConfig): Promise<RateLimitResult>;
5
5
  getStats(key: string, config: RateLimitConfig): Promise<RateLimitStats | null>;
6
+ private checkAndRecordInD1;
7
+ private getStatsInD1;
6
8
  }
7
9
  export declare const sqliteRateLimitProvider: SqliteRateLimitProvider;