or3-provider-sqlite 0.0.2 → 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 (24) hide show
  1. package/README.md +14 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/server/connect/sqlite-connect-store.d.ts +2 -0
  4. package/dist/runtime/server/connect/sqlite-connect-store.js +885 -0
  5. package/dist/runtime/server/db/migrate.js +19 -2
  6. package/dist/runtime/server/db/migrations/009_or3_connect.d.ts +4 -0
  7. package/dist/runtime/server/db/migrations/009_or3_connect.js +91 -0
  8. package/dist/runtime/server/db/migrations/010_connect_user_code_hmac.d.ts +10 -0
  9. package/dist/runtime/server/db/migrations/010_connect_user_code_hmac.js +97 -0
  10. package/dist/runtime/server/db/migrations/011_connect_environment_scope_indexes.d.ts +4 -0
  11. package/dist/runtime/server/db/migrations/011_connect_environment_scope_indexes.js +26 -0
  12. package/dist/runtime/server/db/migrations/012_connect_credential_redelivery.d.ts +4 -0
  13. package/dist/runtime/server/db/migrations/012_connect_credential_redelivery.js +124 -0
  14. package/dist/runtime/server/db/migrations/013_connect_environment_lifecycle.d.ts +10 -0
  15. package/dist/runtime/server/db/migrations/013_connect_environment_lifecycle.js +261 -0
  16. package/dist/runtime/server/db/migrations/014_connect_retention_activation.d.ts +4 -0
  17. package/dist/runtime/server/db/migrations/014_connect_retention_activation.js +44 -0
  18. package/dist/runtime/server/db/migrations/015_rate_limits.d.ts +4 -0
  19. package/dist/runtime/server/db/migrations/015_rate_limits.js +18 -0
  20. package/dist/runtime/server/db/schema.d.ts +57 -0
  21. package/dist/runtime/server/plugins/register.js +23 -8
  22. package/dist/runtime/server/rate-limit/sqlite-provider.d.ts +7 -0
  23. package/dist/runtime/server/rate-limit/sqlite-provider.js +70 -0
  24. package/package.json +5 -1
@@ -7,6 +7,13 @@ import * as m005 from "./migrations/005_admin_stores.js";
7
7
  import * as m006 from "./migrations/006_sync_snapshots.js";
8
8
  import * as m007 from "./migrations/007_device_cursor_ownership.js";
9
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";
10
17
  const REQUIRED_SCHEMA_TABLES = [
11
18
  "users",
12
19
  "auth_accounts",
@@ -28,7 +35,10 @@ const REQUIRED_SCHEMA_TABLES = [
28
35
  "admin_workspace_settings",
29
36
  "sync_snapshots",
30
37
  "sync_snapshot_items",
31
- "upload_intents"
38
+ "upload_intents",
39
+ "connect_device_authorizations",
40
+ "connect_environments",
41
+ "rate_limits"
32
42
  ];
33
43
  const migrations = {
34
44
  "001_init": m001,
@@ -38,7 +48,14 @@ const migrations = {
38
48
  "005_admin_stores": m005,
39
49
  "006_sync_snapshots": m006,
40
50
  "007_device_cursor_ownership": m007,
41
- "008_upload_intents": m008
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
42
59
  };
43
60
  class StaticMigrationProvider {
44
61
  async getMigrations() {
@@ -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>;
@@ -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
+ }