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
@@ -4,18 +4,78 @@ import * as m002 from "./migrations/002_sync_tables.js";
4
4
  import * as m003 from "./migrations/003_sync_hardening.js";
5
5
  import * as m004 from "./migrations/004_auth_invites.js";
6
6
  import * as m005 from "./migrations/005_admin_stores.js";
7
+ import * as m006 from "./migrations/006_sync_snapshots.js";
8
+ import * as m007 from "./migrations/007_device_cursor_ownership.js";
9
+ import * as m008 from "./migrations/008_upload_intents.js";
10
+ import * as m009 from "./migrations/009_or3_connect.js";
11
+ import * as m010 from "./migrations/010_connect_user_code_hmac.js";
12
+ import * as m011 from "./migrations/011_connect_environment_scope_indexes.js";
13
+ import * as m012 from "./migrations/012_connect_credential_redelivery.js";
14
+ import * as m013 from "./migrations/013_connect_environment_lifecycle.js";
15
+ import * as m014 from "./migrations/014_connect_retention_activation.js";
16
+ import * as m015 from "./migrations/015_rate_limits.js";
17
+ const REQUIRED_SCHEMA_TABLES = [
18
+ "users",
19
+ "auth_accounts",
20
+ "workspaces",
21
+ "workspace_members",
22
+ "server_version_counter",
23
+ "change_log",
24
+ "device_cursors",
25
+ "tombstones",
26
+ "s_threads",
27
+ "s_messages",
28
+ "s_projects",
29
+ "s_posts",
30
+ "s_kv",
31
+ "s_file_meta",
32
+ "s_notifications",
33
+ "auth_invites",
34
+ "admin_users",
35
+ "admin_workspace_settings",
36
+ "sync_snapshots",
37
+ "sync_snapshot_items",
38
+ "upload_intents",
39
+ "connect_device_authorizations",
40
+ "connect_environments",
41
+ "rate_limits"
42
+ ];
7
43
  const migrations = {
8
44
  "001_init": m001,
9
45
  "002_sync_tables": m002,
10
46
  "003_sync_hardening": m003,
11
47
  "004_auth_invites": m004,
12
- "005_admin_stores": m005
48
+ "005_admin_stores": m005,
49
+ "006_sync_snapshots": m006,
50
+ "007_device_cursor_ownership": m007,
51
+ "008_upload_intents": m008,
52
+ "009_or3_connect": m009,
53
+ "010_connect_user_code_hmac": m010,
54
+ "011_connect_environment_scope_indexes": m011,
55
+ "012_connect_credential_redelivery": m012,
56
+ "013_connect_environment_lifecycle": m013,
57
+ "014_connect_retention_activation": m014,
58
+ "015_rate_limits": m015
13
59
  };
14
60
  class StaticMigrationProvider {
15
61
  async getMigrations() {
16
62
  return migrations;
17
63
  }
18
64
  }
