@treeseed/agent 0.8.5
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/Dockerfile +7 -0
- package/README.md +198 -0
- package/dist/agent-runtime.d.ts +17 -0
- package/dist/agent-runtime.js +117 -0
- package/dist/agents/adapters/execution.d.ts +41 -0
- package/dist/agents/adapters/execution.js +73 -0
- package/dist/agents/adapters/mutations.d.ts +22 -0
- package/dist/agents/adapters/mutations.js +30 -0
- package/dist/agents/adapters/notification.d.ts +26 -0
- package/dist/agents/adapters/notification.js +46 -0
- package/dist/agents/adapters/repository.d.ts +28 -0
- package/dist/agents/adapters/repository.js +61 -0
- package/dist/agents/adapters/research.d.ts +26 -0
- package/dist/agents/adapters/research.js +59 -0
- package/dist/agents/adapters/verification.d.ts +36 -0
- package/dist/agents/adapters/verification.js +62 -0
- package/dist/agents/cli-tools.d.ts +1 -0
- package/dist/agents/cli-tools.js +5 -0
- package/dist/agents/cli.d.ts +15 -0
- package/dist/agents/cli.js +109 -0
- package/dist/agents/contracts/messages.d.ts +88 -0
- package/dist/agents/contracts/messages.js +138 -0
- package/dist/agents/contracts/run.d.ts +21 -0
- package/dist/agents/contracts/run.js +0 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.js +5 -0
- package/dist/agents/kernel/agent-kernel.d.ts +63 -0
- package/dist/agents/kernel/agent-kernel.js +291 -0
- package/dist/agents/kernel/trigger-resolver.d.ts +19 -0
- package/dist/agents/kernel/trigger-resolver.js +157 -0
- package/dist/agents/registry-helper.d.ts +4 -0
- package/dist/agents/registry-helper.js +14 -0
- package/dist/agents/registry.d.ts +6 -0
- package/dist/agents/registry.js +98 -0
- package/dist/agents/runtime-types.d.ts +118 -0
- package/dist/agents/runtime-types.js +0 -0
- package/dist/agents/spec-loader.d.ts +18 -0
- package/dist/agents/spec-loader.js +54 -0
- package/dist/agents/spec-normalizer.d.ts +2 -0
- package/dist/agents/spec-normalizer.js +327 -0
- package/dist/agents/spec-types.d.ts +64 -0
- package/dist/agents/spec-types.js +0 -0
- package/dist/agents/testing/agents-smoke.d.ts +1 -0
- package/dist/agents/testing/agents-smoke.js +32 -0
- package/dist/agents/testing/e2e-harness.d.ts +44 -0
- package/dist/agents/testing/e2e-harness.js +503 -0
- package/dist/api/agent-routes.d.ts +13 -0
- package/dist/api/agent-routes.js +327 -0
- package/dist/api/app.d.ts +8 -0
- package/dist/api/app.js +444 -0
- package/dist/api/auth/d1-database.d.ts +3 -0
- package/dist/api/auth/d1-database.js +20 -0
- package/dist/api/auth/d1-provider.d.ts +79 -0
- package/dist/api/auth/d1-provider.js +92 -0
- package/dist/api/auth/d1-store.d.ts +114 -0
- package/dist/api/auth/d1-store.js +895 -0
- package/dist/api/auth/memory-provider.d.ts +77 -0
- package/dist/api/auth/memory-provider.js +249 -0
- package/dist/api/auth/rbac.d.ts +22 -0
- package/dist/api/auth/rbac.js +162 -0
- package/dist/api/auth/tokens.d.ts +18 -0
- package/dist/api/auth/tokens.js +56 -0
- package/dist/api/capabilities.d.ts +9 -0
- package/dist/api/capabilities.js +33 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +77 -0
- package/dist/api/http.d.ts +28 -0
- package/dist/api/http.js +51 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.js +20 -0
- package/dist/api/operations-routes.d.ts +11 -0
- package/dist/api/operations-routes.js +87 -0
- package/dist/api/operations.d.ts +3 -0
- package/dist/api/operations.js +26 -0
- package/dist/api/project-routes.d.ts +8 -0
- package/dist/api/project-routes.js +585 -0
- package/dist/api/providers.d.ts +2 -0
- package/dist/api/providers.js +62 -0
- package/dist/api/railway.d.ts +51 -0
- package/dist/api/railway.js +71 -0
- package/dist/api/sdk-dispatch.d.ts +5 -0
- package/dist/api/sdk-dispatch.js +13 -0
- package/dist/api/sdk-routes.d.ts +11 -0
- package/dist/api/sdk-routes.js +29 -0
- package/dist/api/server.d.ts +2 -0
- package/dist/api/server.js +10 -0
- package/dist/api/templates.d.ts +3 -0
- package/dist/api/templates.js +31 -0
- package/dist/api/types.d.ts +237 -0
- package/dist/api/types.js +0 -0
- package/dist/env.yaml +957 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +41 -0
- package/dist/scripts/assert-release-tag-version.d.ts +1 -0
- package/dist/scripts/assert-release-tag-version.js +20 -0
- package/dist/scripts/build-dist.d.ts +1 -0
- package/dist/scripts/build-dist.js +106 -0
- package/dist/scripts/package-tools.d.ts +1 -0
- package/dist/scripts/package-tools.js +7 -0
- package/dist/scripts/publish-package.d.ts +1 -0
- package/dist/scripts/publish-package.js +24 -0
- package/dist/scripts/release-verify.d.ts +1 -0
- package/dist/scripts/release-verify.js +152 -0
- package/dist/scripts/test-smoke.d.ts +1 -0
- package/dist/scripts/test-smoke.js +23 -0
- package/dist/scripts/treeseed-agent-api.d.ts +2 -0
- package/dist/scripts/treeseed-agent-api.js +25 -0
- package/dist/scripts/treeseed-agent-service.d.ts +2 -0
- package/dist/scripts/treeseed-agent-service.js +36 -0
- package/dist/scripts/treeseed-agents.d.ts +2 -0
- package/dist/scripts/treeseed-agents.js +13 -0
- package/dist/services/agents.d.ts +17 -0
- package/dist/services/agents.js +48 -0
- package/dist/services/common.d.ts +66 -0
- package/dist/services/common.js +212 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +19 -0
- package/dist/services/manager.d.ts +333 -0
- package/dist/services/manager.js +1368 -0
- package/dist/services/remote-runner.d.ts +30 -0
- package/dist/services/remote-runner.js +230 -0
- package/dist/services/workday-content.d.ts +53 -0
- package/dist/services/workday-content.js +190 -0
- package/dist/services/workday-manager.d.ts +391 -0
- package/dist/services/workday-manager.js +163 -0
- package/dist/services/workday-report.d.ts +238 -0
- package/dist/services/workday-report.js +17 -0
- package/dist/services/workday-start.d.ts +238 -0
- package/dist/services/workday-start.js +17 -0
- package/dist/services/worker-capacity.d.ts +58 -0
- package/dist/services/worker-capacity.js +208 -0
- package/dist/services/worker-pool-scaler.d.ts +27 -0
- package/dist/services/worker-pool-scaler.js +127 -0
- package/dist/services/worker.d.ts +19 -0
- package/dist/services/worker.js +436 -0
- package/dist/templates/github/deploy-processing.workflow.yml +119 -0
- package/package.json +136 -0
- package/templates/github/deploy-processing.workflow.yml +119 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { DEFAULT_PERMISSIONS, DEFAULT_ROLES } from "./rbac.js";
|
|
3
|
+
import { createAccessToken, nextOpaqueToken, principalFromAccessTokenPayload, verifyAccessToken } from "./tokens.js";
|
|
4
|
+
const AUTH_SCHEMA_SQL = [
|
|
5
|
+
`CREATE TABLE IF NOT EXISTS users (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
email TEXT,
|
|
8
|
+
username TEXT UNIQUE,
|
|
9
|
+
display_name TEXT,
|
|
10
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
11
|
+
metadata_json TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL
|
|
14
|
+
)`,
|
|
15
|
+
`CREATE TABLE IF NOT EXISTS user_identities (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
user_id TEXT NOT NULL,
|
|
18
|
+
provider TEXT NOT NULL,
|
|
19
|
+
provider_subject TEXT NOT NULL,
|
|
20
|
+
email TEXT,
|
|
21
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
profile_json TEXT,
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
updated_at TEXT NOT NULL,
|
|
25
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
26
|
+
)`,
|
|
27
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_identities_provider_subject
|
|
28
|
+
ON user_identities(provider, provider_subject)`,
|
|
29
|
+
`CREATE TABLE IF NOT EXISTS roles (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
key TEXT NOT NULL UNIQUE,
|
|
32
|
+
description TEXT,
|
|
33
|
+
created_at TEXT NOT NULL
|
|
34
|
+
)`,
|
|
35
|
+
`CREATE TABLE IF NOT EXISTS permissions (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
key TEXT NOT NULL UNIQUE,
|
|
38
|
+
resource TEXT NOT NULL,
|
|
39
|
+
action TEXT NOT NULL,
|
|
40
|
+
scope TEXT NOT NULL,
|
|
41
|
+
description TEXT,
|
|
42
|
+
created_at TEXT NOT NULL
|
|
43
|
+
)`,
|
|
44
|
+
`CREATE TABLE IF NOT EXISTS role_permissions (
|
|
45
|
+
role_id TEXT NOT NULL,
|
|
46
|
+
permission_id TEXT NOT NULL,
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
PRIMARY KEY (role_id, permission_id),
|
|
49
|
+
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
|
50
|
+
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
|
51
|
+
)`,
|
|
52
|
+
`CREATE TABLE IF NOT EXISTS user_role_bindings (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
user_id TEXT NOT NULL,
|
|
55
|
+
role_id TEXT NOT NULL,
|
|
56
|
+
created_at TEXT NOT NULL,
|
|
57
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
58
|
+
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
|
59
|
+
)`,
|
|
60
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_role_bindings_user_role
|
|
61
|
+
ON user_role_bindings(user_id, role_id)`,
|
|
62
|
+
`CREATE TABLE IF NOT EXISTS api_tokens (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
user_id TEXT NOT NULL,
|
|
65
|
+
kind TEXT NOT NULL,
|
|
66
|
+
name TEXT NOT NULL,
|
|
67
|
+
token_prefix TEXT NOT NULL,
|
|
68
|
+
token_hash TEXT NOT NULL,
|
|
69
|
+
scopes_json TEXT NOT NULL,
|
|
70
|
+
expires_at TEXT,
|
|
71
|
+
last_used_at TEXT,
|
|
72
|
+
revoked_at TEXT,
|
|
73
|
+
metadata_json TEXT,
|
|
74
|
+
created_at TEXT NOT NULL,
|
|
75
|
+
updated_at TEXT NOT NULL,
|
|
76
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
77
|
+
)`,
|
|
78
|
+
`CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id
|
|
79
|
+
ON api_tokens(user_id)`,
|
|
80
|
+
`CREATE INDEX IF NOT EXISTS idx_api_tokens_prefix
|
|
81
|
+
ON api_tokens(token_prefix)`,
|
|
82
|
+
`CREATE TABLE IF NOT EXISTS service_credentials (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
service_id TEXT NOT NULL UNIQUE,
|
|
85
|
+
name TEXT NOT NULL,
|
|
86
|
+
secret_hash TEXT NOT NULL,
|
|
87
|
+
roles_json TEXT NOT NULL,
|
|
88
|
+
permissions_json TEXT NOT NULL,
|
|
89
|
+
revoked_at TEXT,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
updated_at TEXT NOT NULL,
|
|
92
|
+
last_used_at TEXT
|
|
93
|
+
)`,
|
|
94
|
+
`CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
user_id TEXT NOT NULL,
|
|
97
|
+
session_type TEXT NOT NULL,
|
|
98
|
+
refresh_token_hash TEXT NOT NULL,
|
|
99
|
+
scopes_json TEXT NOT NULL,
|
|
100
|
+
expires_at TEXT NOT NULL,
|
|
101
|
+
revoked_at TEXT,
|
|
102
|
+
data_json TEXT,
|
|
103
|
+
created_at TEXT NOT NULL,
|
|
104
|
+
updated_at TEXT NOT NULL,
|
|
105
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
106
|
+
)`,
|
|
107
|
+
`CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id
|
|
108
|
+
ON auth_sessions(user_id)`,
|
|
109
|
+
`CREATE TABLE IF NOT EXISTS audit_events (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
actor_type TEXT NOT NULL,
|
|
112
|
+
actor_id TEXT,
|
|
113
|
+
event_type TEXT NOT NULL,
|
|
114
|
+
target_type TEXT,
|
|
115
|
+
target_id TEXT,
|
|
116
|
+
data_json TEXT,
|
|
117
|
+
created_at TEXT NOT NULL
|
|
118
|
+
)`,
|
|
119
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_events_target
|
|
120
|
+
ON audit_events(target_type, target_id)`,
|
|
121
|
+
`CREATE TABLE IF NOT EXISTS device_codes (
|
|
122
|
+
id TEXT PRIMARY KEY,
|
|
123
|
+
device_code TEXT NOT NULL UNIQUE,
|
|
124
|
+
user_code TEXT NOT NULL UNIQUE,
|
|
125
|
+
requested_scopes_json TEXT NOT NULL,
|
|
126
|
+
expires_at TEXT NOT NULL,
|
|
127
|
+
interval_seconds INTEGER NOT NULL,
|
|
128
|
+
status TEXT NOT NULL,
|
|
129
|
+
user_id TEXT,
|
|
130
|
+
created_at TEXT NOT NULL,
|
|
131
|
+
updated_at TEXT NOT NULL
|
|
132
|
+
)`
|
|
133
|
+
];
|
|
134
|
+
function now() {
|
|
135
|
+
return /* @__PURE__ */ new Date();
|
|
136
|
+
}
|
|
137
|
+
function isoNow() {
|
|
138
|
+
return now().toISOString();
|
|
139
|
+
}
|
|
140
|
+
function addSeconds(date, seconds) {
|
|
141
|
+
return new Date(date.getTime() + seconds * 1e3);
|
|
142
|
+
}
|
|
143
|
+
function parseJson(value, fallback) {
|
|
144
|
+
if (!value) return fallback;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(value);
|
|
147
|
+
} catch {
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function stableHash(value, secret) {
|
|
152
|
+
return createHash("sha256").update(`${secret}:${value}`).digest("hex");
|
|
153
|
+
}
|
|
154
|
+
function equalHash(left, right) {
|
|
155
|
+
const leftBuffer = Buffer.from(left);
|
|
156
|
+
const rightBuffer = Buffer.from(right);
|
|
157
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
158
|
+
}
|
|
159
|
+
class D1AuthStore {
|
|
160
|
+
constructor(config, db) {
|
|
161
|
+
this.config = config;
|
|
162
|
+
this.db = db;
|
|
163
|
+
}
|
|
164
|
+
config;
|
|
165
|
+
db;
|
|
166
|
+
initializationPromise = null;
|
|
167
|
+
async run(query, params = []) {
|
|
168
|
+
await this.db.prepare(query).bind(...params).run();
|
|
169
|
+
}
|
|
170
|
+
async first(query, params = []) {
|
|
171
|
+
return this.db.prepare(query).bind(...params).first();
|
|
172
|
+
}
|
|
173
|
+
async all(query, params = []) {
|
|
174
|
+
const result = await this.db.prepare(query).bind(...params).all();
|
|
175
|
+
return result.results ?? [];
|
|
176
|
+
}
|
|
177
|
+
ensureInitialized() {
|
|
178
|
+
if (!this.initializationPromise) {
|
|
179
|
+
this.initializationPromise = this.ensureAuthSchema().then(() => this.seedCatalog()).then(() => this.seedConfiguredServices());
|
|
180
|
+
}
|
|
181
|
+
return this.initializationPromise;
|
|
182
|
+
}
|
|
183
|
+
async ensureAuthSchema() {
|
|
184
|
+
for (const statement of AUTH_SCHEMA_SQL) await this.run(statement);
|
|
185
|
+
const result = await this.db.prepare("PRAGMA table_info(users)").all();
|
|
186
|
+
const columns = new Set((result.results ?? []).map((row) => row.name));
|
|
187
|
+
if (!columns.has("username")) {
|
|
188
|
+
await this.run("ALTER TABLE users ADD COLUMN username TEXT");
|
|
189
|
+
}
|
|
190
|
+
await this.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username)");
|
|
191
|
+
}
|
|
192
|
+
async seedCatalog() {
|
|
193
|
+
const createdAt = isoNow();
|
|
194
|
+
const seeded = await this.first(
|
|
195
|
+
`SELECT key FROM permissions WHERE key = '*:*:*' LIMIT 1`
|
|
196
|
+
);
|
|
197
|
+
const adminRole = await this.first(
|
|
198
|
+
`SELECT key FROM roles WHERE key = 'platform_admin' LIMIT 1`
|
|
199
|
+
);
|
|
200
|
+
if (seeded?.key && adminRole?.key) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const permission of DEFAULT_PERMISSIONS) {
|
|
204
|
+
await this.run(
|
|
205
|
+
`INSERT OR IGNORE INTO permissions (id, key, resource, action, scope, description, created_at)
|
|
206
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
207
|
+
[randomUUID(), permission.key, permission.resource, permission.action, permission.scope, permission.description, createdAt]
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
for (const role of DEFAULT_ROLES) {
|
|
211
|
+
await this.run(
|
|
212
|
+
`INSERT OR IGNORE INTO roles (id, key, description, created_at)
|
|
213
|
+
VALUES (?, ?, ?, ?)`,
|
|
214
|
+
[randomUUID(), role.key, role.description, createdAt]
|
|
215
|
+
);
|
|
216
|
+
const roleRow = await this.first(`SELECT id FROM roles WHERE key = ?`, [role.key]);
|
|
217
|
+
if (!roleRow) continue;
|
|
218
|
+
for (const permissionKey of role.permissions) {
|
|
219
|
+
const permissionRow = await this.first(`SELECT id FROM permissions WHERE key = ?`, [permissionKey]);
|
|
220
|
+
if (permissionRow) {
|
|
221
|
+
await this.run(
|
|
222
|
+
`INSERT OR IGNORE INTO role_permissions (role_id, permission_id, created_at)
|
|
223
|
+
VALUES (?, ?, ?)`,
|
|
224
|
+
[roleRow.id, permissionRow.id, createdAt]
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async seedConfiguredServices() {
|
|
231
|
+
if (!this.config.webServiceSecret) return;
|
|
232
|
+
await this.upsertServiceCredential({
|
|
233
|
+
serviceId: this.config.webServiceId,
|
|
234
|
+
name: "Trusted web tier",
|
|
235
|
+
secret: this.config.webServiceSecret,
|
|
236
|
+
roles: ["market_admin"],
|
|
237
|
+
permissions: ["services:impersonate:global"]
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async loadUser(userId) {
|
|
241
|
+
return this.first(`SELECT * FROM users WHERE id = ?`, [userId]);
|
|
242
|
+
}
|
|
243
|
+
async loadIdentityByProvider(provider, providerSubject) {
|
|
244
|
+
return this.first(
|
|
245
|
+
`SELECT * FROM user_identities WHERE provider = ? AND provider_subject = ?`,
|
|
246
|
+
[provider, providerSubject]
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
async loadUserByVerifiedEmail(email) {
|
|
250
|
+
return this.first(
|
|
251
|
+
`SELECT * FROM users WHERE LOWER(email) = LOWER(?) AND status = 'active' LIMIT 1`,
|
|
252
|
+
[email]
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
async rolesForUser(userId) {
|
|
256
|
+
const rows = await this.all(
|
|
257
|
+
`SELECT roles.key AS key
|
|
258
|
+
FROM user_role_bindings
|
|
259
|
+
INNER JOIN roles ON roles.id = user_role_bindings.role_id
|
|
260
|
+
WHERE user_role_bindings.user_id = ?`,
|
|
261
|
+
[userId]
|
|
262
|
+
);
|
|
263
|
+
return rows.map((row) => row.key);
|
|
264
|
+
}
|
|
265
|
+
async permissionsForUser(userId) {
|
|
266
|
+
const rows = await this.all(
|
|
267
|
+
`SELECT DISTINCT permissions.key AS key
|
|
268
|
+
FROM user_role_bindings
|
|
269
|
+
INNER JOIN role_permissions ON role_permissions.role_id = user_role_bindings.role_id
|
|
270
|
+
INNER JOIN permissions ON permissions.id = role_permissions.permission_id
|
|
271
|
+
WHERE user_role_bindings.user_id = ?`,
|
|
272
|
+
[userId]
|
|
273
|
+
);
|
|
274
|
+
return rows.map((row) => row.key);
|
|
275
|
+
}
|
|
276
|
+
async permissionsForRoles(roleKeys) {
|
|
277
|
+
if (roleKeys.length === 0) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
const placeholders = roleKeys.map(() => "?").join(", ");
|
|
281
|
+
const rows = await this.all(
|
|
282
|
+
`SELECT DISTINCT permissions.key AS key
|
|
283
|
+
FROM roles
|
|
284
|
+
INNER JOIN role_permissions ON role_permissions.role_id = roles.id
|
|
285
|
+
INNER JOIN permissions ON permissions.id = role_permissions.permission_id
|
|
286
|
+
WHERE roles.key IN (${placeholders})`,
|
|
287
|
+
roleKeys
|
|
288
|
+
);
|
|
289
|
+
return rows.map((row) => row.key);
|
|
290
|
+
}
|
|
291
|
+
scopesForPrincipal(permissions) {
|
|
292
|
+
const scopes = /* @__PURE__ */ new Set(["auth:me"]);
|
|
293
|
+
if (permissions.includes("*:*:*") || permissions.includes("sdk:execute:global")) scopes.add("sdk");
|
|
294
|
+
if (permissions.includes("*:*:*") || permissions.includes("agent:execute:global")) scopes.add("agent");
|
|
295
|
+
if (permissions.includes("*:*:*") || permissions.includes("operations:execute:global")) scopes.add("operations");
|
|
296
|
+
return [...scopes];
|
|
297
|
+
}
|
|
298
|
+
async principalForUser(userId) {
|
|
299
|
+
const user = await this.loadUser(userId);
|
|
300
|
+
if (!user) {
|
|
301
|
+
throw new Error(`Unknown user "${userId}".`);
|
|
302
|
+
}
|
|
303
|
+
const roles = await this.rolesForUser(userId);
|
|
304
|
+
const permissions = await this.permissionsForUser(userId);
|
|
305
|
+
return {
|
|
306
|
+
userId,
|
|
307
|
+
principal: {
|
|
308
|
+
id: user.id,
|
|
309
|
+
displayName: user.display_name ?? void 0,
|
|
310
|
+
roles,
|
|
311
|
+
permissions,
|
|
312
|
+
scopes: this.scopesForPrincipal(permissions),
|
|
313
|
+
metadata: {
|
|
314
|
+
...parseJson(user.metadata_json, {}),
|
|
315
|
+
username: user.username ?? void 0
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async assignRole(userId, roleKey) {
|
|
321
|
+
const role = await this.first(`SELECT id FROM roles WHERE key = ?`, [roleKey]);
|
|
322
|
+
if (!role) return;
|
|
323
|
+
await this.run(
|
|
324
|
+
`INSERT OR IGNORE INTO user_role_bindings (id, user_id, role_id, created_at)
|
|
325
|
+
VALUES (?, ?, ?, ?)`,
|
|
326
|
+
[randomUUID(), userId, role.id, isoNow()]
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
async replaceRoles(userId, roleKeys) {
|
|
330
|
+
await this.run(`DELETE FROM user_role_bindings WHERE user_id = ?`, [userId]);
|
|
331
|
+
for (const roleKey of roleKeys) {
|
|
332
|
+
await this.assignRole(userId, roleKey);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async bootstrapRolesForUser(userId, identity) {
|
|
336
|
+
await this.assignRole(userId, "member");
|
|
337
|
+
if ((await this.rolesForUser(userId)).includes("platform_admin")) return;
|
|
338
|
+
const allowlist = this.config.bootstrapAdminAllowlist;
|
|
339
|
+
const email = identity.email?.trim().toLowerCase() ?? "";
|
|
340
|
+
const providerSubject = `${identity.provider}:${identity.providerSubject}`;
|
|
341
|
+
if (allowlist.includes(email) || allowlist.includes(providerSubject)) {
|
|
342
|
+
await this.assignRole(userId, "platform_admin");
|
|
343
|
+
await this.writeAuditEvent({
|
|
344
|
+
actorType: "system",
|
|
345
|
+
actorId: null,
|
|
346
|
+
eventType: "auth.bootstrap_admin",
|
|
347
|
+
targetType: "user",
|
|
348
|
+
targetId: userId,
|
|
349
|
+
data: { matched: allowlist.includes(providerSubject) ? providerSubject : email }
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async writeAuditEvent(input) {
|
|
354
|
+
await this.run(
|
|
355
|
+
`INSERT INTO audit_events (id, actor_type, actor_id, event_type, target_type, target_id, data_json, created_at)
|
|
356
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
357
|
+
[
|
|
358
|
+
randomUUID(),
|
|
359
|
+
input.actorType,
|
|
360
|
+
input.actorId,
|
|
361
|
+
input.eventType,
|
|
362
|
+
input.targetType,
|
|
363
|
+
input.targetId,
|
|
364
|
+
JSON.stringify(input.data ?? {}),
|
|
365
|
+
isoNow()
|
|
366
|
+
]
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
userMetadata(identity, existingUsername = null) {
|
|
370
|
+
const profile = identity.profile ?? {};
|
|
371
|
+
return {
|
|
372
|
+
emailVerified: identity.emailVerified ?? false,
|
|
373
|
+
authProvider: identity.provider,
|
|
374
|
+
username: identity.username ?? existingUsername,
|
|
375
|
+
firstName: typeof profile.firstName === "string" ? profile.firstName : null,
|
|
376
|
+
lastName: typeof profile.lastName === "string" ? profile.lastName : null
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async syncUser(identity) {
|
|
380
|
+
await this.ensureInitialized();
|
|
381
|
+
const nowIso = isoNow();
|
|
382
|
+
const existingIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
|
|
383
|
+
let userId = existingIdentity?.user_id;
|
|
384
|
+
if (!userId) {
|
|
385
|
+
const linkedUser = identity.email && identity.emailVerified ? await this.loadUserByVerifiedEmail(identity.email) : null;
|
|
386
|
+
userId = linkedUser?.id ?? randomUUID();
|
|
387
|
+
if (linkedUser) {
|
|
388
|
+
await this.run(
|
|
389
|
+
`UPDATE users
|
|
390
|
+
SET email = COALESCE(?, email),
|
|
391
|
+
username = COALESCE(username, ?),
|
|
392
|
+
display_name = COALESCE(?, display_name),
|
|
393
|
+
metadata_json = ?,
|
|
394
|
+
updated_at = ?
|
|
395
|
+
WHERE id = ?`,
|
|
396
|
+
[
|
|
397
|
+
identity.email ?? null,
|
|
398
|
+
identity.username ?? null,
|
|
399
|
+
identity.displayName ?? null,
|
|
400
|
+
JSON.stringify(this.userMetadata(identity, linkedUser.username ?? null)),
|
|
401
|
+
nowIso,
|
|
402
|
+
userId
|
|
403
|
+
]
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
await this.run(
|
|
407
|
+
`INSERT INTO users (id, email, username, display_name, status, metadata_json, created_at, updated_at)
|
|
408
|
+
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`,
|
|
409
|
+
[
|
|
410
|
+
userId,
|
|
411
|
+
identity.email ?? null,
|
|
412
|
+
identity.username ?? null,
|
|
413
|
+
identity.displayName ?? null,
|
|
414
|
+
JSON.stringify(this.userMetadata(identity)),
|
|
415
|
+
nowIso,
|
|
416
|
+
nowIso
|
|
417
|
+
]
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
await this.run(
|
|
421
|
+
`INSERT INTO user_identities (id, user_id, provider, provider_subject, email, email_verified, profile_json, created_at, updated_at)
|
|
422
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
423
|
+
[
|
|
424
|
+
randomUUID(),
|
|
425
|
+
userId,
|
|
426
|
+
identity.provider,
|
|
427
|
+
identity.providerSubject,
|
|
428
|
+
identity.email ?? null,
|
|
429
|
+
identity.emailVerified ? 1 : 0,
|
|
430
|
+
JSON.stringify(identity.profile ?? {}),
|
|
431
|
+
nowIso,
|
|
432
|
+
nowIso
|
|
433
|
+
]
|
|
434
|
+
);
|
|
435
|
+
} else {
|
|
436
|
+
await this.run(
|
|
437
|
+
`UPDATE users
|
|
438
|
+
SET email = COALESCE(?, email),
|
|
439
|
+
username = COALESCE(username, ?),
|
|
440
|
+
display_name = COALESCE(?, display_name),
|
|
441
|
+
metadata_json = ?,
|
|
442
|
+
updated_at = ?
|
|
443
|
+
WHERE id = ?`,
|
|
444
|
+
[
|
|
445
|
+
identity.email ?? null,
|
|
446
|
+
identity.username ?? null,
|
|
447
|
+
identity.displayName ?? null,
|
|
448
|
+
JSON.stringify(this.userMetadata(identity)),
|
|
449
|
+
nowIso,
|
|
450
|
+
userId
|
|
451
|
+
]
|
|
452
|
+
);
|
|
453
|
+
await this.run(
|
|
454
|
+
`UPDATE user_identities
|
|
455
|
+
SET email = ?, email_verified = ?, profile_json = ?, updated_at = ?
|
|
456
|
+
WHERE provider = ? AND provider_subject = ?`,
|
|
457
|
+
[
|
|
458
|
+
identity.email ?? null,
|
|
459
|
+
identity.emailVerified ? 1 : 0,
|
|
460
|
+
JSON.stringify(identity.profile ?? {}),
|
|
461
|
+
nowIso,
|
|
462
|
+
identity.provider,
|
|
463
|
+
identity.providerSubject
|
|
464
|
+
]
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
await this.bootstrapRolesForUser(userId, identity);
|
|
468
|
+
await this.writeAuditEvent({
|
|
469
|
+
actorType: "service",
|
|
470
|
+
actorId: this.config.webServiceId,
|
|
471
|
+
eventType: "auth.user_synced",
|
|
472
|
+
targetType: "user",
|
|
473
|
+
targetId: userId,
|
|
474
|
+
data: { provider: identity.provider }
|
|
475
|
+
});
|
|
476
|
+
const principal = await this.principalForUser(userId);
|
|
477
|
+
const syncedIdentity = await this.loadIdentityByProvider(identity.provider, identity.providerSubject);
|
|
478
|
+
return {
|
|
479
|
+
...principal,
|
|
480
|
+
identityId: syncedIdentity?.id ?? null
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async createUser(input) {
|
|
484
|
+
await this.ensureInitialized();
|
|
485
|
+
const timestamp = isoNow();
|
|
486
|
+
const userId = randomUUID();
|
|
487
|
+
await this.run(
|
|
488
|
+
`INSERT INTO users (id, email, username, display_name, status, metadata_json, created_at, updated_at)
|
|
489
|
+
VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`,
|
|
490
|
+
[
|
|
491
|
+
userId,
|
|
492
|
+
input.email?.trim() || null,
|
|
493
|
+
input.username?.trim().toLowerCase() || null,
|
|
494
|
+
input.displayName?.trim() || null,
|
|
495
|
+
JSON.stringify(input.metadata ?? {}),
|
|
496
|
+
timestamp,
|
|
497
|
+
timestamp
|
|
498
|
+
]
|
|
499
|
+
);
|
|
500
|
+
await this.assignRole(userId, "member");
|
|
501
|
+
await this.writeAuditEvent({
|
|
502
|
+
actorType: "service",
|
|
503
|
+
actorId: this.config.webServiceId,
|
|
504
|
+
eventType: "auth.user_created",
|
|
505
|
+
targetType: "user",
|
|
506
|
+
targetId: userId,
|
|
507
|
+
data: { source: "admin" }
|
|
508
|
+
});
|
|
509
|
+
return this.principalForUser(userId);
|
|
510
|
+
}
|
|
511
|
+
async setUserRoles(userId, roles) {
|
|
512
|
+
await this.ensureInitialized();
|
|
513
|
+
const requestedRoles = [...new Set(roles.map((role) => role.trim()).filter(Boolean))];
|
|
514
|
+
await this.replaceRoles(userId, requestedRoles.length > 0 ? requestedRoles : ["member"]);
|
|
515
|
+
await this.writeAuditEvent({
|
|
516
|
+
actorType: "service",
|
|
517
|
+
actorId: this.config.webServiceId,
|
|
518
|
+
eventType: "auth.user_roles_set",
|
|
519
|
+
targetType: "user",
|
|
520
|
+
targetId: userId,
|
|
521
|
+
data: { roles: requestedRoles }
|
|
522
|
+
});
|
|
523
|
+
return this.principalForUser(userId);
|
|
524
|
+
}
|
|
525
|
+
async startDeviceFlow(request) {
|
|
526
|
+
await this.ensureInitialized();
|
|
527
|
+
const current = now();
|
|
528
|
+
const expiresAt = addSeconds(current, this.config.deviceCodeTtlSeconds);
|
|
529
|
+
const deviceCode = nextOpaqueToken("device");
|
|
530
|
+
const userCode = `${Math.random().toString(36).slice(2, 6).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`;
|
|
531
|
+
await this.run(
|
|
532
|
+
`INSERT INTO device_codes (id, device_code, user_code, requested_scopes_json, expires_at, interval_seconds, status, user_id, created_at, updated_at)
|
|
533
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, ?)`,
|
|
534
|
+
[
|
|
535
|
+
randomUUID(),
|
|
536
|
+
deviceCode,
|
|
537
|
+
userCode,
|
|
538
|
+
JSON.stringify(request.scopes?.length ? request.scopes : ["auth:me"]),
|
|
539
|
+
expiresAt.toISOString(),
|
|
540
|
+
this.config.deviceCodePollIntervalSeconds,
|
|
541
|
+
current.toISOString(),
|
|
542
|
+
current.toISOString()
|
|
543
|
+
]
|
|
544
|
+
);
|
|
545
|
+
return {
|
|
546
|
+
ok: true,
|
|
547
|
+
deviceCode,
|
|
548
|
+
userCode,
|
|
549
|
+
verificationUri: `${this.config.baseUrl}/auth/device/approve`,
|
|
550
|
+
verificationUriComplete: `${this.config.baseUrl}/auth/device/approve?user_code=${encodeURIComponent(userCode)}`,
|
|
551
|
+
intervalSeconds: this.config.deviceCodePollIntervalSeconds,
|
|
552
|
+
expiresAt: expiresAt.toISOString(),
|
|
553
|
+
expiresInSeconds: this.config.deviceCodeTtlSeconds
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async approveDeviceFlow(request) {
|
|
557
|
+
await this.ensureInitialized();
|
|
558
|
+
const row = await this.first(`SELECT * FROM device_codes WHERE user_code = ?`, [request.userCode]);
|
|
559
|
+
if (!row || new Date(row.expires_at).getTime() <= Date.now()) {
|
|
560
|
+
throw new Error("Device code approval failed because the user code is unknown or expired.");
|
|
561
|
+
}
|
|
562
|
+
let userId = request.principalId;
|
|
563
|
+
if (!await this.loadUser(userId)) {
|
|
564
|
+
const createdAt = isoNow();
|
|
565
|
+
await this.run(
|
|
566
|
+
`INSERT INTO users (id, email, display_name, status, metadata_json, created_at, updated_at)
|
|
567
|
+
VALUES (?, NULL, ?, 'active', ?, ?, ?)`,
|
|
568
|
+
[userId, request.displayName ?? null, JSON.stringify(request.metadata ?? {}), createdAt, createdAt]
|
|
569
|
+
);
|
|
570
|
+
await this.assignRole(userId, "member");
|
|
571
|
+
}
|
|
572
|
+
await this.run(`UPDATE device_codes SET status = 'approved', user_id = ?, updated_at = ? WHERE id = ?`, [userId, isoNow(), row.id]);
|
|
573
|
+
await this.writeAuditEvent({
|
|
574
|
+
actorType: "user",
|
|
575
|
+
actorId: userId,
|
|
576
|
+
eventType: "auth.device_approved",
|
|
577
|
+
targetType: "device_code",
|
|
578
|
+
targetId: row.id
|
|
579
|
+
});
|
|
580
|
+
return { ok: true };
|
|
581
|
+
}
|
|
582
|
+
async pollDeviceFlow(request) {
|
|
583
|
+
await this.ensureInitialized();
|
|
584
|
+
const row = await this.first(`SELECT * FROM device_codes WHERE device_code = ?`, [request.deviceCode]);
|
|
585
|
+
if (!row) {
|
|
586
|
+
return { ok: false, status: "invalid", error: "Unknown device code." };
|
|
587
|
+
}
|
|
588
|
+
if (new Date(row.expires_at).getTime() <= Date.now()) {
|
|
589
|
+
return { ok: false, status: "expired", error: "Device code expired." };
|
|
590
|
+
}
|
|
591
|
+
if (row.status === "pending" || !row.user_id) {
|
|
592
|
+
return { ok: true, status: "pending", intervalSeconds: row.interval_seconds };
|
|
593
|
+
}
|
|
594
|
+
if (row.status === "used") {
|
|
595
|
+
return { ok: false, status: "already_used", error: "Device code already used." };
|
|
596
|
+
}
|
|
597
|
+
await this.run(`UPDATE device_codes SET status = 'used', updated_at = ? WHERE id = ?`, [isoNow(), row.id]);
|
|
598
|
+
const principalRecord = await this.principalForUser(row.user_id);
|
|
599
|
+
const refreshToken = nextOpaqueToken("refresh");
|
|
600
|
+
const sessionId = randomUUID();
|
|
601
|
+
const refreshTokenHash = stableHash(refreshToken, this.config.authSecret);
|
|
602
|
+
const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds);
|
|
603
|
+
const refreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds);
|
|
604
|
+
await this.run(
|
|
605
|
+
`INSERT INTO auth_sessions (id, user_id, session_type, refresh_token_hash, scopes_json, expires_at, revoked_at, data_json, created_at, updated_at)
|
|
606
|
+
VALUES (?, ?, 'device', ?, ?, ?, NULL, ?, ?, ?)`,
|
|
607
|
+
[
|
|
608
|
+
sessionId,
|
|
609
|
+
row.user_id,
|
|
610
|
+
refreshTokenHash,
|
|
611
|
+
row.requested_scopes_json,
|
|
612
|
+
refreshExpiresAt.toISOString(),
|
|
613
|
+
JSON.stringify({ deviceCodeId: row.id }),
|
|
614
|
+
isoNow(),
|
|
615
|
+
isoNow()
|
|
616
|
+
]
|
|
617
|
+
);
|
|
618
|
+
const requestedScopes = parseJson(row.requested_scopes_json, principalRecord.principal.scopes);
|
|
619
|
+
const accessToken = createAccessToken({
|
|
620
|
+
sub: principalRecord.principal.id,
|
|
621
|
+
displayName: principalRecord.principal.displayName,
|
|
622
|
+
scopes: requestedScopes,
|
|
623
|
+
roles: principalRecord.principal.roles,
|
|
624
|
+
permissions: principalRecord.principal.permissions,
|
|
625
|
+
metadata: principalRecord.principal.metadata,
|
|
626
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
627
|
+
exp: Math.floor(expiresAt.getTime() / 1e3),
|
|
628
|
+
iss: this.config.issuer,
|
|
629
|
+
jti: randomUUID(),
|
|
630
|
+
tokenType: "access"
|
|
631
|
+
}, this.config.authSecret);
|
|
632
|
+
return {
|
|
633
|
+
ok: true,
|
|
634
|
+
status: "approved",
|
|
635
|
+
accessToken,
|
|
636
|
+
refreshToken,
|
|
637
|
+
tokenType: "Bearer",
|
|
638
|
+
expiresAt: expiresAt.toISOString(),
|
|
639
|
+
expiresInSeconds: this.config.accessTokenTtlSeconds,
|
|
640
|
+
principal: {
|
|
641
|
+
...principalRecord.principal,
|
|
642
|
+
scopes: requestedScopes
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async refreshAccessToken(request) {
|
|
647
|
+
await this.ensureInitialized();
|
|
648
|
+
const refreshHash = stableHash(request.refreshToken, this.config.authSecret);
|
|
649
|
+
const row = await this.first(
|
|
650
|
+
`SELECT * FROM auth_sessions WHERE refresh_token_hash = ? AND revoked_at IS NULL`,
|
|
651
|
+
[refreshHash]
|
|
652
|
+
);
|
|
653
|
+
if (!row || new Date(row.expires_at).getTime() <= Date.now()) {
|
|
654
|
+
throw new Error("Refresh token is invalid or expired.");
|
|
655
|
+
}
|
|
656
|
+
const principalRecord = await this.principalForUser(row.user_id);
|
|
657
|
+
const nextRefreshToken = nextOpaqueToken("refresh");
|
|
658
|
+
const nextRefreshHash = stableHash(nextRefreshToken, this.config.authSecret);
|
|
659
|
+
const nextRefreshExpiresAt = addSeconds(now(), this.config.refreshTokenTtlSeconds);
|
|
660
|
+
await this.run(
|
|
661
|
+
`UPDATE auth_sessions SET refresh_token_hash = ?, expires_at = ?, updated_at = ? WHERE id = ?`,
|
|
662
|
+
[nextRefreshHash, nextRefreshExpiresAt.toISOString(), isoNow(), row.id]
|
|
663
|
+
);
|
|
664
|
+
const requestedScopes = parseJson(row.scopes_json, principalRecord.principal.scopes);
|
|
665
|
+
const expiresAt = addSeconds(now(), this.config.accessTokenTtlSeconds);
|
|
666
|
+
const accessToken = createAccessToken({
|
|
667
|
+
sub: principalRecord.principal.id,
|
|
668
|
+
displayName: principalRecord.principal.displayName,
|
|
669
|
+
scopes: requestedScopes,
|
|
670
|
+
roles: principalRecord.principal.roles,
|
|
671
|
+
permissions: principalRecord.principal.permissions,
|
|
672
|
+
metadata: principalRecord.principal.metadata,
|
|
673
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
674
|
+
exp: Math.floor(expiresAt.getTime() / 1e3),
|
|
675
|
+
iss: this.config.issuer,
|
|
676
|
+
jti: randomUUID(),
|
|
677
|
+
tokenType: "access"
|
|
678
|
+
}, this.config.authSecret);
|
|
679
|
+
return {
|
|
680
|
+
ok: true,
|
|
681
|
+
accessToken,
|
|
682
|
+
refreshToken: nextRefreshToken,
|
|
683
|
+
tokenType: "Bearer",
|
|
684
|
+
expiresAt: expiresAt.toISOString(),
|
|
685
|
+
expiresInSeconds: this.config.accessTokenTtlSeconds,
|
|
686
|
+
principal: {
|
|
687
|
+
...principalRecord.principal,
|
|
688
|
+
scopes: requestedScopes
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
async createPersonalAccessToken(userId, input) {
|
|
693
|
+
await this.ensureInitialized();
|
|
694
|
+
const nowIso = isoNow();
|
|
695
|
+
const token = nextOpaqueToken("pat");
|
|
696
|
+
const id = randomUUID();
|
|
697
|
+
const tokenHash = stableHash(token, this.config.authSecret);
|
|
698
|
+
const prefix = token.slice(0, 12);
|
|
699
|
+
await this.run(
|
|
700
|
+
`INSERT INTO api_tokens (id, user_id, kind, name, token_prefix, token_hash, scopes_json, expires_at, last_used_at, revoked_at, metadata_json, created_at, updated_at)
|
|
701
|
+
VALUES (?, ?, 'personal_access_token', ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?)`,
|
|
702
|
+
[
|
|
703
|
+
id,
|
|
704
|
+
userId,
|
|
705
|
+
input.name,
|
|
706
|
+
prefix,
|
|
707
|
+
tokenHash,
|
|
708
|
+
JSON.stringify(input.scopes?.length ? input.scopes : ["auth:me"]),
|
|
709
|
+
input.expiresAt ?? null,
|
|
710
|
+
JSON.stringify({}),
|
|
711
|
+
nowIso,
|
|
712
|
+
nowIso
|
|
713
|
+
]
|
|
714
|
+
);
|
|
715
|
+
await this.writeAuditEvent({
|
|
716
|
+
actorType: "user",
|
|
717
|
+
actorId: userId,
|
|
718
|
+
eventType: "auth.pat_created",
|
|
719
|
+
targetType: "api_token",
|
|
720
|
+
targetId: id,
|
|
721
|
+
data: { name: input.name }
|
|
722
|
+
});
|
|
723
|
+
return { id, token, prefix, name: input.name, expiresAt: input.expiresAt ?? null };
|
|
724
|
+
}
|
|
725
|
+
async listPersonalAccessTokens(userId) {
|
|
726
|
+
await this.ensureInitialized();
|
|
727
|
+
return this.all(
|
|
728
|
+
`SELECT id, name, token_prefix, expires_at, last_used_at, revoked_at, created_at
|
|
729
|
+
FROM api_tokens
|
|
730
|
+
WHERE user_id = ?
|
|
731
|
+
ORDER BY created_at DESC`,
|
|
732
|
+
[userId]
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
async revokePersonalAccessToken(userId, tokenId) {
|
|
736
|
+
await this.ensureInitialized();
|
|
737
|
+
await this.run(`UPDATE api_tokens SET revoked_at = ? WHERE id = ? AND user_id = ?`, [isoNow(), tokenId, userId]);
|
|
738
|
+
await this.writeAuditEvent({
|
|
739
|
+
actorType: "user",
|
|
740
|
+
actorId: userId,
|
|
741
|
+
eventType: "auth.pat_revoked",
|
|
742
|
+
targetType: "api_token",
|
|
743
|
+
targetId: tokenId
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
async upsertServiceCredential(input) {
|
|
747
|
+
const nowIso = isoNow();
|
|
748
|
+
const existing = await this.first(`SELECT id FROM service_credentials WHERE service_id = ?`, [input.serviceId]);
|
|
749
|
+
const secretHash = stableHash(input.secret, this.config.authSecret);
|
|
750
|
+
if (existing) {
|
|
751
|
+
await this.run(
|
|
752
|
+
`UPDATE service_credentials
|
|
753
|
+
SET name = ?, secret_hash = ?, roles_json = ?, permissions_json = ?, revoked_at = NULL, updated_at = ?
|
|
754
|
+
WHERE id = ?`,
|
|
755
|
+
[input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, existing.id]
|
|
756
|
+
);
|
|
757
|
+
return existing.id;
|
|
758
|
+
}
|
|
759
|
+
const id = randomUUID();
|
|
760
|
+
await this.run(
|
|
761
|
+
`INSERT INTO service_credentials (id, service_id, name, secret_hash, roles_json, permissions_json, revoked_at, last_used_at, created_at, updated_at)
|
|
762
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`,
|
|
763
|
+
[id, input.serviceId, input.name, secretHash, JSON.stringify(input.roles ?? []), JSON.stringify(input.permissions ?? []), nowIso, nowIso]
|
|
764
|
+
);
|
|
765
|
+
return id;
|
|
766
|
+
}
|
|
767
|
+
async createServiceCredential(input) {
|
|
768
|
+
await this.ensureInitialized();
|
|
769
|
+
const secret = nextOpaqueToken("svc");
|
|
770
|
+
const id = await this.upsertServiceCredential({ ...input, secret });
|
|
771
|
+
return { id, serviceId: input.serviceId, secret };
|
|
772
|
+
}
|
|
773
|
+
async rotateServiceCredential(serviceId) {
|
|
774
|
+
await this.ensureInitialized();
|
|
775
|
+
const row = await this.first(
|
|
776
|
+
`SELECT name, roles_json, permissions_json FROM service_credentials WHERE service_id = ? AND revoked_at IS NULL`,
|
|
777
|
+
[serviceId]
|
|
778
|
+
);
|
|
779
|
+
if (!row) {
|
|
780
|
+
throw new Error(`Unknown active service credential "${serviceId}".`);
|
|
781
|
+
}
|
|
782
|
+
return this.createServiceCredential({
|
|
783
|
+
serviceId,
|
|
784
|
+
name: row.name,
|
|
785
|
+
roles: parseJson(row.roles_json, []),
|
|
786
|
+
permissions: parseJson(row.permissions_json, [])
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async authenticateBearerToken(token) {
|
|
790
|
+
await this.ensureInitialized();
|
|
791
|
+
const patHash = stableHash(token, this.config.authSecret);
|
|
792
|
+
const pat = await this.first(
|
|
793
|
+
`SELECT id, user_id, name, scopes_json, expires_at, revoked_at
|
|
794
|
+
FROM api_tokens
|
|
795
|
+
WHERE token_hash = ?`,
|
|
796
|
+
[patHash]
|
|
797
|
+
);
|
|
798
|
+
if (pat && !pat.revoked_at && (!pat.expires_at || new Date(pat.expires_at).getTime() > Date.now())) {
|
|
799
|
+
await this.run(`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`, [isoNow(), pat.id]);
|
|
800
|
+
const principal = (await this.principalForUser(pat.user_id)).principal;
|
|
801
|
+
return {
|
|
802
|
+
principal: { ...principal, scopes: parseJson(pat.scopes_json, principal.scopes) },
|
|
803
|
+
credential: { type: "personal_access_token", id: pat.id, label: pat.name }
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const payload = verifyAccessToken(token, this.config.authSecret);
|
|
807
|
+
if (!payload) return null;
|
|
808
|
+
return {
|
|
809
|
+
principal: principalFromAccessTokenPayload(payload),
|
|
810
|
+
credential: {
|
|
811
|
+
type: payload.tokenType === "service" ? "service_token" : "access_token",
|
|
812
|
+
id: payload.jti,
|
|
813
|
+
label: payload.tokenType
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
async authenticateService(serviceId, secret) {
|
|
818
|
+
await this.ensureInitialized();
|
|
819
|
+
const row = await this.first(
|
|
820
|
+
`SELECT id, name, secret_hash, roles_json, permissions_json, revoked_at
|
|
821
|
+
FROM service_credentials
|
|
822
|
+
WHERE service_id = ?`,
|
|
823
|
+
[serviceId]
|
|
824
|
+
);
|
|
825
|
+
if (!row || row.revoked_at) return null;
|
|
826
|
+
const incomingHash = stableHash(secret, this.config.authSecret);
|
|
827
|
+
if (!equalHash(row.secret_hash, incomingHash)) return null;
|
|
828
|
+
await this.run(`UPDATE service_credentials SET last_used_at = ?, updated_at = ? WHERE id = ?`, [isoNow(), isoNow(), row.id]);
|
|
829
|
+
const roles = parseJson(row.roles_json, []);
|
|
830
|
+
const permissions = [
|
|
831
|
+
.../* @__PURE__ */ new Set([
|
|
832
|
+
...await this.permissionsForRoles(roles),
|
|
833
|
+
...parseJson(row.permissions_json, [])
|
|
834
|
+
])
|
|
835
|
+
];
|
|
836
|
+
return {
|
|
837
|
+
principal: {
|
|
838
|
+
id: serviceId,
|
|
839
|
+
displayName: row.name,
|
|
840
|
+
roles,
|
|
841
|
+
permissions,
|
|
842
|
+
scopes: this.scopesForPrincipal(permissions),
|
|
843
|
+
metadata: { serviceId }
|
|
844
|
+
},
|
|
845
|
+
credential: { type: "service_secret", id: row.id, label: row.name }
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
async exchangeTrustedUserAssertion(claims) {
|
|
849
|
+
await this.ensureInitialized();
|
|
850
|
+
const principalRecord = await this.principalForUser(claims.userId);
|
|
851
|
+
const expiresAt = addSeconds(now(), this.config.webExchangeTtlSeconds);
|
|
852
|
+
const accessToken = createAccessToken({
|
|
853
|
+
sub: principalRecord.principal.id,
|
|
854
|
+
displayName: principalRecord.principal.displayName,
|
|
855
|
+
scopes: principalRecord.principal.scopes,
|
|
856
|
+
roles: principalRecord.principal.roles,
|
|
857
|
+
permissions: principalRecord.principal.permissions,
|
|
858
|
+
metadata: {
|
|
859
|
+
...principalRecord.principal.metadata,
|
|
860
|
+
actingSessionId: claims.sessionId,
|
|
861
|
+
identityId: claims.identityId,
|
|
862
|
+
teamId: claims.teamId ?? null,
|
|
863
|
+
projectId: claims.projectId ?? null,
|
|
864
|
+
membershipId: claims.membershipId ?? null,
|
|
865
|
+
teamRoles: [...new Set((claims.teamRoles ?? []).filter((entry) => typeof entry === "string" && entry.trim()))],
|
|
866
|
+
teamCapabilities: [...new Set((claims.teamCapabilities ?? []).filter((entry) => typeof entry === "string" && entry.trim()))],
|
|
867
|
+
authTime: claims.authTime
|
|
868
|
+
},
|
|
869
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
870
|
+
exp: Math.floor(expiresAt.getTime() / 1e3),
|
|
871
|
+
iss: this.config.issuer,
|
|
872
|
+
jti: randomUUID(),
|
|
873
|
+
tokenType: "access"
|
|
874
|
+
}, this.config.authSecret);
|
|
875
|
+
await this.writeAuditEvent({
|
|
876
|
+
actorType: "service",
|
|
877
|
+
actorId: this.config.webServiceId,
|
|
878
|
+
eventType: "auth.web_exchange",
|
|
879
|
+
targetType: "user",
|
|
880
|
+
targetId: claims.userId,
|
|
881
|
+
data: { sessionId: claims.sessionId }
|
|
882
|
+
});
|
|
883
|
+
return {
|
|
884
|
+
ok: true,
|
|
885
|
+
accessToken,
|
|
886
|
+
tokenType: "Bearer",
|
|
887
|
+
expiresAt: expiresAt.toISOString(),
|
|
888
|
+
expiresInSeconds: this.config.webExchangeTtlSeconds,
|
|
889
|
+
principal: principalRecord.principal
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
export {
|
|
894
|
+
D1AuthStore
|
|
895
|
+
};
|