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,261 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ const environmentColumns = await tableColumns(db, "connect_environments");
4
+ if (environmentColumns.has("lifecycle_claim_token")) return;
5
+ await dropConnectIndexes(db);
6
+ await sql`
7
+ ALTER TABLE connect_device_authorizations
8
+ RENAME TO connect_device_authorizations_before_lifecycle
9
+ `.execute(db);
10
+ await sql`
11
+ ALTER TABLE connect_environments
12
+ RENAME TO connect_environments_before_lifecycle
13
+ `.execute(db);
14
+ await createAuthorizationTable(db, true);
15
+ await createEnvironmentTable(db, true);
16
+ await sql`
17
+ INSERT INTO connect_device_authorizations (
18
+ id, device_code_hash, user_code_hash, status, host_json,
19
+ approved_user_id, approved_workspace_id, environment_id,
20
+ credential_ciphertext, credential_delivery_started_at,
21
+ credential_redeliver_until, expires_at, created_at, updated_at
22
+ )
23
+ SELECT
24
+ id, device_code_hash, user_code_hash, status, host_json,
25
+ approved_user_id, approved_workspace_id, environment_id,
26
+ credential_ciphertext, credential_delivery_started_at,
27
+ credential_redeliver_until, expires_at, created_at, updated_at
28
+ FROM connect_device_authorizations_before_lifecycle
29
+ `.execute(db);
30
+ await sql`
31
+ INSERT INTO connect_environments (
32
+ id, user_id, workspace_id, name, platform, architecture,
33
+ host_id, signing_public_key, noise_public_key, hostname,
34
+ tunnel_id, dns_record_id, control_token_hash,
35
+ access_credential_ciphertext, status, lifecycle_attempts,
36
+ lifecycle_next_attempt_at, last_seen_at, created_at, updated_at,
37
+ revoked_at
38
+ )
39
+ SELECT
40
+ id, user_id, workspace_id, name, platform, architecture,
41
+ host_id, signing_public_key, noise_public_key, hostname,
42
+ tunnel_id, dns_record_id, control_token_hash,
43
+ access_credential_ciphertext, status, 0, 0, last_seen_at,
44
+ created_at, updated_at, revoked_at
45
+ FROM connect_environments_before_lifecycle
46
+ `.execute(db);
47
+ await sql`
48
+ DROP TABLE connect_device_authorizations_before_lifecycle
49
+ `.execute(db);
50
+ await sql`DROP TABLE connect_environments_before_lifecycle`.execute(db);
51
+ }
52
+ export async function down(db) {
53
+ const environmentColumns = await tableColumns(db, "connect_environments");
54
+ if (!environmentColumns.has("lifecycle_claim_token")) return;
55
+ await dropConnectIndexes(db);
56
+ await sql`
57
+ ALTER TABLE connect_device_authorizations
58
+ RENAME TO connect_device_authorizations_with_lifecycle
59
+ `.execute(db);
60
+ await sql`
61
+ ALTER TABLE connect_environments
62
+ RENAME TO connect_environments_with_lifecycle
63
+ `.execute(db);
64
+ await createAuthorizationTable(db, false);
65
+ await createEnvironmentTable(db, false);
66
+ await sql`
67
+ INSERT INTO connect_device_authorizations (
68
+ id, device_code_hash, user_code_hash, status, host_json,
69
+ approved_user_id, approved_workspace_id, environment_id,
70
+ credential_ciphertext, credential_delivery_started_at,
71
+ credential_redeliver_until, expires_at, created_at, updated_at
72
+ )
73
+ SELECT
74
+ id, device_code_hash, user_code_hash,
75
+ CASE WHEN status = 'provisioning' THEN 'expired' ELSE status END,
76
+ host_json, approved_user_id, approved_workspace_id, environment_id,
77
+ credential_ciphertext, credential_delivery_started_at,
78
+ credential_redeliver_until, expires_at, created_at, updated_at
79
+ FROM connect_device_authorizations_with_lifecycle
80
+ `.execute(db);
81
+ await sql`
82
+ INSERT INTO connect_environments (
83
+ id, user_id, workspace_id, name, platform, architecture,
84
+ host_id, signing_public_key, noise_public_key, hostname,
85
+ tunnel_id, dns_record_id, control_token_hash,
86
+ access_credential_ciphertext, status, last_seen_at, created_at,
87
+ updated_at, revoked_at
88
+ )
89
+ SELECT
90
+ id, user_id, workspace_id, name, platform, architecture,
91
+ host_id, signing_public_key, noise_public_key, hostname,
92
+ tunnel_id, dns_record_id, control_token_hash,
93
+ access_credential_ciphertext,
94
+ CASE
95
+ WHEN status = 'provisioning' THEN 'error'
96
+ WHEN status = 'revoking' THEN 'revoked'
97
+ ELSE status
98
+ END,
99
+ last_seen_at, created_at, updated_at, revoked_at
100
+ FROM connect_environments_with_lifecycle
101
+ `.execute(db);
102
+ await sql`
103
+ DROP TABLE connect_device_authorizations_with_lifecycle
104
+ `.execute(db);
105
+ await sql`DROP TABLE connect_environments_with_lifecycle`.execute(db);
106
+ }
107
+ async function tableColumns(db, table) {
108
+ const result = await sql`
109
+ PRAGMA table_info(${sql.raw(table)})
110
+ `.execute(db);
111
+ return new Set(result.rows.map((column) => column.name));
112
+ }
113
+ async function createAuthorizationTable(db, lifecycle) {
114
+ const statuses = lifecycle ? "'pending', 'provisioning', 'approved', 'delivering', 'denied', 'consumed', 'expired'" : "'pending', 'approved', 'delivering', 'denied', 'consumed', 'expired'";
115
+ await sql`
116
+ CREATE TABLE connect_device_authorizations (
117
+ id TEXT PRIMARY KEY,
118
+ device_code_hash TEXT NOT NULL UNIQUE,
119
+ user_code_hash TEXT NOT NULL,
120
+ status TEXT NOT NULL CHECK (status IN (${sql.raw(statuses)})),
121
+ host_json TEXT NOT NULL,
122
+ approved_user_id TEXT,
123
+ approved_workspace_id TEXT,
124
+ environment_id TEXT,
125
+ credential_ciphertext TEXT,
126
+ credential_delivery_started_at INTEGER,
127
+ credential_redeliver_until INTEGER,
128
+ expires_at INTEGER NOT NULL,
129
+ created_at INTEGER NOT NULL,
130
+ updated_at INTEGER NOT NULL
131
+ )
132
+ `.execute(db);
133
+ await createAuthorizationIndexes(db);
134
+ }
135
+ async function createEnvironmentTable(db, lifecycle) {
136
+ if (!lifecycle) {
137
+ await sql`
138
+ CREATE TABLE connect_environments (
139
+ id TEXT PRIMARY KEY,
140
+ user_id TEXT NOT NULL,
141
+ workspace_id TEXT NOT NULL,
142
+ name TEXT NOT NULL,
143
+ platform TEXT NOT NULL,
144
+ architecture TEXT NOT NULL,
145
+ host_id TEXT,
146
+ signing_public_key TEXT,
147
+ noise_public_key TEXT,
148
+ hostname TEXT NOT NULL,
149
+ tunnel_id TEXT NOT NULL,
150
+ dns_record_id TEXT NOT NULL,
151
+ control_token_hash TEXT NOT NULL UNIQUE,
152
+ access_credential_ciphertext TEXT NOT NULL,
153
+ status TEXT NOT NULL CHECK (
154
+ status IN ('active', 'revoked', 'error')
155
+ ),
156
+ last_seen_at INTEGER,
157
+ created_at INTEGER NOT NULL,
158
+ updated_at INTEGER NOT NULL,
159
+ revoked_at INTEGER
160
+ )
161
+ `.execute(db);
162
+ await createEnvironmentIndexes(db, false);
163
+ return;
164
+ }
165
+ await sql`
166
+ CREATE TABLE connect_environments (
167
+ id TEXT PRIMARY KEY,
168
+ user_id TEXT NOT NULL,
169
+ workspace_id TEXT NOT NULL,
170
+ name TEXT NOT NULL,
171
+ platform TEXT NOT NULL,
172
+ architecture TEXT NOT NULL,
173
+ host_id TEXT,
174
+ signing_public_key TEXT,
175
+ noise_public_key TEXT,
176
+ authorization_id TEXT,
177
+ hostname TEXT NOT NULL,
178
+ tunnel_id TEXT NOT NULL,
179
+ dns_record_id TEXT NOT NULL,
180
+ control_token_hash TEXT NOT NULL UNIQUE,
181
+ access_credential_ciphertext TEXT NOT NULL,
182
+ tunnel_secret_ciphertext TEXT,
183
+ status TEXT NOT NULL CHECK (
184
+ status IN (
185
+ 'provisioning', 'active', 'revoking', 'revoked', 'error'
186
+ )
187
+ ),
188
+ lifecycle_attempts INTEGER NOT NULL DEFAULT 0,
189
+ lifecycle_next_attempt_at INTEGER NOT NULL DEFAULT 0,
190
+ lifecycle_claim_token TEXT,
191
+ lifecycle_claimed_until INTEGER,
192
+ provisioning_deadline_at INTEGER,
193
+ lifecycle_error TEXT,
194
+ last_seen_at INTEGER,
195
+ created_at INTEGER NOT NULL,
196
+ updated_at INTEGER NOT NULL,
197
+ revoked_at INTEGER
198
+ )
199
+ `.execute(db);
200
+ await createEnvironmentIndexes(db, true);
201
+ }
202
+ async function dropConnectIndexes(db) {
203
+ const names = [
204
+ "connect_authorizations_status_expiry",
205
+ "connect_authorizations_user_code",
206
+ "connect_authorizations_environment",
207
+ "connect_environments_user_status",
208
+ "connect_environments_workspace_status",
209
+ "connect_environments_user_workspace_status_updated",
210
+ "connect_environments_user_workspace_control_token",
211
+ "connect_environments_user_workspace_id",
212
+ "connect_environments_lifecycle_due"
213
+ ];
214
+ for (const name of names) {
215
+ await sql`DROP INDEX IF EXISTS ${sql.raw(name)}`.execute(db);
216
+ }
217
+ }
218
+ async function createAuthorizationIndexes(db) {
219
+ await sql`
220
+ CREATE INDEX connect_authorizations_status_expiry
221
+ ON connect_device_authorizations(status, expires_at)
222
+ `.execute(db);
223
+ await sql`
224
+ CREATE INDEX connect_authorizations_user_code
225
+ ON connect_device_authorizations(user_code_hash, created_at)
226
+ `.execute(db);
227
+ await sql`
228
+ CREATE INDEX connect_authorizations_environment
229
+ ON connect_device_authorizations(environment_id)
230
+ `.execute(db);
231
+ }
232
+ async function createEnvironmentIndexes(db, lifecycle) {
233
+ await sql`
234
+ CREATE INDEX connect_environments_user_status
235
+ ON connect_environments(user_id, status, updated_at)
236
+ `.execute(db);
237
+ await sql`
238
+ CREATE INDEX connect_environments_workspace_status
239
+ ON connect_environments(workspace_id, status, updated_at)
240
+ `.execute(db);
241
+ await sql`
242
+ CREATE INDEX connect_environments_user_workspace_status_updated
243
+ ON connect_environments(user_id, workspace_id, status, updated_at)
244
+ `.execute(db);
245
+ await sql`
246
+ CREATE INDEX connect_environments_user_workspace_control_token
247
+ ON connect_environments(user_id, workspace_id, control_token_hash)
248
+ `.execute(db);
249
+ await sql`
250
+ CREATE INDEX connect_environments_user_workspace_id
251
+ ON connect_environments(user_id, workspace_id, id)
252
+ `.execute(db);
253
+ if (lifecycle) {
254
+ await sql`
255
+ CREATE INDEX connect_environments_lifecycle_due
256
+ ON connect_environments(
257
+ status, lifecycle_next_attempt_at, lifecycle_claimed_until
258
+ )
259
+ `.execute(db);
260
+ }
261
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Adds bounded retention and unclaimed-activation lifecycle indexes. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ const columns = await sql`
4
+ PRAGMA table_info(connect_environments)
5
+ `.execute(db);
6
+ const names = new Set(columns.rows.map((column) => column.name));
7
+ if (!names.has("activation_deadline_at")) {
8
+ await sql`
9
+ ALTER TABLE connect_environments
10
+ ADD COLUMN activation_deadline_at INTEGER
11
+ `.execute(db);
12
+ }
13
+ if (!names.has("activation_claimed_at")) {
14
+ await sql`
15
+ ALTER TABLE connect_environments
16
+ ADD COLUMN activation_claimed_at INTEGER
17
+ `.execute(db);
18
+ }
19
+ if (!names.has("relay_authenticator")) {
20
+ await sql`
21
+ ALTER TABLE connect_environments
22
+ ADD COLUMN relay_authenticator TEXT
23
+ `.execute(db);
24
+ }
25
+ await sql`
26
+ CREATE INDEX IF NOT EXISTS connect_authorizations_status_updated
27
+ ON connect_device_authorizations(status, updated_at, id)
28
+ `.execute(db);
29
+ await sql`
30
+ CREATE INDEX IF NOT EXISTS connect_environments_status_updated
31
+ ON connect_environments(status, updated_at, id)
32
+ `.execute(db);
33
+ await sql`
34
+ CREATE INDEX IF NOT EXISTS connect_environments_activation_due
35
+ ON connect_environments(
36
+ status, activation_claimed_at, activation_deadline_at, id
37
+ )
38
+ `.execute(db);
39
+ }
40
+ export async function down(db) {
41
+ await sql`DROP INDEX IF EXISTS connect_environments_activation_due`.execute(db);
42
+ await sql`DROP INDEX IF EXISTS connect_environments_status_updated`.execute(db);
43
+ await sql`DROP INDEX IF EXISTS connect_authorizations_status_updated`.execute(db);
44
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Durable deployment-wide fixed-window request budgets. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ 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 rate_limits (
5
+ key TEXT PRIMARY KEY,
6
+ count INTEGER NOT NULL,
7
+ window_started_at INTEGER NOT NULL,
8
+ expires_at INTEGER NOT NULL
9
+ )
10
+ `.execute(db);
11
+ await sql`
12
+ CREATE INDEX IF NOT EXISTS rate_limits_expiry
13
+ ON rate_limits(expires_at)
14
+ `.execute(db);
15
+ }
16
+ export async function down(db) {
17
+ await sql`DROP TABLE IF EXISTS rate_limits`.execute(db);
18
+ }
@@ -70,6 +70,7 @@ export interface DeviceCursorsTable {
70
70
  id: string;
71
71
  workspace_id: string;
72
72
  device_id: string;
73
+ owner_user_id: string | null;
73
74
  last_seen_version: number;
74
75
  updated_at: Generated<number>;
75
76
  }
@@ -80,6 +81,8 @@ export interface TombstonesTable {
80
81
  pk: string;
81
82
  deleted_at: number;
82
83
  clock: number;
84
+ hlc: string;
85
+ op_id: string;
83
86
  server_version: number;
84
87
  created_at: Generated<number>;
85
88
  }
@@ -90,10 +93,98 @@ export interface SyncedEntityTable {
90
93
  clock: number;
91
94
  hlc: string;
92
95
  device_id: string;
96
+ op_id: string;
93
97
  deleted: Generated<number>;
94
98
  created_at: Generated<number>;
95
99
  updated_at: Generated<number>;
96
100
  }
101
+ export interface SyncSnapshotsTable {
102
+ id: string;
103
+ workspace_id: string;
104
+ high_watermark: number;
105
+ tables_json: string;
106
+ created_at: number;
107
+ expires_at: number;
108
+ }
109
+ export interface SyncSnapshotItemsTable {
110
+ snapshot_id: string;
111
+ table_name: string;
112
+ pk: string;
113
+ kind: string;
114
+ payload_json: string | null;
115
+ clock: number;
116
+ hlc: string;
117
+ op_id: string;
118
+ server_deleted_at: number | null;
119
+ }
120
+ export interface UploadIntentsTable {
121
+ id: string;
122
+ workspace_id: string;
123
+ hash: string;
124
+ mime_type: string;
125
+ size_bytes: number;
126
+ reserved_bytes: number;
127
+ expires_at: number;
128
+ status: 'active' | 'consumed' | 'cancelled' | 'expired';
129
+ storage_id: string | null;
130
+ created_at: number;
131
+ consumed_at: number | null;
132
+ cancelled_at: number | null;
133
+ }
134
+ export interface ConnectDeviceAuthorizationsTable {
135
+ id: string;
136
+ device_code_hash: string;
137
+ user_code_hash: string;
138
+ status: 'pending' | 'provisioning' | 'approved' | 'delivering' | 'denied' | 'consumed' | 'expired';
139
+ host_json: string;
140
+ approved_user_id: string | null;
141
+ approved_workspace_id: string | null;
142
+ environment_id: string | null;
143
+ credential_ciphertext: string | null;
144
+ credential_delivery_started_at: number | null;
145
+ credential_redeliver_until: number | null;
146
+ expires_at: number;
147
+ created_at: number;
148
+ updated_at: number;
149
+ }
150
+ export interface ConnectEnvironmentsTable {
151
+ id: string;
152
+ user_id: string;
153
+ workspace_id: string;
154
+ name: string;
155
+ platform: string;
156
+ architecture: string;
157
+ host_id: string | null;
158
+ signing_public_key: string | null;
159
+ noise_public_key: string | null;
160
+ authorization_id: string | null;
161
+ hostname: string;
162
+ tunnel_id: string;
163
+ dns_record_id: string;
164
+ control_token_hash: string;
165
+ access_credential_ciphertext: string;
166
+ tunnel_secret_ciphertext: string | null;
167
+ status: 'provisioning' | 'active' | 'revoking' | 'revoked' | 'error';
168
+ lifecycle_attempts: number;
169
+ lifecycle_next_attempt_at: number;
170
+ lifecycle_claim_token: string | null;
171
+ lifecycle_claimed_until: number | null;
172
+ provisioning_deadline_at: number | null;
173
+ activation_deadline_at: number | null;
174
+ activation_claimed_at: number | null;
175
+ relay_authenticator: string | null;
176
+ lifecycle_error: string | null;
177
+ last_seen_at: number | null;
178
+ created_at: number;
179
+ updated_at: number;
180
+ revoked_at: number | null;
181
+ }
182
+ export interface RateLimitsTable {
183
+ key: string;
184
+ count: number;
185
+ window_started_at: number;
186
+ expires_at: number;
187
+ }
97
188
  export interface AdminUsersTable {
98
189
  user_id: string;
99
190
  created_at: number;
@@ -116,6 +207,12 @@ export interface Or3SqliteDb {
116
207
  change_log: ChangeLogTable;
117
208
  device_cursors: DeviceCursorsTable;
118
209
  tombstones: TombstonesTable;
210
+ sync_snapshots: SyncSnapshotsTable;
211
+ sync_snapshot_items: SyncSnapshotItemsTable;
212
+ upload_intents: UploadIntentsTable;
213
+ connect_device_authorizations: ConnectDeviceAuthorizationsTable;
214
+ connect_environments: ConnectEnvironmentsTable;
215
+ rate_limits: RateLimitsTable;
119
216
  s_threads: SyncedEntityTable;
120
217
  s_messages: SyncedEntityTable;
121
218
  s_projects: SyncedEntityTable;
@@ -2,34 +2,55 @@ import { registerAuthWorkspaceStore } from "~~/server/auth/store/registry";
2
2
  import { registerProviderAdminAdapter } from "~~/server/admin/providers/registry";
3
3
  import { registerAdminStoreProvider } from "~~/server/admin/stores/registry";
4
4
  import { registerSyncGatewayAdapter } from "~~/server/sync/gateway/registry";
5
+ import { registerWebhookStore } from "~~/server/utils/webhooks/store/registry";
6
+ import { registerConnectStore } from "~~/server/connect/store/registry";
7
+ import { registerRateLimitProvider } from "~~/server/utils/rate-limit/registry";
5
8
  import { createSqliteAuthWorkspaceStore } from "../auth/sqlite-auth-workspace-store.js";
6
9
  import { createSqliteSyncGatewayAdapter } from "../sync/sqlite-sync-gateway-adapter.js";
10
+ import { createSqliteWebhookStore } from "../webhooks/sqlite-webhook-store.js";
7
11
  import {
8
12
  createSqliteAdminUserStore,
9
13
  createSqliteWorkspaceAccessStore,
10
14
  createSqliteWorkspaceSettingsStore
11
15
  } from "../admin/stores/sqlite-store.js";
12
16
  import { sqliteSyncAdminAdapter } from "../admin/adapters/sync-sqlite.js";
17
+ import { createSqliteConnectStore } from "../connect/sqlite-connect-store.js";
13
18
  import { getSqliteDb } from "../db/kysely.js";
14
19
  import { runMigrations } from "../db/migrate.js";
20
+ import { sqliteRateLimitProvider } from "../rate-limit/sqlite-provider.js";
15
21
  import { useRuntimeConfig } from "#imports";
16
22
  const SQLITE_PROVIDER_ID = "sqlite";
17
23
  export default defineNitroPlugin(async () => {
18
24
  const config = useRuntimeConfig();
19
25
  if (!config.auth?.enabled) return;
20
- if (!config.sync?.enabled) return;
21
- if (config.sync?.provider !== SQLITE_PROVIDER_ID) return;
26
+ const syncSelected = config.sync?.enabled === true && config.sync?.provider === SQLITE_PROVIDER_ID;
27
+ const connectSelected = config.connect?.enabled === true && config.connect?.provider === SQLITE_PROVIDER_ID;
28
+ if (!syncSelected && !connectSelected) return;
22
29
  const db = getSqliteDb();
23
- await runMigrations(db);
24
30
  registerAuthWorkspaceStore({
25
31
  id: SQLITE_PROVIDER_ID,
26
32
  order: 100,
27
33
  create: createSqliteAuthWorkspaceStore
28
34
  });
29
- registerSyncGatewayAdapter({
35
+ registerRateLimitProvider(SQLITE_PROVIDER_ID, sqliteRateLimitProvider);
36
+ if (syncSelected) {
37
+ registerSyncGatewayAdapter({
38
+ id: SQLITE_PROVIDER_ID,
39
+ order: 100,
40
+ create: createSqliteSyncGatewayAdapter
41
+ });
42
+ }
43
+ if (connectSelected) {
44
+ registerConnectStore({
45
+ id: SQLITE_PROVIDER_ID,
46
+ order: 100,
47
+ create: createSqliteConnectStore
48
+ });
49
+ }
50
+ registerWebhookStore({
30
51
  id: SQLITE_PROVIDER_ID,
31
52
  order: 100,
32
- create: createSqliteSyncGatewayAdapter
53
+ create: createSqliteWebhookStore
33
54
  });
34
55
  registerAdminStoreProvider({
35
56
  id: SQLITE_PROVIDER_ID,
@@ -45,4 +66,5 @@ export default defineNitroPlugin(async () => {
45
66
  })
46
67
  });
47
68
  registerProviderAdminAdapter(sqliteSyncAdminAdapter);
69
+ await runMigrations(db);
48
70
  });
@@ -0,0 +1,7 @@
1
+ import type { RateLimitConfig, RateLimitProvider, RateLimitResult, RateLimitStats } from '~~/server/utils/rate-limit/types';
2
+ export declare class SqliteRateLimitProvider implements RateLimitProvider {
3
+ readonly name = "sqlite";
4
+ checkAndRecord(key: string, config: RateLimitConfig): Promise<RateLimitResult>;
5
+ getStats(key: string, config: RateLimitConfig): Promise<RateLimitStats | null>;
6
+ }
7
+ export declare const sqliteRateLimitProvider: SqliteRateLimitProvider;
@@ -0,0 +1,70 @@
1
+ import { getRawDb } from "../db/kysely.js";
2
+ export class SqliteRateLimitProvider {
3
+ name = "sqlite";
4
+ async checkAndRecord(key, config) {
5
+ requireConfig(config);
6
+ const now = Date.now();
7
+ const db = getRawDb();
8
+ return db.transaction(() => {
9
+ const row = db.prepare(
10
+ `SELECT count, window_started_at, expires_at
11
+ FROM rate_limits WHERE key = ?`
12
+ ).get(key);
13
+ if (!row || row.expires_at <= now) {
14
+ db.prepare(
15
+ `INSERT INTO rate_limits (
16
+ key, count, window_started_at, expires_at
17
+ ) VALUES (?, 1, ?, ?)
18
+ ON CONFLICT(key) DO UPDATE SET
19
+ count = 1,
20
+ window_started_at = excluded.window_started_at,
21
+ expires_at = excluded.expires_at`
22
+ ).run(key, now, now + config.windowMs);
23
+ return {
24
+ allowed: true,
25
+ remaining: config.maxRequests - 1
26
+ };
27
+ }
28
+ if (row.count >= config.maxRequests) {
29
+ return {
30
+ allowed: false,
31
+ remaining: 0,
32
+ retryAfterMs: Math.max(0, row.expires_at - now)
33
+ };
34
+ }
35
+ db.prepare(
36
+ `UPDATE rate_limits SET count = count + 1 WHERE key = ?`
37
+ ).run(key);
38
+ return {
39
+ allowed: true,
40
+ remaining: config.maxRequests - row.count - 1
41
+ };
42
+ })();
43
+ }
44
+ async getStats(key, config) {
45
+ requireConfig(config);
46
+ const now = Date.now();
47
+ const row = getRawDb().prepare(
48
+ `SELECT count, window_started_at, expires_at
49
+ FROM rate_limits WHERE key = ?`
50
+ ).get(key);
51
+ if (!row || row.expires_at <= now) {
52
+ return {
53
+ limit: config.maxRequests,
54
+ remaining: config.maxRequests,
55
+ resetMs: config.windowMs
56
+ };
57
+ }
58
+ return {
59
+ limit: config.maxRequests,
60
+ remaining: Math.max(0, config.maxRequests - row.count),
61
+ resetMs: Math.max(0, row.expires_at - now)
62
+ };
63
+ }
64
+ }
65
+ export const sqliteRateLimitProvider = new SqliteRateLimitProvider();
66
+ function requireConfig(config) {
67
+ if (!Number.isSafeInteger(config.windowMs) || config.windowMs <= 0 || !Number.isSafeInteger(config.maxRequests) || config.maxRequests <= 0) {
68
+ throw new Error("Invalid SQLite rate-limit configuration");
69
+ }
70
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sync history retention is deliberately fail-closed.
3
+ *
4
+ * An operator cannot enable this with environment configuration. The policy
5
+ * may change only after snapshot-at-high-watermark bootstrap is implemented
6
+ * and verified for SQLite deployments.
7
+ */
8
+ export declare const SYNC_HISTORY_GC_POLICY: Readonly<{
9
+ enabled: true;
10
+ snapshotBootstrapVerified: true;
11
+ reason: "SQLite history GC is guarded by the verified snapshot-v1 retention contract.";
12
+ }>;
13
+ export declare function canRunSyncHistoryGc(): boolean;
@@ -0,0 +1,8 @@
1
+ export const SYNC_HISTORY_GC_POLICY = Object.freeze({
2
+ enabled: true,
3
+ snapshotBootstrapVerified: true,
4
+ reason: "SQLite history GC is guarded by the verified snapshot-v1 retention contract."
5
+ });
6
+ export function canRunSyncHistoryGc() {
7
+ return SYNC_HISTORY_GC_POLICY.enabled && SYNC_HISTORY_GC_POLICY.snapshotBootstrapVerified;
8
+ }
@@ -6,13 +6,25 @@
6
6
  * (BEGIN IMMEDIATE prevents concurrent server_version races).
7
7
  */
8
8
  import type { H3Event } from 'h3';
9
- import type { SyncGatewayAdapter } from '~~/server/sync/gateway/types';
10
- import type { PullRequest, PullResponse, PushBatch, PushResult } from '~~/shared/sync/types';
9
+ import type { CanonicalStorageQueryRequest, CanonicalStorageQueryResponse, SyncGatewayAdapter, UploadIntentConsumptionRequest, UploadIntentReservationRequest } from '~~/server/sync/gateway/types';
10
+ import type { PullRequest, PullResponse, PushBatch, PushResult, SnapshotRequest, SnapshotResponse } from '~~/shared/sync/types';
11
11
  export declare class SqliteSyncGatewayAdapter implements SyncGatewayAdapter {
12
12
  id: string;
13
+ readonly capabilities: {
14
+ readonly snapshotBootstrap: "snapshot-v1";
15
+ readonly historyRetention: "snapshot-v1";
16
+ };
13
17
  private get db();
18
+ reserveUploadIntent(event: H3Event, input: UploadIntentReservationRequest): Promise<void>;
19
+ consumeUploadIntent(event: H3Event, input: UploadIntentConsumptionRequest): Promise<void>;
20
+ cancelUploadIntent(event: H3Event, input: {
21
+ workspaceId: string;
22
+ intentId: string;
23
+ }): Promise<void>;
24
+ queryCanonicalStorage(event: H3Event, input: CanonicalStorageQueryRequest): Promise<CanonicalStorageQueryResponse>;
14
25
  push(event: H3Event, input: PushBatch): Promise<PushResult>;
15
26
  pull(event: H3Event, input: PullRequest): Promise<PullResponse>;
27
+ snapshot(event: H3Event, input: SnapshotRequest): Promise<SnapshotResponse>;
16
28
  updateCursor(event: H3Event, input: {
17
29
  scope: {
18
30
  workspaceId: string;