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
package/README.md CHANGED
@@ -5,7 +5,8 @@ SQLite sync and workspace store provider for OR3 Chat. Provides a lightweight, s
5
5
  ## What it provides
6
6
 
7
7
  - **AuthWorkspaceStore** (`sqlite`) — user identity mapping, workspace CRUD, role resolution
8
- - **SyncGatewayAdapter** (`sqlite`) — push/pull sync with LWW conflict resolution, cursor tracking, GC
8
+ - **SyncGatewayAdapter** (`sqlite`) — push/pull sync, consistent materialized snapshot pages, LWW conflict resolution, and cursor tracking
9
+ - **ConnectStore** (`sqlite`) — durable, atomic device enrollment and connected-computer records for OR3 Connect
9
10
 
10
11
  ## Install
11
12
 
@@ -39,6 +40,15 @@ OR3_SQLITE_PRAGMA_JOURNAL_MODE=WAL
39
40
  OR3_SQLITE_PRAGMA_SYNCHRONOUS=NORMAL
40
41
  ```
41
42
 
43
+ OR3 Connect uses this same database by default:
44
+
45
+ ```bash
46
+ OR3_CONNECT_ENABLED=true
47
+ OR3_CONNECT_PROVIDER=sqlite
48
+ ```
49
+
50
+ No Convex deployment or second database is required.
51
+
42
52
  ## How it works
43
53
 
44
54
  ### Registration
@@ -48,16 +58,20 @@ On server startup, the Nitro plugin:
48
58
  1. Initializes the SQLite database (creates file if needed)
49
59
  2. Runs schema migrations automatically
50
60
  3. Registers `AuthWorkspaceStore` with ID `sqlite`
51
- 4. Registers `SyncGatewayAdapter` with ID `sqlite`
61
+ 4. Registers `SyncGatewayAdapter` when SQLite sync is selected
62
+ 5. Registers `ConnectStore` when SQLite Connect persistence is selected
52
63
 
53
64
  Registration is skipped when `auth.enabled` is `false` (local-only mode).
54
65
 
55
66
  ### Schema
56
67
 
57
- Two migrations create all tables:
68
+ Ordered migrations create and evolve all tables:
58
69
 
59
70
  - **001_init**: `users`, `auth_accounts`, `workspaces`, `workspace_members`
60
71
  - **002_sync_tables**: `server_version_counter`, `change_log`, `device_cursors`, `tombstones`, plus materialized entity tables (`s_threads`, `s_messages`, etc.)
72
+ - **003–005**: workspace-scoped sync keys, invitations, and admin stores
73
+ - **006_sync_snapshots**: winning operation IDs plus immutable snapshot headers/items
74
+ - **009_or3_connect**: single-use device authorizations and connected computers
61
75
 
62
76
  All tables use snake_case aligned with the sync wire format.
63
77
 
@@ -65,8 +79,9 @@ All tables use snake_case aligned with the sync wire format.
65
79
 
66
80
  - **Push**: validates ops → checks `op_id` idempotency → allocates contiguous `server_version` block → applies LWW to materialized tables → writes change_log → upserts tombstones for deletes
67
81
  - **Pull**: returns ordered changes for `server_version > cursor` with limit/pagination and optional table filtering
82
+ - **Snapshot**: captures canonical live rows and current tombstones with one `highWatermark` under `BEGIN IMMEDIATE`, then serves immutable, keyset-paginated pages ordered by `(tableName, pk, kind)`
68
83
  - **Cursor**: forward-only per-device cursor tracking
69
- - **GC**: tombstone and change_log cleanup respects min device cursor + retention window
84
+ - **Retention safety**: tombstone and `change_log` GC is enabled only under the explicit `snapshot-v1` capability and deletes old revisions acknowledged by every registered device
70
85
 
71
86
  LWW conflict resolution: incoming wins when `clock` is higher, or when clocks are equal and `hlc` is lexicographically greater.
72
87
 
@@ -100,6 +115,7 @@ bun run build # build for distribution
100
115
 
101
116
  - Works with multiple auth providers (`basic-auth`, `clerk`, or custom)
102
117
  - Replaces `or3-provider-convex` for sync + workspace store functionality
118
+ - Provides OR3 Connect persistence without Convex
103
119
  - Does NOT provide storage — pair with `or3-provider-fs` for file storage
104
120
 
105
121
  ### Known differences vs Convex
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
3
  "configKey": "or3-provider-sqlite",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,14 +1,6 @@
1
1
  import { createError } from "h3";
2
- import { createSqliteSyncGatewayAdapter } from "../../sync/sqlite-sync-gateway-adapter.js";
2
+ import { SYNC_HISTORY_GC_POLICY } from "../../sync/history-gc-policy.js";
3
3
  const SQLITE_PROVIDER_ID = "sqlite";
4
- const DEFAULT_RETENTION_SECONDS = 30 * 24 * 3600;
5
- function resolveRetentionSeconds(payload) {
6
- const days = typeof payload?.retentionDays === "number" ? payload.retentionDays : null;
7
- const seconds = typeof payload?.retentionSeconds === "number" ? payload.retentionSeconds : null;
8
- if (seconds && Number.isFinite(seconds) && seconds > 0) return Math.floor(seconds);
9
- if (days && Number.isFinite(days) && days > 0) return Math.floor(days * 24 * 3600);
10
- return DEFAULT_RETENTION_SECONDS;
11
- }
12
4
  export const sqliteSyncAdminAdapter = {
13
5
  id: SQLITE_PROVIDER_ID,
14
6
  kind: "sync",
@@ -26,6 +18,10 @@ export const sqliteSyncAdminAdapter = {
26
18
  message: "SQLite is configured with :memory:. Data will not persist across restarts."
27
19
  });
28
20
  }
21
+ warnings.push({
22
+ level: "warning",
23
+ message: SYNC_HISTORY_GC_POLICY.reason
24
+ });
29
25
  return {
30
26
  details: {
31
27
  dbPath: dbPath ?? ":memory:",
@@ -33,39 +29,21 @@ export const sqliteSyncAdminAdapter = {
33
29
  synchronous: process.env.OR3_SQLITE_PRAGMA_SYNCHRONOUS ?? "NORMAL"
34
30
  },
35
31
  warnings,
36
- actions: [
37
- {
38
- id: "sync.gc-change-log",
39
- label: "Run Sync Change Log GC",
40
- description: "Purge old change_log entries after retention and device cursor checks.",
41
- danger: true
42
- },
43
- {
44
- id: "sync.gc-tombstones",
45
- label: "Run Sync Tombstone GC",
46
- description: "Purge old tombstones after retention and device cursor checks.",
47
- danger: true
48
- }
49
- ]
32
+ actions: []
50
33
  };
51
34
  },
52
- async runAction(event, actionId, payload, ctx) {
35
+ async runAction(_event, actionId, _payload, ctx) {
53
36
  if (!ctx.session.workspace?.id) {
54
37
  throw createError({
55
38
  statusCode: 400,
56
39
  statusMessage: "Workspace not resolved"
57
40
  });
58
41
  }
59
- const adapter = createSqliteSyncGatewayAdapter();
60
- const retentionSeconds = resolveRetentionSeconds(payload);
61
- const scope = { workspaceId: ctx.session.workspace.id };
62
- if (actionId === "sync.gc-change-log") {
63
- await adapter.gcChangeLog?.(event, { scope, retentionSeconds });
64
- return { ok: true, action: actionId, retentionSeconds };
65
- }
66
- if (actionId === "sync.gc-tombstones") {
67
- await adapter.gcTombstones?.(event, { scope, retentionSeconds });
68
- return { ok: true, action: actionId, retentionSeconds };
42
+ if (actionId === "sync.gc-change-log" || actionId === "sync.gc-tombstones") {
43
+ throw createError({
44
+ statusCode: 503,
45
+ statusMessage: SYNC_HISTORY_GC_POLICY.reason
46
+ });
69
47
  }
70
48
  throw createError({ statusCode: 400, statusMessage: "Unknown action" });
71
49
  }
@@ -79,25 +79,80 @@ class SqliteWorkspaceAccessStore {
79
79
  ).execute();
80
80
  }
81
81
  }
82
+ const existingMembership = await this.db.selectFrom("workspace_members").select("role").where("workspace_id", "=", input.workspaceId).where("user_id", "=", userId).executeTakeFirst();
83
+ if (existingMembership) {
84
+ await this.setMemberRole({
85
+ workspaceId: input.workspaceId,
86
+ userId,
87
+ role: input.role
88
+ });
89
+ return;
90
+ }
82
91
  await this.db.insertInto("workspace_members").values({
83
92
  id: randomUUID(),
84
93
  workspace_id: input.workspaceId,
85
94
  user_id: userId,
86
95
  role: input.role,
87
96
  created_at: now
88
- }).onConflict(
89
- (oc) => oc.columns(["workspace_id", "user_id"]).doUpdateSet({ role: input.role })
90
- ).execute();
97
+ }).execute();
91
98
  }
92
99
  async setMemberRole(input) {
93
- const result = await this.db.updateTable("workspace_members").set({ role: input.role }).where("workspace_id", "=", input.workspaceId).where("user_id", "=", input.userId).executeTakeFirst();
94
- const updated = Number(result.numUpdatedRows ?? 0);
95
- if (updated === 0) {
96
- throw new Error("Workspace member not found");
97
- }
100
+ await this.db.transaction().execute(async (tx) => {
101
+ const member = await tx.selectFrom("workspace_members").select("role").where("workspace_id", "=", input.workspaceId).where("user_id", "=", input.userId).executeTakeFirst();
102
+ if (!member) {
103
+ throw new Error("Workspace member not found");
104
+ }
105
+ if (member.role === "owner" && input.role !== "owner") {
106
+ const owners = await tx.selectFrom("workspace_members").select("user_id").where("workspace_id", "=", input.workspaceId).where("role", "=", "owner").execute();
107
+ if (owners.length <= 1) {
108
+ throw new Error(
109
+ "Cannot demote the last workspace owner. Transfer ownership first."
110
+ );
111
+ }
112
+ const workspace = await tx.selectFrom("workspaces").select("owner_user_id").where("id", "=", input.workspaceId).executeTakeFirst();
113
+ if (workspace?.owner_user_id === input.userId) {
114
+ const replacement = owners.find(
115
+ (owner) => owner.user_id !== input.userId
116
+ );
117
+ if (!replacement) {
118
+ throw new Error(
119
+ "Cannot demote the last workspace owner. Transfer ownership first."
120
+ );
121
+ }
122
+ await tx.updateTable("workspaces").set({ owner_user_id: replacement.user_id }).where("id", "=", input.workspaceId).execute();
123
+ }
124
+ }
125
+ await tx.updateTable("workspace_members").set({ role: input.role }).where("workspace_id", "=", input.workspaceId).where("user_id", "=", input.userId).execute();
126
+ });
98
127
  }
99
128
  async removeMember(input) {
100
- await this.db.deleteFrom("workspace_members").where("workspace_id", "=", input.workspaceId).where("user_id", "=", input.userId).execute();
129
+ await this.db.transaction().execute(async (tx) => {
130
+ const member = await tx.selectFrom("workspace_members").select("role").where("workspace_id", "=", input.workspaceId).where("user_id", "=", input.userId).executeTakeFirst();
131
+ if (!member) {
132
+ throw new Error("Workspace member not found");
133
+ }
134
+ if (member.role === "owner") {
135
+ const owners = await tx.selectFrom("workspace_members").select("user_id").where("workspace_id", "=", input.workspaceId).where("role", "=", "owner").execute();
136
+ if (owners.length <= 1) {
137
+ throw new Error(
138
+ "Cannot remove the last workspace owner. Transfer ownership first."
139
+ );
140
+ }
141
+ const workspace = await tx.selectFrom("workspaces").select("owner_user_id").where("id", "=", input.workspaceId).executeTakeFirst();
142
+ if (workspace?.owner_user_id === input.userId) {
143
+ const replacement = owners.find(
144
+ (owner) => owner.user_id !== input.userId
145
+ );
146
+ if (!replacement) {
147
+ throw new Error(
148
+ "Cannot remove the last workspace owner. Transfer ownership first."
149
+ );
150
+ }
151
+ await tx.updateTable("workspaces").set({ owner_user_id: replacement.user_id }).where("id", "=", input.workspaceId).execute();
152
+ }
153
+ }
154
+ await tx.deleteFrom("workspace_members").where("workspace_id", "=", input.workspaceId).where("user_id", "=", input.userId).execute();
155
+ });
101
156
  }
102
157
  async listWorkspaces(input) {
103
158
  const includeDeleted = input.includeDeleted === true;
@@ -4,10 +4,11 @@
4
4
  * Maps provider identity -> internal user and manages workspace CRUD.
5
5
  * Uses Kysely for all queries. No Convex or Clerk SDK imports.
6
6
  */
7
- import type { AuthWorkspaceStore } from '~~/server/auth/store/types';
7
+ import type { AuthWorkspaceStore, InviteProvisionResult, InviteValidationResult } from '~~/server/auth/store/types';
8
8
  import type { WorkspaceRole } from '~~/app/core/hooks/hook-types';
9
9
  export declare class SqliteAuthWorkspaceStore implements AuthWorkspaceStore {
10
10
  private get db();
11
+ protected onInviteProvisionStep(_step: 'invite_validated' | 'user_created' | 'membership_created' | 'invite_consumed'): void;
11
12
  getOrCreateUser(input: {
12
13
  provider: string;
13
14
  providerUserId: string;
@@ -27,6 +28,7 @@ export declare class SqliteAuthWorkspaceStore implements AuthWorkspaceStore {
27
28
  getOrCreateDefaultWorkspace(userId: string): Promise<{
28
29
  workspaceId: string;
29
30
  workspaceName: string;
31
+ created?: boolean;
30
32
  }>;
31
33
  getWorkspaceRole(input: {
32
34
  userId: string;
@@ -95,6 +97,19 @@ export declare class SqliteAuthWorkspaceStore implements AuthWorkspaceStore {
95
97
  inviteId: string;
96
98
  revokedByUserId: string;
97
99
  }): Promise<void>;
100
+ validateInvite(input: {
101
+ workspaceId: string;
102
+ email: string;
103
+ tokenHash: string;
104
+ }): Promise<InviteValidationResult>;
105
+ acceptInviteAndProvisionUser(input: {
106
+ provider: string;
107
+ providerUserId: string;
108
+ email: string;
109
+ displayName?: string;
110
+ workspaceId: string;
111
+ tokenHash: string;
112
+ }): Promise<InviteProvisionResult>;
98
113
  consumeInvite(input: {
99
114
  workspaceId: string;
100
115
  email: string;
@@ -13,6 +13,8 @@ export class SqliteAuthWorkspaceStore {
13
13
  get db() {
14
14
  return getSqliteDb();
15
15
  }
16
+ onInviteProvisionStep(_step) {
17
+ }
16
18
  async getOrCreateUser(input) {
17
19
  this.db;
18
20
  const raw = getRawDb();
@@ -94,7 +96,8 @@ export class SqliteAuthWorkspaceStore {
94
96
  if (activeMembership) {
95
97
  return {
96
98
  workspaceId: activeMembership.id,
97
- workspaceName: activeMembership.name
99
+ workspaceName: activeMembership.name,
100
+ created: false
98
101
  };
99
102
  }
100
103
  const membership = await db.selectFrom("workspace_members").innerJoin("workspaces", "workspaces.id", "workspace_members.workspace_id").select([
@@ -103,7 +106,11 @@ export class SqliteAuthWorkspaceStore {
103
106
  ]).where("workspace_members.user_id", "=", userId).where("workspaces.deleted", "=", 0).orderBy("workspace_members.created_at", "asc").executeTakeFirst();
104
107
  if (membership) {
105
108
  await db.updateTable("users").set({ active_workspace_id: membership.id }).where("id", "=", userId).execute();
106
- return { workspaceId: membership.id, workspaceName: membership.name };
109
+ return {
110
+ workspaceId: membership.id,
111
+ workspaceName: membership.name,
112
+ created: false
113
+ };
107
114
  }
108
115
  const workspaceId = uid();
109
116
  const memberId = uid();
@@ -128,7 +135,7 @@ export class SqliteAuthWorkspaceStore {
128
135
  }).execute();
129
136
  await tx.updateTable("users").set({ active_workspace_id: workspaceId }).where("id", "=", userId).execute();
130
137
  });
131
- return { workspaceId, workspaceName: name };
138
+ return { workspaceId, workspaceName: name, created: true };
132
139
  }
133
140
  async getWorkspaceRole(input) {
134
141
  const db = this.db;
@@ -277,6 +284,110 @@ export class SqliteAuthWorkspaceStore {
277
284
  }).where("workspace_id", "=", input.workspaceId).where("id", "=", input.inviteId).execute();
278
285
  void input.revokedByUserId;
279
286
  }
287
+ async validateInvite(input) {
288
+ this.db;
289
+ const raw = getRawDb();
290
+ const now = nowEpoch();
291
+ const invite = raw.prepare(
292
+ `SELECT role, status, email, expires_at
293
+ FROM auth_invites
294
+ WHERE workspace_id = ? AND token_hash = ?
295
+ LIMIT 1`
296
+ ).get(input.workspaceId, input.tokenHash);
297
+ if (!invite) return { ok: false, reason: "not_found" };
298
+ if (invite.email !== normalizeEmail(input.email)) {
299
+ return { ok: false, reason: "email_mismatch" };
300
+ }
301
+ if (invite.status === "accepted") return { ok: false, reason: "already_used" };
302
+ if (invite.status === "revoked") return { ok: false, reason: "revoked" };
303
+ if (invite.status === "expired" || invite.expires_at <= now) {
304
+ return { ok: false, reason: "expired" };
305
+ }
306
+ return { ok: true, role: invite.role };
307
+ }
308
+ async acceptInviteAndProvisionUser(input) {
309
+ this.db;
310
+ const raw = getRawDb();
311
+ const now = nowEpoch();
312
+ const normalizedEmail = normalizeEmail(input.email);
313
+ return raw.transaction(() => {
314
+ const invite = raw.prepare(
315
+ `SELECT id, role, status, email, expires_at
316
+ FROM auth_invites
317
+ WHERE workspace_id = ? AND token_hash = ?
318
+ LIMIT 1`
319
+ ).get(input.workspaceId, input.tokenHash);
320
+ if (!invite) return { ok: false, reason: "not_found" };
321
+ if (invite.email !== normalizedEmail) {
322
+ return { ok: false, reason: "email_mismatch" };
323
+ }
324
+ if (invite.status === "accepted") {
325
+ return { ok: false, reason: "already_used" };
326
+ }
327
+ if (invite.status === "revoked") {
328
+ return { ok: false, reason: "revoked" };
329
+ }
330
+ if (invite.status === "expired" || invite.expires_at <= now) {
331
+ return { ok: false, reason: "expired" };
332
+ }
333
+ this.onInviteProvisionStep("invite_validated");
334
+ const existingAccount = raw.prepare(
335
+ `SELECT user_id
336
+ FROM auth_accounts
337
+ WHERE provider = ? AND provider_user_id = ?
338
+ LIMIT 1`
339
+ ).get(input.provider, input.providerUserId);
340
+ const userId = existingAccount?.user_id ?? uid();
341
+ const createdUser = !existingAccount;
342
+ if (createdUser) {
343
+ raw.prepare(
344
+ `INSERT INTO users (id, email, display_name, active_workspace_id, created_at)
345
+ VALUES (?, ?, ?, NULL, ?)`
346
+ ).run(userId, normalizedEmail, input.displayName ?? null, now);
347
+ raw.prepare(
348
+ `INSERT INTO auth_accounts (id, user_id, provider, provider_user_id, created_at)
349
+ VALUES (?, ?, ?, ?, ?)`
350
+ ).run(uid(), userId, input.provider, input.providerUserId, now);
351
+ }
352
+ this.onInviteProvisionStep("user_created");
353
+ const existingMembership = raw.prepare(
354
+ `SELECT id FROM workspace_members
355
+ WHERE workspace_id = ? AND user_id = ?
356
+ LIMIT 1`
357
+ ).get(input.workspaceId, userId);
358
+ if (existingMembership) {
359
+ raw.prepare("UPDATE workspace_members SET role = ? WHERE id = ?").run(
360
+ invite.role,
361
+ existingMembership.id
362
+ );
363
+ } else {
364
+ raw.prepare(
365
+ `INSERT INTO workspace_members (id, workspace_id, user_id, role, created_at)
366
+ VALUES (?, ?, ?, ?, ?)`
367
+ ).run(uid(), input.workspaceId, userId, invite.role, now);
368
+ }
369
+ raw.prepare("UPDATE users SET active_workspace_id = ? WHERE id = ?").run(
370
+ input.workspaceId,
371
+ userId
372
+ );
373
+ this.onInviteProvisionStep("membership_created");
374
+ const consumed = raw.prepare(
375
+ `UPDATE auth_invites
376
+ SET status = 'accepted', accepted_at = ?, accepted_user_id = ?, updated_at = ?
377
+ WHERE id = ? AND status = 'pending'`
378
+ ).run(now, userId, now, invite.id);
379
+ if (consumed.changes !== 1) {
380
+ throw new Error("Invite state changed during atomic acceptance");
381
+ }
382
+ this.onInviteProvisionStep("invite_consumed");
383
+ return {
384
+ ok: true,
385
+ userId,
386
+ role: invite.role,
387
+ createdUser
388
+ };
389
+ }).immediate();
390
+ }
280
391
  async consumeInvite(input) {
281
392
  this.db;
282
393
  const raw = getRawDb();
@@ -0,0 +1,2 @@
1
+ import { type ConnectStore } from '~~/server/connect/store/types';
2
+ export declare function createSqliteConnectStore(): ConnectStore;