okengine 0.2.2 → 0.2.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.
- package/package.json +1 -1
- package/src/cli/dev.ts +68 -6
- package/src/cli/doctor.ts +3 -18
- package/src/cli/index.ts +4 -1
- package/src/cli/json-out.test.ts +1 -1
- package/src/cli/load-config.images.test.ts +75 -0
- package/src/cli/load-config.ts +67 -2
- package/src/cli/ports.test.ts +51 -0
- package/src/cli/ports.ts +81 -0
- package/src/cli/registry.help.test.ts +18 -0
- package/src/cli/registry.ts +0 -4
- package/src/cli/safe-defaults.test.ts +3 -3
- package/src/console/server/app.ts +2 -1
- package/src/console/server/flows.ts +1 -0
- package/src/console/server/index.ts +5 -0
- package/src/console/server/operator-db.test.ts +75 -0
- package/src/console/server/operator-db.ts +324 -0
- package/src/console/server/serve.ts +39 -0
- package/src/console/server/state.ts +13 -1
- package/src/console/ui/dist/assets/{index-B71Yl_SS.js → index-Bnf_3Hei.js} +3 -3
- package/src/console/ui/dist/assets/{panel-overview-Bd48d9km.js → panel-overview-Dt_AeXgd.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-BwsWqKeB.js → panel-runs-DGstFHeq.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-9najbZY2.js → panel-signals-DzEa2Fnt.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-OHkP2pDp.js → panel-store-BJkbNgxx.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-tn2JoY8U.js → panel-traces-BaRVO3gM.js} +1 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +3 -0
- package/src/console/ui/dist/favicon.svg +7 -0
- package/src/console/ui/dist/index.html +3 -2
- package/src/console/ui/shell/App.tsx +34 -2
- package/src/console/ui/shell/components/oke-logo.tsx +40 -0
- package/src/console/ui/shell/index.html +1 -0
- package/src/console/ui/shell/layout/Shell.tsx +2 -3
- package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +8 -0
- package/src/console/ui/shell/public/favicon.svg +7 -0
- package/src/console/ui/shell/setup/Wizard.tsx +5 -2
- package/src/docker/derive.ts +3 -1
- package/src/docker/dockerfile.integration.test.ts +1 -1
- package/src/docker/stack.integration.test.ts +1 -1
- package/src/console/ui/dist/assets/style-Cnl7WLya.css +0 -3
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable operator plane for Console — `.oke/console.sqlite` + secret file.
|
|
3
|
+
*
|
|
4
|
+
* Spec: wizard closes permanently once the first operator exists (console §2.5).
|
|
5
|
+
* Claim codes stay ephemeral; operators and the signing secret must survive restarts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
import {
|
|
12
|
+
createOperatorStore,
|
|
13
|
+
type OperatorStore,
|
|
14
|
+
} from "../../auth/operator.ts";
|
|
15
|
+
import type {
|
|
16
|
+
OperatorCredentialRow,
|
|
17
|
+
OperatorRow,
|
|
18
|
+
OperatorSsoLinkRow,
|
|
19
|
+
} from "../../auth/tables.ts";
|
|
20
|
+
import { AUTH_TABLES } from "../../auth/tables.ts";
|
|
21
|
+
|
|
22
|
+
/** Relative paths under project cwd. */
|
|
23
|
+
export const CONSOLE_OKE_DIR = ".oke";
|
|
24
|
+
export const CONSOLE_SQLITE_NAME = "console.sqlite";
|
|
25
|
+
export const CONSOLE_SECRET_NAME = "console.secret";
|
|
26
|
+
|
|
27
|
+
/** Opened Console persistence handle. */
|
|
28
|
+
export interface ConsolePersistence {
|
|
29
|
+
/** Stable signing secret. */
|
|
30
|
+
readonly secret: string;
|
|
31
|
+
/** Hydrated operator Maps. */
|
|
32
|
+
readonly operators: OperatorStore;
|
|
33
|
+
/** Persist (or update) one operator + credential + roles/sso. */
|
|
34
|
+
readonly persistOperator: (operatorId: string) => void;
|
|
35
|
+
/** Close the SQLite connection. */
|
|
36
|
+
readonly close: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure `.oke/` exists and return absolute paths.
|
|
41
|
+
*
|
|
42
|
+
* @param cwd - Project root
|
|
43
|
+
*/
|
|
44
|
+
export function consoleOkePaths(cwd: string): {
|
|
45
|
+
readonly dir: string;
|
|
46
|
+
readonly sqlite: string;
|
|
47
|
+
readonly secret: string;
|
|
48
|
+
} {
|
|
49
|
+
const dir = join(cwd, CONSOLE_OKE_DIR);
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
return {
|
|
52
|
+
dir,
|
|
53
|
+
sqlite: join(dir, CONSOLE_SQLITE_NAME),
|
|
54
|
+
secret: join(dir, CONSOLE_SECRET_NAME),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load or create a stable Console signing secret under `.oke/console.secret`.
|
|
60
|
+
*
|
|
61
|
+
* @param cwd - Project root
|
|
62
|
+
* @param envSecret - Optional `OKE_CONSOLE_SECRET` override
|
|
63
|
+
*/
|
|
64
|
+
export async function resolveConsoleSecret(
|
|
65
|
+
cwd: string,
|
|
66
|
+
envSecret?: string,
|
|
67
|
+
): Promise<string> {
|
|
68
|
+
if (envSecret !== undefined && envSecret.length > 0) return envSecret;
|
|
69
|
+
const paths = consoleOkePaths(cwd);
|
|
70
|
+
const file = Bun.file(paths.secret);
|
|
71
|
+
if (await file.exists()) {
|
|
72
|
+
const existing = (await file.text()).trim();
|
|
73
|
+
if (existing.length > 0) return existing;
|
|
74
|
+
}
|
|
75
|
+
const bytes = new Uint8Array(32);
|
|
76
|
+
crypto.getRandomValues(bytes);
|
|
77
|
+
const secret = [...bytes]
|
|
78
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
79
|
+
.join("");
|
|
80
|
+
await Bun.write(paths.secret, `${secret}\n`);
|
|
81
|
+
return secret;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Open Console operator DB, migrate schema, hydrate Maps.
|
|
86
|
+
*
|
|
87
|
+
* @param cwd - Project root
|
|
88
|
+
* @param options - Optional env secret override
|
|
89
|
+
*/
|
|
90
|
+
export async function openConsolePersistence(
|
|
91
|
+
cwd: string,
|
|
92
|
+
options: { readonly envSecret?: string } = {},
|
|
93
|
+
): Promise<ConsolePersistence> {
|
|
94
|
+
const paths = consoleOkePaths(cwd);
|
|
95
|
+
const secret = await resolveConsoleSecret(cwd, options.envSecret);
|
|
96
|
+
const db = new Database(paths.sqlite, { create: true });
|
|
97
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
98
|
+
migrateOperatorSchema(db);
|
|
99
|
+
const operators = loadOperatorStore(db);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
secret,
|
|
103
|
+
operators,
|
|
104
|
+
persistOperator(operatorId: string) {
|
|
105
|
+
persistOperator(db, operators, operatorId);
|
|
106
|
+
},
|
|
107
|
+
close() {
|
|
108
|
+
db.close();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create operator tables if missing.
|
|
115
|
+
*
|
|
116
|
+
* @param db - bun:sqlite database
|
|
117
|
+
*/
|
|
118
|
+
export function migrateOperatorSchema(db: Database): void {
|
|
119
|
+
db.exec(`
|
|
120
|
+
CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operators} (
|
|
121
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
122
|
+
email TEXT NOT NULL UNIQUE,
|
|
123
|
+
name TEXT NOT NULL,
|
|
124
|
+
status TEXT NOT NULL,
|
|
125
|
+
mfa_enabled INTEGER NOT NULL DEFAULT 0,
|
|
126
|
+
invited_by TEXT,
|
|
127
|
+
last_seen_at INTEGER
|
|
128
|
+
);
|
|
129
|
+
CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operatorCredentials} (
|
|
130
|
+
operator_id TEXT PRIMARY KEY NOT NULL,
|
|
131
|
+
password_hash TEXT NOT NULL,
|
|
132
|
+
login_enabled INTEGER NOT NULL DEFAULT 1,
|
|
133
|
+
FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
|
|
134
|
+
);
|
|
135
|
+
CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operatorSsoLinks} (
|
|
136
|
+
operator_id TEXT NOT NULL,
|
|
137
|
+
provider TEXT NOT NULL,
|
|
138
|
+
subject TEXT NOT NULL,
|
|
139
|
+
PRIMARY KEY (operator_id, provider, subject),
|
|
140
|
+
FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
|
|
141
|
+
);
|
|
142
|
+
CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.operatorRoles} (
|
|
143
|
+
operator_id TEXT NOT NULL,
|
|
144
|
+
role TEXT NOT NULL,
|
|
145
|
+
PRIMARY KEY (operator_id, role),
|
|
146
|
+
FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
|
|
147
|
+
);
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hydrate an {@link OperatorStore} from SQLite.
|
|
153
|
+
*
|
|
154
|
+
* @param db - Open database
|
|
155
|
+
*/
|
|
156
|
+
export function loadOperatorStore(db: Database): OperatorStore {
|
|
157
|
+
const store = createOperatorStore();
|
|
158
|
+
|
|
159
|
+
const opRows = db
|
|
160
|
+
.query(
|
|
161
|
+
`SELECT id, email, name, status, mfa_enabled, invited_by, last_seen_at
|
|
162
|
+
FROM ${AUTH_TABLES.operators}`,
|
|
163
|
+
)
|
|
164
|
+
.all() as Array<{
|
|
165
|
+
id: string;
|
|
166
|
+
email: string;
|
|
167
|
+
name: string;
|
|
168
|
+
status: OperatorRow["status"];
|
|
169
|
+
mfa_enabled: number;
|
|
170
|
+
invited_by: string | null;
|
|
171
|
+
last_seen_at: number | null;
|
|
172
|
+
}>;
|
|
173
|
+
|
|
174
|
+
for (const row of opRows) {
|
|
175
|
+
store.operators.set(row.id, {
|
|
176
|
+
id: row.id,
|
|
177
|
+
email: row.email,
|
|
178
|
+
name: row.name,
|
|
179
|
+
status: row.status,
|
|
180
|
+
mfaEnabled: row.mfa_enabled === 1,
|
|
181
|
+
invitedBy: row.invited_by,
|
|
182
|
+
lastSeenAt: row.last_seen_at,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const credRows = db
|
|
187
|
+
.query(
|
|
188
|
+
`SELECT operator_id, password_hash, login_enabled
|
|
189
|
+
FROM ${AUTH_TABLES.operatorCredentials}`,
|
|
190
|
+
)
|
|
191
|
+
.all() as Array<{
|
|
192
|
+
operator_id: string;
|
|
193
|
+
password_hash: string;
|
|
194
|
+
login_enabled: number;
|
|
195
|
+
}>;
|
|
196
|
+
|
|
197
|
+
for (const row of credRows) {
|
|
198
|
+
const cred: OperatorCredentialRow = {
|
|
199
|
+
operatorId: row.operator_id,
|
|
200
|
+
passwordHash: row.password_hash,
|
|
201
|
+
loginEnabled: row.login_enabled === 1,
|
|
202
|
+
};
|
|
203
|
+
store.credentials.set(row.operator_id, cred);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const ssoRows = db
|
|
207
|
+
.query(
|
|
208
|
+
`SELECT operator_id, provider, subject FROM ${AUTH_TABLES.operatorSsoLinks}`,
|
|
209
|
+
)
|
|
210
|
+
.all() as Array<{
|
|
211
|
+
operator_id: string;
|
|
212
|
+
provider: string;
|
|
213
|
+
subject: string;
|
|
214
|
+
}>;
|
|
215
|
+
|
|
216
|
+
for (const row of ssoRows) {
|
|
217
|
+
const link: OperatorSsoLinkRow = {
|
|
218
|
+
operatorId: row.operator_id,
|
|
219
|
+
provider: row.provider,
|
|
220
|
+
subject: row.subject,
|
|
221
|
+
};
|
|
222
|
+
const list = store.ssoLinks.get(row.operator_id) ?? [];
|
|
223
|
+
list.push(link);
|
|
224
|
+
store.ssoLinks.set(row.operator_id, list);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const roleRows = db
|
|
228
|
+
.query(
|
|
229
|
+
`SELECT operator_id, role FROM ${AUTH_TABLES.operatorRoles}`,
|
|
230
|
+
)
|
|
231
|
+
.all() as Array<{ operator_id: string; role: string }>;
|
|
232
|
+
|
|
233
|
+
for (const row of roleRows) {
|
|
234
|
+
const list = store.roles.get(row.operator_id) ?? [];
|
|
235
|
+
list.push(row.role);
|
|
236
|
+
store.roles.set(row.operator_id, list);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return store;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Upsert one operator (and related rows) from the in-memory store.
|
|
244
|
+
*
|
|
245
|
+
* @param db - Open database
|
|
246
|
+
* @param store - In-memory store
|
|
247
|
+
* @param operatorId - Operator id
|
|
248
|
+
*/
|
|
249
|
+
export function persistOperator(
|
|
250
|
+
db: Database,
|
|
251
|
+
store: OperatorStore,
|
|
252
|
+
operatorId: string,
|
|
253
|
+
): void {
|
|
254
|
+
const op = store.operators.get(operatorId);
|
|
255
|
+
if (!op) {
|
|
256
|
+
throw new Error(`oke console: unknown operator ${operatorId}`);
|
|
257
|
+
}
|
|
258
|
+
const cred = store.credentials.get(operatorId);
|
|
259
|
+
if (!cred) {
|
|
260
|
+
throw new Error(`oke console: missing credential for ${operatorId}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const upsertOp = db.query(
|
|
264
|
+
`INSERT INTO ${AUTH_TABLES.operators}
|
|
265
|
+
(id, email, name, status, mfa_enabled, invited_by, last_seen_at)
|
|
266
|
+
VALUES ($id, $email, $name, $status, $mfa, $invited, $seen)
|
|
267
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
268
|
+
email = excluded.email,
|
|
269
|
+
name = excluded.name,
|
|
270
|
+
status = excluded.status,
|
|
271
|
+
mfa_enabled = excluded.mfa_enabled,
|
|
272
|
+
invited_by = excluded.invited_by,
|
|
273
|
+
last_seen_at = excluded.last_seen_at`,
|
|
274
|
+
);
|
|
275
|
+
upsertOp.run({
|
|
276
|
+
$id: op.id,
|
|
277
|
+
$email: op.email,
|
|
278
|
+
$name: op.name,
|
|
279
|
+
$status: op.status,
|
|
280
|
+
$mfa: op.mfaEnabled ? 1 : 0,
|
|
281
|
+
$invited: op.invitedBy,
|
|
282
|
+
$seen: op.lastSeenAt,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const upsertCred = db.query(
|
|
286
|
+
`INSERT INTO ${AUTH_TABLES.operatorCredentials}
|
|
287
|
+
(operator_id, password_hash, login_enabled)
|
|
288
|
+
VALUES ($id, $hash, $enabled)
|
|
289
|
+
ON CONFLICT(operator_id) DO UPDATE SET
|
|
290
|
+
password_hash = excluded.password_hash,
|
|
291
|
+
login_enabled = excluded.login_enabled`,
|
|
292
|
+
);
|
|
293
|
+
upsertCred.run({
|
|
294
|
+
$id: cred.operatorId,
|
|
295
|
+
$hash: cred.passwordHash,
|
|
296
|
+
$enabled: cred.loginEnabled ? 1 : 0,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
db.query(
|
|
300
|
+
`DELETE FROM ${AUTH_TABLES.operatorSsoLinks} WHERE operator_id = $id`,
|
|
301
|
+
).run({ $id: operatorId });
|
|
302
|
+
const insertSso = db.query(
|
|
303
|
+
`INSERT INTO ${AUTH_TABLES.operatorSsoLinks} (operator_id, provider, subject)
|
|
304
|
+
VALUES ($id, $provider, $subject)`,
|
|
305
|
+
);
|
|
306
|
+
for (const link of store.ssoLinks.get(operatorId) ?? []) {
|
|
307
|
+
insertSso.run({
|
|
308
|
+
$id: link.operatorId,
|
|
309
|
+
$provider: link.provider,
|
|
310
|
+
$subject: link.subject,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
db.query(
|
|
315
|
+
`DELETE FROM ${AUTH_TABLES.operatorRoles} WHERE operator_id = $id`,
|
|
316
|
+
).run({ $id: operatorId });
|
|
317
|
+
const insertRole = db.query(
|
|
318
|
+
`INSERT INTO ${AUTH_TABLES.operatorRoles} (operator_id, role)
|
|
319
|
+
VALUES ($id, $role)`,
|
|
320
|
+
);
|
|
321
|
+
for (const role of store.roles.get(operatorId) ?? []) {
|
|
322
|
+
insertRole.run({ $id: operatorId, $role: role });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type CreateConsoleAppOptions,
|
|
23
23
|
} from "./app.ts";
|
|
24
24
|
import { createLiveWebsocket, type ConsoleLiveData } from "./live.ts";
|
|
25
|
+
import { openConsolePersistence } from "./operator-db.ts";
|
|
25
26
|
import {
|
|
26
27
|
CONSOLE_COOKIES,
|
|
27
28
|
consoleSessionCookie,
|
|
@@ -37,6 +38,11 @@ export interface ServeConsoleOptions extends CreateConsoleAppOptions {
|
|
|
37
38
|
readonly staticDir?: string;
|
|
38
39
|
/** Boot environment — use `"dev"` / `"prod"` so Bearer auth is production-like. */
|
|
39
40
|
readonly env?: "dev" | "prod" | "test";
|
|
41
|
+
/**
|
|
42
|
+
* Persist operators + secret under `.oke/` (default true).
|
|
43
|
+
* Set false for ephemeral test servers.
|
|
44
|
+
*/
|
|
45
|
+
readonly persist?: boolean;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** Running Console server handle. */
|
|
@@ -54,9 +60,24 @@ export async function serveConsole(
|
|
|
54
60
|
): Promise<ConsoleServerHandle> {
|
|
55
61
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
56
62
|
const port = options.port ?? CONSOLE_PORT;
|
|
63
|
+
const cwd = options.cwd ?? process.cwd();
|
|
64
|
+
const wantPersist = options.persist !== false && options.operators === undefined;
|
|
65
|
+
const persistence = wantPersist
|
|
66
|
+
? await openConsolePersistence(cwd, {
|
|
67
|
+
envSecret: options.secret ?? process.env.OKE_CONSOLE_SECRET,
|
|
68
|
+
})
|
|
69
|
+
: null;
|
|
57
70
|
const handle = createConsoleApp({
|
|
58
71
|
...options,
|
|
72
|
+
cwd,
|
|
59
73
|
silentClaim: options.silentClaim ?? false,
|
|
74
|
+
...(persistence
|
|
75
|
+
? {
|
|
76
|
+
secret: options.secret ?? persistence.secret,
|
|
77
|
+
operators: persistence.operators,
|
|
78
|
+
persistOperator: persistence.persistOperator,
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
60
81
|
});
|
|
61
82
|
|
|
62
83
|
const env = options.env ?? "dev";
|
|
@@ -160,6 +181,7 @@ export async function serveConsole(
|
|
|
160
181
|
stop(closeActive = false) {
|
|
161
182
|
server.stop(closeActive);
|
|
162
183
|
void handle.app.stop();
|
|
184
|
+
persistence?.close();
|
|
163
185
|
},
|
|
164
186
|
};
|
|
165
187
|
}
|
|
@@ -172,11 +194,28 @@ export async function serveConsole(
|
|
|
172
194
|
export async function startConsoleApp(
|
|
173
195
|
options: CreateConsoleAppOptions & {
|
|
174
196
|
readonly env?: "dev" | "prod" | "test";
|
|
197
|
+
/** Opt-in durable operators under `.oke/` (default false for unit tests). */
|
|
198
|
+
readonly persist?: boolean;
|
|
175
199
|
} = {},
|
|
176
200
|
): Promise<ConsoleAppHandle> {
|
|
201
|
+
const cwd = options.cwd ?? process.cwd();
|
|
202
|
+
const persistence =
|
|
203
|
+
options.persist === true && options.operators === undefined
|
|
204
|
+
? await openConsolePersistence(cwd, {
|
|
205
|
+
envSecret: options.secret ?? process.env.OKE_CONSOLE_SECRET,
|
|
206
|
+
})
|
|
207
|
+
: null;
|
|
177
208
|
const handle = createConsoleApp({
|
|
178
209
|
...options,
|
|
210
|
+
cwd,
|
|
179
211
|
silentClaim: options.silentClaim ?? true,
|
|
212
|
+
...(persistence
|
|
213
|
+
? {
|
|
214
|
+
secret: options.secret ?? persistence.secret,
|
|
215
|
+
operators: persistence.operators,
|
|
216
|
+
persistOperator: persistence.persistOperator,
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
180
219
|
});
|
|
181
220
|
await bootConsoleApp(handle);
|
|
182
221
|
return handle;
|
|
@@ -345,6 +345,11 @@ export interface ConsoleState {
|
|
|
345
345
|
}>;
|
|
346
346
|
/** Whether first operator exists (wizard permanently closed). */
|
|
347
347
|
get setupClosed(): boolean;
|
|
348
|
+
/**
|
|
349
|
+
* Persist an operator after claim/create (SQLite under `.oke/`).
|
|
350
|
+
* No-op when persistence is disabled (tests / memory-only).
|
|
351
|
+
*/
|
|
352
|
+
persistOperator: (operatorId: string) => void;
|
|
348
353
|
/**
|
|
349
354
|
* Per-email login attempt timestamps for credential-check rate limiting
|
|
350
355
|
* (console §10.4 — same 5/60s strategy as setup-claim).
|
|
@@ -398,6 +403,10 @@ export interface CreateConsoleStateOptions {
|
|
|
398
403
|
readonly okeConfig?: OkeConfig | null;
|
|
399
404
|
/** Host plugin registry for scopes / capabilities. */
|
|
400
405
|
readonly pluginRegistry?: PluginRegistry | null;
|
|
406
|
+
/** Pre-hydrated operator store (from `.oke/console.sqlite`). */
|
|
407
|
+
readonly operators?: OperatorStore;
|
|
408
|
+
/** Persist hook after claim/create. */
|
|
409
|
+
readonly persistOperator?: (operatorId: string) => void;
|
|
401
410
|
}
|
|
402
411
|
|
|
403
412
|
/**
|
|
@@ -413,10 +422,12 @@ export function createConsoleState(
|
|
|
413
422
|
process.env.OKE_CONSOLE_SECRET ??
|
|
414
423
|
`oke-console-dev-${crypto.randomUUID()}`;
|
|
415
424
|
const now = options.now ?? (() => Date.now());
|
|
425
|
+
const operators = options.operators ?? createOperatorStore();
|
|
416
426
|
const claim = mintClaimCode(now);
|
|
417
|
-
const operators = createOperatorStore();
|
|
418
427
|
const sessions = createSessionStore();
|
|
419
428
|
const liveSubscribers = new Set<(msg: ConsoleLiveMessage) => void>();
|
|
429
|
+
const persistOperator =
|
|
430
|
+
options.persistOperator ?? ((_operatorId: string) => {});
|
|
420
431
|
|
|
421
432
|
const signalConfig = createMemorySignalConfigStore();
|
|
422
433
|
const gateAuth = createDefaultGateAuthStores();
|
|
@@ -843,6 +854,7 @@ export function createConsoleState(
|
|
|
843
854
|
get setupClosed() {
|
|
844
855
|
return operators.operators.size > 0;
|
|
845
856
|
},
|
|
857
|
+
persistOperator,
|
|
846
858
|
};
|
|
847
859
|
|
|
848
860
|
return state;
|