65
+ async function assertLatestSchemaIntegrity(db) {
66
+ const tables = await db.introspection.getTables({
67
+ withInternalKyselyTables: true
68
+ });
69
+ const tableNames = new Set(tables.map((table) => table.name));
70
+ const missing = REQUIRED_SCHEMA_TABLES.filter(
71
+ (tableName) => !tableNames.has(tableName)
72
+ );
73
+ if (missing.length > 0) {
74
+ throw new Error(
75
+ `SQLite schema integrity check failed; required tables are missing: ${missing.join(", ")}`
76
+ );
77
+ }
78
+ }
19
79
  export async function runMigrations(db) {
20
80
  const migrator = new Migrator({
21
81
  db,
@@ -35,4 +95,5 @@ export async function runMigrations(db) {
35
95
  console.error("[or3-sqlite] migration error:", error);
36
96
  throw error;
37
97
  }
98
+ await assertLatestSchemaIntegrity(db);
38
99
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Migration 006: Durable, consistent sync snapshot pages.
3
+ *
4
+ * - Stores the winning operation ID on materialized rows and tombstones.
5
+ * - Adds immutable snapshot headers/items for pagination across requests.
6
+ * - Backfills deterministic operation IDs for existing materialized state.
7
+ */
8
+ import type { Kysely } from 'kysely';
9
+ export declare function up(db: Kysely<unknown>): Promise<void>;
10
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,106 @@
1
+ import { sql } from "kysely";
2
+ const SYNCED_TABLES = [
3
+ ["s_threads", "threads"],
4
+ ["s_messages", "messages"],
5
+ ["s_projects", "projects"],
6
+ ["s_posts", "posts"],
7
+ ["s_kv", "kv"],
8
+ ["s_file_meta", "file_meta"],
9
+ ["s_notifications", "notifications"]
10
+ ];
11
+ export async function up(db) {
12
+ for (const [materializedTable, tableName] of SYNCED_TABLES) {
13
+ await sql.raw(
14
+ `ALTER TABLE "${materializedTable}" ADD COLUMN op_id TEXT NOT NULL DEFAULT ''`
15
+ ).execute(db);
16
+ await sql.raw(`
17
+ UPDATE "${materializedTable}" AS materialized
18
+ SET op_id = COALESCE(
19
+ (
20
+ SELECT change.op_id
21
+ FROM change_log AS change
22
+ WHERE change.workspace_id = materialized.workspace_id
23
+ AND change.table_name = '${tableName}'
24
+ AND change.pk = materialized.id
25
+ AND change.op = CASE WHEN materialized.deleted <> 0 THEN 'delete' ELSE 'put' END
26
+ AND change.clock = materialized.clock
27
+ AND change.hlc = materialized.hlc
28
+ ORDER BY change.server_version DESC
29
+ LIMIT 1
30
+ ),
31
+ 'legacy:${tableName}:' || materialized.id || ':' || materialized.clock || ':' || materialized.hlc
32
+ )
33
+ WHERE materialized.op_id = ''
34
+ `).execute(db);
35
+ }
36
+ await sql.raw(
37
+ `ALTER TABLE tombstones ADD COLUMN hlc TEXT NOT NULL DEFAULT ''`
38
+ ).execute(db);
39
+ await sql.raw(
40
+ `ALTER TABLE tombstones ADD COLUMN op_id TEXT NOT NULL DEFAULT ''`
41
+ ).execute(db);
42
+ await sql.raw(`
43
+ UPDATE tombstones AS tombstone
44
+ SET
45
+ hlc = COALESCE(
46
+ (
47
+ SELECT change.hlc
48
+ FROM change_log AS change
49
+ WHERE change.workspace_id = tombstone.workspace_id
50
+ AND change.table_name = tombstone.table_name
51
+ AND change.pk = tombstone.pk
52
+ AND change.op = 'delete'
53
+ AND change.server_version = tombstone.server_version
54
+ LIMIT 1
55
+ ),
56
+ CASE WHEN tombstone.hlc <> '' THEN tombstone.hlc ELSE 'legacy:0' END
57
+ ),
58
+ op_id = COALESCE(
59
+ (
60
+ SELECT change.op_id
61
+ FROM change_log AS change
62
+ WHERE change.workspace_id = tombstone.workspace_id
63
+ AND change.table_name = tombstone.table_name
64
+ AND change.pk = tombstone.pk
65
+ AND change.op = 'delete'
66
+ AND change.server_version = tombstone.server_version
67
+ LIMIT 1
68
+ ),
69
+ 'legacy:tombstone:' || tombstone.table_name || ':' || tombstone.pk || ':' || tombstone.clock
70
+ )
71
+ WHERE tombstone.hlc = '' OR tombstone.op_id = ''
72
+ `).execute(db);
73
+ await sql.raw(`
74
+ CREATE TABLE sync_snapshots (
75
+ id TEXT PRIMARY KEY,
76
+ workspace_id TEXT NOT NULL,
77
+ high_watermark INTEGER NOT NULL,
78
+ tables_json TEXT NOT NULL,
79
+ created_at INTEGER NOT NULL,
80
+ expires_at INTEGER NOT NULL
81
+ )
82
+ `).execute(db);
83
+ await sql.raw(`
84
+ CREATE INDEX idx_sync_snapshots_expires
85
+ ON sync_snapshots(expires_at)
86
+ `).execute(db);
87
+ await sql.raw(`
88
+ CREATE TABLE sync_snapshot_items (
89
+ snapshot_id TEXT NOT NULL,
90
+ table_name TEXT NOT NULL,
91
+ pk TEXT NOT NULL,
92
+ kind TEXT NOT NULL CHECK(kind IN ('row', 'tombstone')),
93
+ payload_json TEXT,
94
+ clock INTEGER NOT NULL,
95
+ hlc TEXT NOT NULL,
96
+ op_id TEXT NOT NULL,
97
+ server_deleted_at INTEGER,
98
+ PRIMARY KEY (snapshot_id, table_name, pk, kind),
99
+ FOREIGN KEY (snapshot_id) REFERENCES sync_snapshots(id) ON DELETE CASCADE
100
+ ) WITHOUT ROWID
101
+ `).execute(db);
102
+ }
103
+ export async function down(db) {
104
+ await db.schema.dropTable("sync_snapshot_items").ifExists().execute();
105
+ await db.schema.dropTable("sync_snapshots").ifExists().execute();
106
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Migration 007: bind device cursors to the authenticated user that created
3
+ * them. Legacy rows remain unbound until their next authenticated update.
4
+ */
5
+ import type { Kysely } from 'kysely';
6
+ export declare function up(db: Kysely<unknown>): Promise<void>;
7
+ export declare function down(_db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,5 @@
1
+ export async function up(db) {
2
+ await db.schema.alterTable("device_cursors").addColumn("owner_user_id", "text").execute();
3
+ }
4
+ export async function down(_db) {
5
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Persisted upload intents are both one-time authorizations and quota reservations. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ CREATE TABLE upload_intents (
5
+ id TEXT PRIMARY KEY,
6
+ workspace_id TEXT NOT NULL,
7
+ hash TEXT NOT NULL,
8
+ mime_type TEXT NOT NULL,
9
+ size_bytes INTEGER NOT NULL,
10
+ reserved_bytes INTEGER NOT NULL,
11
+ expires_at INTEGER NOT NULL,
12
+ status TEXT NOT NULL CHECK (status IN ('active', 'consumed', 'cancelled', 'expired')),
13
+ storage_id TEXT,
14
+ created_at INTEGER NOT NULL,
15
+ consumed_at INTEGER,
16
+ cancelled_at INTEGER
17
+ )
18
+ `.execute(db);
19
+ await sql`CREATE INDEX upload_intents_workspace_status_expiry
20
+ ON upload_intents(workspace_id, status, expires_at, id)`.execute(db);
21
+ await sql`CREATE INDEX upload_intents_workspace_hash_status
22
+ ON upload_intents(workspace_id, hash, status)`.execute(db);
23
+ }
24
+ export async function down(db) {
25
+ await db.schema.dropTable("upload_intents").ifExists().execute();
26
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Durable device authorization and enrolled-computer records for OR3 Connect. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ CREATE TABLE connect_device_authorizations (
5
+ id TEXT PRIMARY KEY,
6
+ device_code_hash TEXT NOT NULL UNIQUE,
7
+ user_code_hash TEXT NOT NULL,
8
+ status TEXT NOT NULL CHECK (
9
+ status IN (
10
+ 'pending', 'provisioning', 'approved', 'delivering',
11
+ 'denied', 'consumed', 'expired'
12
+ )
13
+ ),
14
+ host_json TEXT NOT NULL,
15
+ approved_user_id TEXT,
16
+ approved_workspace_id TEXT,
17
+ environment_id TEXT,
18
+ credential_ciphertext TEXT,
19
+ credential_delivery_started_at INTEGER,
20
+ credential_redeliver_until INTEGER,
21
+ expires_at INTEGER NOT NULL,
22
+ created_at INTEGER NOT NULL,
23
+ updated_at INTEGER NOT NULL
24
+ )
25
+ `.execute(db);
26
+ await sql`
27
+ CREATE INDEX connect_authorizations_status_expiry
28
+ ON connect_device_authorizations(status, expires_at)
29
+ `.execute(db);
30
+ await sql`
31
+ CREATE INDEX connect_authorizations_user_code
32
+ ON connect_device_authorizations(user_code_hash, created_at)
33
+ `.execute(db);
34
+ await sql`
35
+ CREATE INDEX connect_authorizations_environment
36
+ ON connect_device_authorizations(environment_id)
37
+ `.execute(db);
38
+ await sql`
39
+ CREATE TABLE connect_environments (
40
+ id TEXT PRIMARY KEY,
41
+ user_id TEXT NOT NULL,
42
+ workspace_id TEXT NOT NULL,
43
+ name TEXT NOT NULL,
44
+ platform TEXT NOT NULL,
45
+ architecture TEXT NOT NULL,
46
+ host_id TEXT,
47
+ signing_public_key TEXT,
48
+ noise_public_key TEXT,
49
+ authorization_id TEXT,
50
+ hostname TEXT NOT NULL,
51
+ tunnel_id TEXT NOT NULL,
52
+ dns_record_id TEXT NOT NULL,
53
+ control_token_hash TEXT NOT NULL UNIQUE,
54
+ access_credential_ciphertext TEXT NOT NULL,
55
+ tunnel_secret_ciphertext TEXT,
56
+ status TEXT NOT NULL CHECK (
57
+ status IN (
58
+ 'provisioning', 'active', 'revoking', 'revoked', 'error'
59
+ )
60
+ ),
61
+ lifecycle_attempts INTEGER NOT NULL DEFAULT 0,
62
+ lifecycle_next_attempt_at INTEGER NOT NULL DEFAULT 0,
63
+ lifecycle_claim_token TEXT,
64
+ lifecycle_claimed_until INTEGER,
65
+ provisioning_deadline_at INTEGER,
66
+ lifecycle_error TEXT,
67
+ last_seen_at INTEGER,
68
+ created_at INTEGER NOT NULL,
69
+ updated_at INTEGER NOT NULL,
70
+ revoked_at INTEGER
71
+ )
72
+ `.execute(db);
73
+ await sql`
74
+ CREATE INDEX connect_environments_user_status
75
+ ON connect_environments(user_id, status, updated_at)
76
+ `.execute(db);
77
+ await sql`
78
+ CREATE INDEX connect_environments_workspace_status
79
+ ON connect_environments(workspace_id, status, updated_at)
80
+ `.execute(db);
81
+ await sql`
82
+ CREATE INDEX connect_environments_lifecycle_due
83
+ ON connect_environments(
84
+ status, lifecycle_next_attempt_at, lifecycle_claimed_until
85
+ )
86
+ `.execute(db);
87
+ }
88
+ export async function down(db) {
89
+ await db.schema.dropTable("connect_environments").ifExists().execute();
90
+ await db.schema.dropTable("connect_device_authorizations").ifExists().execute();
91
+ }
@@ -0,0 +1,10 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Purges legacy readable pairing phrases.
4
+ *
5
+ * Device authorizations are ten-minute, retryable setup records. Their former
6
+ * unkeyed hashes cannot be upgraded to the server-keyed HMAC without retaining
7
+ * the plaintext, so an upgrade deliberately invalidates in-flight records.
8
+ */
9
+ export declare function up(db: Kysely<unknown>): Promise<void>;
10
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,97 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ const columns = await sql`
4
+ PRAGMA table_info(connect_device_authorizations)
5
+ `.execute(db);
6
+ if (!columns.rows.some((column) => column.name === "user_code_display")) {
7
+ return;
8
+ }
9
+ await sql`DROP INDEX IF EXISTS connect_authorizations_status_expiry`.execute(db);
10
+ await sql`DROP INDEX IF EXISTS connect_authorizations_user_code`.execute(db);
11
+ await sql`DROP INDEX IF EXISTS connect_authorizations_environment`.execute(db);
12
+ await sql`
13
+ ALTER TABLE connect_device_authorizations
14
+ RENAME TO connect_device_authorizations_legacy
15
+ `.execute(db);
16
+ await createSecureAuthorizationTable(db);
17
+ await sql`DROP TABLE connect_device_authorizations_legacy`.execute(db);
18
+ }
19
+ export async function down(db) {
20
+ const columns = await sql`
21
+ PRAGMA table_info(connect_device_authorizations)
22
+ `.execute(db);
23
+ if (columns.rows.some((column) => column.name === "user_code_display")) {
24
+ return;
25
+ }
26
+ await sql`DROP INDEX IF EXISTS connect_authorizations_status_expiry`.execute(db);
27
+ await sql`DROP INDEX IF EXISTS connect_authorizations_user_code`.execute(db);
28
+ await sql`DROP INDEX IF EXISTS connect_authorizations_environment`.execute(db);
29
+ await sql`
30
+ ALTER TABLE connect_device_authorizations
31
+ RENAME TO connect_device_authorizations_secure
32
+ `.execute(db);
33
+ await sql`
34
+ CREATE TABLE connect_device_authorizations (
35
+ id TEXT PRIMARY KEY,
36
+ device_code_hash TEXT NOT NULL UNIQUE,
37
+ user_code_hash TEXT NOT NULL,
38
+ user_code_display TEXT NOT NULL,
39
+ status TEXT NOT NULL CHECK (
40
+ status IN (
41
+ 'pending', 'provisioning', 'approved',
42
+ 'denied', 'consumed', 'expired'
43
+ )
44
+ ),
45
+ host_json TEXT NOT NULL,
46
+ approved_user_id TEXT,
47
+ approved_workspace_id TEXT,
48
+ environment_id TEXT,
49
+ credential_ciphertext TEXT,
50
+ expires_at INTEGER NOT NULL,
51
+ created_at INTEGER NOT NULL,
52
+ updated_at INTEGER NOT NULL
53
+ )
54
+ `.execute(db);
55
+ await createAuthorizationIndexes(db);
56
+ await sql`DROP TABLE connect_device_authorizations_secure`.execute(db);
57
+ }
58
+ async function createSecureAuthorizationTable(db) {
59
+ await sql`
60
+ CREATE TABLE connect_device_authorizations (
61
+ id TEXT PRIMARY KEY,
62
+ device_code_hash TEXT NOT NULL UNIQUE,
63
+ user_code_hash TEXT NOT NULL,
64
+ status TEXT NOT NULL CHECK (
65
+ status IN (
66
+ 'pending', 'provisioning', 'approved', 'delivering',
67
+ 'denied', 'consumed', 'expired'
68
+ )
69
+ ),
70
+ host_json TEXT NOT NULL,
71
+ approved_user_id TEXT,
72
+ approved_workspace_id TEXT,
73
+ environment_id TEXT,
74
+ credential_ciphertext TEXT,
75
+ credential_delivery_started_at INTEGER,
76
+ credential_redeliver_until INTEGER,
77
+ expires_at INTEGER NOT NULL,
78
+ created_at INTEGER NOT NULL,
79
+ updated_at INTEGER NOT NULL
80
+ )
81
+ `.execute(db);
82
+ await createAuthorizationIndexes(db);
83
+ }
84
+ async function createAuthorizationIndexes(db) {
85
+ await sql`
86
+ CREATE INDEX connect_authorizations_status_expiry
87
+ ON connect_device_authorizations(status, expires_at)
88
+ `.execute(db);
89
+ await sql`
90
+ CREATE INDEX connect_authorizations_user_code
91
+ ON connect_device_authorizations(user_code_hash, created_at)
92
+ `.execute(db);
93
+ await sql`
94
+ CREATE INDEX connect_authorizations_environment
95
+ ON connect_device_authorizations(environment_id)
96
+ `.execute(db);
97
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Compound indexes for account-and-workspace-bound Connect operations. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ await sql`
4
+ CREATE INDEX connect_environments_user_workspace_status_updated
5
+ ON connect_environments(user_id, workspace_id, status, updated_at)
6
+ `.execute(db);
7
+ await sql`
8
+ CREATE INDEX connect_environments_user_workspace_control_token
9
+ ON connect_environments(user_id, workspace_id, control_token_hash)
10
+ `.execute(db);
11
+ await sql`
12
+ CREATE INDEX connect_environments_user_workspace_id
13
+ ON connect_environments(user_id, workspace_id, id)
14
+ `.execute(db);
15
+ }
16
+ export async function down(db) {
17
+ await sql`
18
+ DROP INDEX IF EXISTS connect_environments_user_workspace_id
19
+ `.execute(db);
20
+ await sql`
21
+ DROP INDEX IF EXISTS connect_environments_user_workspace_control_token
22
+ `.execute(db);
23
+ await sql`
24
+ DROP INDEX IF EXISTS connect_environments_user_workspace_status_updated
25
+ `.execute(db);
26
+ }
@@ -0,0 +1,4 @@
1
+ import { type Kysely } from 'kysely';
2
+ /** Adds the bounded credential-delivery lease without losing pending requests. */
3
+ export declare function up(db: Kysely<unknown>): Promise<void>;
4
+ export declare function down(db: Kysely<unknown>): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import { sql } from "kysely";
2
+ export async function up(db) {
3
+ const columns = await authorizationColumns(db);
4
+ if (columns.has("credential_redeliver_until")) return;
5
+ await dropAuthorizationIndexes(db);
6
+ await sql`
7
+ ALTER TABLE connect_device_authorizations
8
+ RENAME TO connect_device_authorizations_before_redelivery
9
+ `.execute(db);
10
+ await createRedeliveryTable(db);
11
+ await sql`
12
+ INSERT INTO connect_device_authorizations (
13
+ id, device_code_hash, user_code_hash, status, host_json,
14
+ approved_user_id, approved_workspace_id, environment_id,
15
+ credential_ciphertext, expires_at, created_at, updated_at
16
+ )
17
+ SELECT
18
+ id, device_code_hash, user_code_hash, status, host_json,
19
+ approved_user_id, approved_workspace_id, environment_id,
20
+ credential_ciphertext, expires_at, created_at, updated_at
21
+ FROM connect_device_authorizations_before_redelivery
22
+ `.execute(db);
23
+ await sql`
24
+ DROP TABLE connect_device_authorizations_before_redelivery
25
+ `.execute(db);
26
+ }
27
+ export async function down(db) {
28
+ const columns = await authorizationColumns(db);
29
+ if (!columns.has("credential_redeliver_until")) return;
30
+ await dropAuthorizationIndexes(db);
31
+ await sql`
32
+ ALTER TABLE connect_device_authorizations
33
+ RENAME TO connect_device_authorizations_with_redelivery
34
+ `.execute(db);
35
+ await sql`
36
+ CREATE TABLE connect_device_authorizations (
37
+ id TEXT PRIMARY KEY,
38
+ device_code_hash TEXT NOT NULL UNIQUE,
39
+ user_code_hash TEXT NOT NULL,
40
+ status TEXT NOT NULL CHECK (
41
+ status IN (
42
+ 'pending', 'provisioning', 'approved',
43
+ 'denied', 'consumed', 'expired'
44
+ )
45
+ ),
46
+ host_json TEXT NOT NULL,
47
+ approved_user_id TEXT,
48
+ approved_workspace_id TEXT,
49
+ environment_id TEXT,
50
+ credential_ciphertext TEXT,
51
+ expires_at INTEGER NOT NULL,
52
+ created_at INTEGER NOT NULL,
53
+ updated_at INTEGER NOT NULL
54
+ )
55
+ `.execute(db);
56
+ await sql`
57
+ INSERT INTO connect_device_authorizations (
58
+ id, device_code_hash, user_code_hash, status, host_json,
59
+ approved_user_id, approved_workspace_id, environment_id,
60
+ credential_ciphertext, expires_at, created_at, updated_at
61
+ )
62
+ SELECT
63
+ id, device_code_hash, user_code_hash,
64
+ CASE WHEN status = 'delivering' THEN 'approved' ELSE status END,
65
+ host_json, approved_user_id, approved_workspace_id, environment_id,
66
+ credential_ciphertext, expires_at, created_at, updated_at
67
+ FROM connect_device_authorizations_with_redelivery
68
+ `.execute(db);
69
+ await createAuthorizationIndexes(db);
70
+ await sql`
71
+ DROP TABLE connect_device_authorizations_with_redelivery
72
+ `.execute(db);
73
+ }
74
+ async function authorizationColumns(db) {
75
+ const result = await sql`
76
+ PRAGMA table_info(connect_device_authorizations)
77
+ `.execute(db);
78
+ return new Set(result.rows.map((column) => column.name));
79
+ }
80
+ async function createRedeliveryTable(db) {
81
+ await sql`
82
+ CREATE TABLE connect_device_authorizations (
83
+ id TEXT PRIMARY KEY,
84
+ device_code_hash TEXT NOT NULL UNIQUE,
85
+ user_code_hash TEXT NOT NULL,
86
+ status TEXT NOT NULL CHECK (
87
+ status IN (
88
+ 'pending', 'provisioning', 'approved', 'delivering',
89
+ 'denied', 'consumed', 'expired'
90
+ )
91
+ ),
92
+ host_json TEXT NOT NULL,
93
+ approved_user_id TEXT,
94
+ approved_workspace_id TEXT,
95
+ environment_id TEXT,
96
+ credential_ciphertext TEXT,
97
+ credential_delivery_started_at INTEGER,
98
+ credential_redeliver_until INTEGER,
99
+ expires_at INTEGER NOT NULL,
100
+ created_at INTEGER NOT NULL,
101
+ updated_at INTEGER NOT NULL
102
+ )
103
+ `.execute(db);
104
+ await createAuthorizationIndexes(db);
105
+ }
106
+ async function dropAuthorizationIndexes(db) {
107
+ await sql`DROP INDEX IF EXISTS connect_authorizations_status_expiry`.execute(db);
108
+ await sql`DROP INDEX IF EXISTS connect_authorizations_user_code`.execute(db);
109
+ await sql`DROP INDEX IF EXISTS connect_authorizations_environment`.execute(db);
110
+ }
111
+ async function createAuthorizationIndexes(db) {
112
+ await sql`
113
+ CREATE INDEX connect_authorizations_status_expiry
114
+ ON connect_device_authorizations(status, expires_at)
115
+ `.execute(db);
116
+ await sql`
117
+ CREATE INDEX connect_authorizations_user_code
118
+ ON connect_device_authorizations(user_code_hash, created_at)
119
+ `.execute(db);
120
+ await sql`
121
+ CREATE INDEX connect_authorizations_environment
122
+ ON connect_device_authorizations(environment_id)
123
+ `.execute(db);
124
+ }
@@ -0,0 +1,10 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Adds durable provisioning/revocation work with lease-based claiming.
4
+ *
5
+ * SQLite cannot widen CHECK constraints in place, so both Connect tables are
6
+ * rebuilt transactionally while preserving active environments and approved
7
+ * credentials.
8
+ */
9
+ export declare function up(db: Kysely<unknown>): Promise<void>;
10
+ export declare function down(db: Kysely<unknown>): Promise<void>;