or3-provider-sqlite 0.0.2 → 0.0.4

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 (25) 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.d.ts +1 -1
  22. package/dist/runtime/server/plugins/register.js +24 -8
  23. package/dist/runtime/server/rate-limit/sqlite-provider.d.ts +7 -0
  24. package/dist/runtime/server/rate-limit/sqlite-provider.js +70 -0
  25. package/package.json +10 -2
@@ -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
+ }
@@ -131,6 +131,60 @@ export interface UploadIntentsTable {
131
131
  consumed_at: number | null;
132
132
  cancelled_at: number | null;
133
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
+ }
134
188
  export interface AdminUsersTable {
135
189
  user_id: string;
136
190
  created_at: number;
@@ -156,6 +210,9 @@ export interface Or3SqliteDb {
156
210
  sync_snapshots: SyncSnapshotsTable;
157
211
  sync_snapshot_items: SyncSnapshotItemsTable;
158
212
  upload_intents: UploadIntentsTable;
213
+ connect_device_authorizations: ConnectDeviceAuthorizationsTable;
214
+ connect_environments: ConnectEnvironmentsTable;
215
+ rate_limits: RateLimitsTable;
159
216
  s_threads: SyncedEntityTable;
160
217
  s_messages: SyncedEntityTable;
161
218
  s_projects: SyncedEntityTable;
@@ -1,2 +1,2 @@
1
- declare const _default: any;
1
+ declare const _default: import("nitropack").NitroAppPlugin;
2
2
  export default _default;
@@ -1,8 +1,11 @@
1
+ import { defineNitroPlugin } from "nitropack/runtime/plugin";
1
2
  import { registerAuthWorkspaceStore } from "~~/server/auth/store/registry";
2
3
  import { registerProviderAdminAdapter } from "~~/server/admin/providers/registry";
3
4
  import { registerAdminStoreProvider } from "~~/server/admin/stores/registry";
4
5
  import { registerSyncGatewayAdapter } from "~~/server/sync/gateway/registry";
5
6
  import { registerWebhookStore } from "~~/server/utils/webhooks/store/registry";
7
+ import { registerConnectStore } from "~~/server/connect/store/registry";
8
+ import { registerRateLimitProvider } from "~~/server/utils/rate-limit/registry";
6
9
  import { createSqliteAuthWorkspaceStore } from "../auth/sqlite-auth-workspace-store.js";
7
10
  import { createSqliteSyncGatewayAdapter } from "../sync/sqlite-sync-gateway-adapter.js";
8
11
  import { createSqliteWebhookStore } from "../webhooks/sqlite-webhook-store.js";
@@ -12,27 +15,39 @@ import {
12
15
  createSqliteWorkspaceSettingsStore
13
16
  } from "../admin/stores/sqlite-store.js";
14
17
  import { sqliteSyncAdminAdapter } from "../admin/adapters/sync-sqlite.js";
18
+ import { createSqliteConnectStore } from "../connect/sqlite-connect-store.js";
15
19
  import { getSqliteDb } from "../db/kysely.js";
16
20
  import { runMigrations } from "../db/migrate.js";
21
+ import { sqliteRateLimitProvider } from "../rate-limit/sqlite-provider.js";
17
22
  import { useRuntimeConfig } from "#imports";
18
23
  const SQLITE_PROVIDER_ID = "sqlite";
19
24
  export default defineNitroPlugin(async () => {
20
25
  const config = useRuntimeConfig();
21
26
  if (!config.auth?.enabled) return;
22
- if (!config.sync?.enabled) return;
23
- if (config.sync?.provider !== SQLITE_PROVIDER_ID) return;
27
+ const syncSelected = config.sync?.enabled === true && config.sync?.provider === SQLITE_PROVIDER_ID;
28
+ const connectSelected = config.connect?.enabled === true && config.connect?.provider === SQLITE_PROVIDER_ID;
29
+ if (!syncSelected && !connectSelected) return;
24
30
  const db = getSqliteDb();
25
- await runMigrations(db);
26
31
  registerAuthWorkspaceStore({
27
32
  id: SQLITE_PROVIDER_ID,
28
33
  order: 100,
29
34
  create: createSqliteAuthWorkspaceStore
30
35
  });
31
- registerSyncGatewayAdapter({
32
- id: SQLITE_PROVIDER_ID,
33
- order: 100,
34
- create: createSqliteSyncGatewayAdapter
35
- });
36
+ registerRateLimitProvider(SQLITE_PROVIDER_ID, sqliteRateLimitProvider);
37
+ if (syncSelected) {
38
+ registerSyncGatewayAdapter({
39
+ id: SQLITE_PROVIDER_ID,
40
+ order: 100,
41
+ create: createSqliteSyncGatewayAdapter
42
+ });
43
+ }
44
+ if (connectSelected) {
45
+ registerConnectStore({
46
+ id: SQLITE_PROVIDER_ID,
47
+ order: 100,
48
+ create: createSqliteConnectStore
49
+ });
50
+ }
36
51
  registerWebhookStore({
37
52
  id: SQLITE_PROVIDER_ID,
38
53
  order: 100,
@@ -52,4 +67,5 @@ export default defineNitroPlugin(async () => {
52
67
  })
53
68
  });
54
69
  registerProviderAdminAdapter(sqliteSyncAdminAdapter);
70
+ await runMigrations(db);
55
71
  });
@@ -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
+ }
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "or3-provider-sqlite",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/Saluana/or3-provider-sqlite.git"
7
+ },
4
8
  "description": "SQLite sync and workspace store provider for OR3 Chat — lightweight self-hosted backend via Kysely.",
5
9
  "license": "MIT",
6
10
  "type": "module",
11
+ "engines": {
12
+ "node": ">=24"
13
+ },
14
+ "packageManager": "bun@1.3.14",
7
15
  "sideEffects": false,
8
16
  "exports": {
9
17
  ".": {
@@ -46,7 +54,7 @@
46
54
  "@eslint/js": "^9.34.0",
47
55
  "@nuxt/module-builder": "^1.0.2",
48
56
  "@types/better-sqlite3": "^7.6.13",
49
- "@types/node": "^25.2.2",
57
+ "@types/node": "^24.10.1",
50
58
  "@typescript-eslint/eslint-plugin": "^8.41.0",
51
59
  "@typescript-eslint/parser": "^8.41.0",
52
60
  "eslint": "^9.34.0",