@saluzi/saluzi-edu 0.2.26 → 0.2.27
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.
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
-- RCS Authentication & Session Schema
|
|
2
|
+
-- Phase 1: users, session_tokens, invitations
|
|
3
|
+
-- Phase 2: teams, team_members, team_environments, session_owners, session_shares, sessions
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
6
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
7
|
+
username TEXT UNIQUE NOT NULL,
|
|
8
|
+
password_hash TEXT NOT NULL,
|
|
9
|
+
role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'guest')),
|
|
10
|
+
created_at TEXT NOT NULL,
|
|
11
|
+
last_login_at TEXT
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS session_tokens (
|
|
15
|
+
token_hash TEXT NOT NULL PRIMARY KEY,
|
|
16
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
17
|
+
kind TEXT NOT NULL CHECK (kind IN ('access', 'refresh')),
|
|
18
|
+
expires_at TEXT NOT NULL,
|
|
19
|
+
revoked_at TEXT,
|
|
20
|
+
created_at TEXT NOT NULL
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS invitations (
|
|
24
|
+
token_hash TEXT NOT NULL PRIMARY KEY,
|
|
25
|
+
team_id TEXT,
|
|
26
|
+
role TEXT NOT NULL,
|
|
27
|
+
expires_at TEXT NOT NULL,
|
|
28
|
+
max_uses INTEGER DEFAULT 1,
|
|
29
|
+
uses INTEGER DEFAULT 0,
|
|
30
|
+
created_by TEXT NOT NULL REFERENCES users(id),
|
|
31
|
+
consumed_at TEXT,
|
|
32
|
+
consumed_by TEXT REFERENCES users(id)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Phase 2 tables
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
slug TEXT UNIQUE NOT NULL,
|
|
41
|
+
description TEXT,
|
|
42
|
+
created_by TEXT NOT NULL REFERENCES users(id),
|
|
43
|
+
created_at TEXT NOT NULL,
|
|
44
|
+
updated_at TEXT NOT NULL,
|
|
45
|
+
deleted_at TEXT
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS team_members (
|
|
49
|
+
team_id TEXT NOT NULL REFERENCES teams(id),
|
|
50
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
51
|
+
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
|
|
52
|
+
added_at TEXT NOT NULL,
|
|
53
|
+
added_by TEXT NOT NULL REFERENCES users(id),
|
|
54
|
+
PRIMARY KEY (team_id, user_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS team_environments (
|
|
58
|
+
team_id TEXT NOT NULL REFERENCES teams(id),
|
|
59
|
+
environment_id TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY (team_id, environment_id)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS session_owners (
|
|
64
|
+
session_id TEXT NOT NULL,
|
|
65
|
+
owner_type TEXT NOT NULL CHECK (owner_type IN ('user', 'team')),
|
|
66
|
+
owner_id TEXT NOT NULL,
|
|
67
|
+
PRIMARY KEY (session_id, owner_type, owner_id)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS session_shares (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
session_id TEXT NOT NULL,
|
|
73
|
+
granted_to_user TEXT REFERENCES users(id),
|
|
74
|
+
granted_to_team TEXT REFERENCES teams(id),
|
|
75
|
+
permission TEXT NOT NULL CHECK (permission IN ('read', 'write')),
|
|
76
|
+
expires_at TEXT,
|
|
77
|
+
granted_by TEXT NOT NULL REFERENCES users(id),
|
|
78
|
+
granted_at TEXT NOT NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
82
|
+
id TEXT PRIMARY KEY,
|
|
83
|
+
secret_hash TEXT NOT NULL,
|
|
84
|
+
team_id TEXT,
|
|
85
|
+
registered_by TEXT,
|
|
86
|
+
machine_name TEXT,
|
|
87
|
+
worker_type TEXT NOT NULL DEFAULT 'claude_code',
|
|
88
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
89
|
+
last_poll_at TEXT,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
owner_user_id TEXT,
|
|
92
|
+
claim_token TEXT,
|
|
93
|
+
claim_expires_at TEXT
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
environment_id TEXT,
|
|
99
|
+
title TEXT,
|
|
100
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
101
|
+
source TEXT NOT NULL DEFAULT 'remote-control',
|
|
102
|
+
visibility TEXT NOT NULL DEFAULT 'private',
|
|
103
|
+
permission_mode TEXT,
|
|
104
|
+
created_at TEXT NOT NULL,
|
|
105
|
+
updated_at TEXT NOT NULL
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
-- Performance indexes
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_session_owners_owner ON session_owners(owner_type, owner_id);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_session_owners_session ON session_owners(session_id);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_session_shares_user ON session_shares(granted_to_user);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_session_shares_team ON session_shares(granted_to_team);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_session_shares_session ON session_shares(session_id);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_visibility ON sessions(visibility);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_environment ON sessions(environment_id);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_session_tokens_user ON session_tokens(user_id);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_environments_team ON environments(team_id);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_environments_owner_user ON environments(owner_user_id);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_environments_claim_token ON environments(claim_token);
|
|
@@ -42,9 +42,33 @@ const ResolvedDatabase: DatabaseCtor = (() => {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Tier 3: libsql (optionalDependency — covers Node 20/21)
|
|
45
|
+
// libsql 0.5.x exports the Database constructor directly (better-sqlite3
|
|
46
|
+
// compatible API): `require('libsql')` returns a function, not an object
|
|
47
|
+
// with a `.Database` property. Check `typeof === 'function'` and call
|
|
48
|
+
// `new libsql(path)` directly.
|
|
49
|
+
//
|
|
50
|
+
// Parameter format compatibility: bun:sqlite and node:sqlite both accept
|
|
51
|
+
// `$name` named params, but libsql (better-sqlite3 lineage) only accepts
|
|
52
|
+
// `:name` and `@name` — `$name` silently fails to bind, causing
|
|
53
|
+
// "NOT NULL constraint failed" errors. Convert `$name` → `:name` in SQL
|
|
54
|
+
// and `{$name: v}` → `{name: v}` in param objects so RCS queries work
|
|
55
|
+
// unchanged across all three backends.
|
|
45
56
|
try {
|
|
46
57
|
const libsql = require('libsql')
|
|
47
|
-
if (libsql
|
|
58
|
+
if (typeof libsql === 'function') {
|
|
59
|
+
const convertSql = (sql: string): string => sql.replace(/\$(\w+)/g, ':$1')
|
|
60
|
+
const convertParams = (params: unknown[]): unknown[] =>
|
|
61
|
+
params.map(p => {
|
|
62
|
+
if (p && typeof p === 'object' && !Array.isArray(p)) {
|
|
63
|
+
const obj = p as Record<string, unknown>
|
|
64
|
+
const out: Record<string, unknown> = {}
|
|
65
|
+
for (const k of Object.keys(obj)) {
|
|
66
|
+
out[k.startsWith('$') ? k.slice(1) : k] = obj[k]
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
return p
|
|
71
|
+
})
|
|
48
72
|
return class LibsqlCompat {
|
|
49
73
|
private _db: {
|
|
50
74
|
prepare(sql: string): {
|
|
@@ -56,17 +80,17 @@ const ResolvedDatabase: DatabaseCtor = (() => {
|
|
|
56
80
|
close(): void
|
|
57
81
|
}
|
|
58
82
|
constructor(path: string) {
|
|
59
|
-
this._db = new libsql
|
|
83
|
+
this._db = new libsql(path)
|
|
60
84
|
}
|
|
61
85
|
exec(sql: string): void {
|
|
62
86
|
this._db.exec(sql)
|
|
63
87
|
}
|
|
64
88
|
query(sql: string) {
|
|
65
|
-
const stmt = this._db.prepare(sql)
|
|
89
|
+
const stmt = this._db.prepare(convertSql(sql))
|
|
66
90
|
return {
|
|
67
|
-
get: (...params: unknown[]) => stmt.get(...params),
|
|
68
|
-
all: (...params: unknown[]) => stmt.all(...params),
|
|
69
|
-
run: (...params: unknown[]) => stmt.run(...params),
|
|
91
|
+
get: (...params: unknown[]) => stmt.get(...convertParams(params)),
|
|
92
|
+
all: (...params: unknown[]) => stmt.all(...convertParams(params)),
|
|
93
|
+
run: (...params: unknown[]) => stmt.run(...convertParams(params)),
|
|
70
94
|
}
|
|
71
95
|
}
|
|
72
96
|
close(): void {
|
|
@@ -92,15 +116,20 @@ let _dbPath: string | null = null
|
|
|
92
116
|
* Idempotent — uses CREATE TABLE IF NOT EXISTS for new tables, plus
|
|
93
117
|
* ALTER TABLE ADD COLUMN for columns added to existing tables (gated by
|
|
94
118
|
* PRAGMA table_info so re-running on an already-migrated DB is a no-op).
|
|
95
|
-
* Reads schema.sql and executes as a single db.exec() call,
|
|
119
|
+
* Reads rcs-schema.sql and executes as a single db.exec() call,
|
|
96
120
|
* which correctly handles semicolons inside CHECK constraints.
|
|
121
|
+
*
|
|
122
|
+
* File is named rcs-schema.sql (not schema.sql) to avoid collision with
|
|
123
|
+
* CodeGraph's schema.sql at the same dist/ path — both modules resolve
|
|
124
|
+
* their schema via __dirname, and the bundler places them in the same
|
|
125
|
+
* chunk directory.
|
|
97
126
|
*/
|
|
98
127
|
export function migrateDatabase(db: Database): void {
|
|
99
|
-
const schemaPath = resolve(__dirname, 'schema.sql')
|
|
128
|
+
const schemaPath = resolve(__dirname, 'rcs-schema.sql')
|
|
100
129
|
const schema = readFileSync(schemaPath, 'utf8')
|
|
101
130
|
|
|
102
131
|
db.exec('PRAGMA foreign_keys = ON')
|
|
103
|
-
// Run ALTER TABLE migrations BEFORE db.exec(schema) — schema.sql's
|
|
132
|
+
// Run ALTER TABLE migrations BEFORE db.exec(schema) — rcs-schema.sql's
|
|
104
133
|
// CREATE INDEX statements reference the new columns, so the columns
|
|
105
134
|
// must exist before the indexes can be created. CREATE TABLE IF NOT
|
|
106
135
|
// EXISTS skips existing tables (so the ALTER adds the missing columns
|
|
@@ -116,7 +145,7 @@ export function migrateDatabase(db: Database): void {
|
|
|
116
145
|
* before owner_user_id / claim_token / claim_expires_at existed.
|
|
117
146
|
* CREATE TABLE IF NOT EXISTS skips existing tables entirely, so without
|
|
118
147
|
* this the new columns would never appear on upgraded DBs. Must run
|
|
119
|
-
* BEFORE db.exec(schema) because schema.sql's CREATE INDEX statements
|
|
148
|
+
* BEFORE db.exec(schema) because rcs-schema.sql's CREATE INDEX statements
|
|
120
149
|
* reference the new columns. On a fresh DB (table doesn't exist yet),
|
|
121
150
|
* skip entirely — db.exec(schema) will create the table with all columns.
|
|
122
151
|
*/
|
|
File without changes
|