okengine 0.2.3 → 0.2.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.
- package/README.md +30 -4
- package/docs/spec/example.md +12 -3
- package/docs/spec/four-applications.md +10 -3
- package/package.json +9 -40
- package/src/cli/dev-app-runner.ts +45 -6
- package/src/cli/dev.ts +136 -17
- package/src/cli/docker-cli.test.ts +10 -8
- package/src/cli/docker.ts +18 -7
- package/src/cli/hero-meta.test.ts +85 -0
- package/src/cli/hero-meta.ts +252 -0
- package/src/cli/load-config.images.test.ts +51 -0
- package/src/cli/load-config.ts +84 -6
- package/src/config/define-config.test.ts +61 -0
- package/src/config/index.ts +89 -6
- package/src/config/resolve-driver.test.ts +30 -0
- package/src/console/server/flows.ts +3 -1
- package/src/console/server/operator-db.test.ts +140 -2
- package/src/console/server/operator-db.ts +147 -1
- package/src/console/server/plugin.ts +20 -0
- package/src/console/server/serve.ts +8 -1
- package/src/console/server/state.ts +12 -1
- package/src/console/ui/dist/assets/{index-Bnf_3Hei.js → index-Dy4jht9P.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/App.tsx +10 -2
- package/src/docker/compose.ts +45 -14
- package/src/docker/derive.ts +29 -9
- package/src/docker/docker.test.ts +28 -4
- package/src/docker/dockerfile.integration.test.ts +2 -0
- package/src/docker/index.ts +10 -0
- package/src/docker/stack-id.test.ts +86 -0
- package/src/docker/stack-id.ts +108 -0
- package/src/docker/stack.integration.test.ts +16 -7
- package/src/docker/types.ts +20 -1
- package/src/kernel/app.ts +39 -8
- package/src/kernel/boot-bind/store.test.ts +60 -0
- package/src/kernel/boot-bind/store.ts +142 -12
- package/src/kernel/boot.ts +28 -4
- package/src/mcp/server.ts +99 -57
- package/src/runtime/dev-request-log.test.ts +33 -0
- package/src/runtime/dev-request-log.ts +130 -0
- package/src/term.test.ts +98 -5
- package/src/term.ts +287 -6
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver map resolution — including `stack` → prod fallback.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolveDriverId } from "./index.ts";
|
|
7
|
+
|
|
8
|
+
describe("resolveDriverId", () => {
|
|
9
|
+
test("reads the named env key", () => {
|
|
10
|
+
expect(
|
|
11
|
+
resolveDriverId({ dev: "sqlite", prod: "postgres" }, "dev"),
|
|
12
|
+
).toBe("sqlite");
|
|
13
|
+
expect(
|
|
14
|
+
resolveDriverId({ dev: "sqlite", prod: "postgres" }, "prod"),
|
|
15
|
+
).toBe("postgres");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("stack prefers stack, then prod, then dev", () => {
|
|
19
|
+
expect(
|
|
20
|
+
resolveDriverId(
|
|
21
|
+
{ dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
22
|
+
"stack",
|
|
23
|
+
),
|
|
24
|
+
).toBe("postgres");
|
|
25
|
+
expect(
|
|
26
|
+
resolveDriverId({ dev: "sqlite", prod: "postgres" }, "stack"),
|
|
27
|
+
).toBe("postgres");
|
|
28
|
+
expect(resolveDriverId({ dev: "sqlite" }, "stack")).toBe("sqlite");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -2366,7 +2366,7 @@ async function issueOperatorSession(
|
|
|
2366
2366
|
): Promise<IssuedSession> {
|
|
2367
2367
|
const op = state.operators.operators.get(operatorId);
|
|
2368
2368
|
if (op) op.lastSeenAt = state.now();
|
|
2369
|
-
|
|
2369
|
+
const issued = await issueSession(
|
|
2370
2370
|
state.sessions,
|
|
2371
2371
|
{
|
|
2372
2372
|
secret: state.secret,
|
|
@@ -2379,6 +2379,8 @@ async function issueOperatorSession(
|
|
|
2379
2379
|
scopes: ["console:*"],
|
|
2380
2380
|
},
|
|
2381
2381
|
);
|
|
2382
|
+
state.persistSessions();
|
|
2383
|
+
return issued;
|
|
2382
2384
|
}
|
|
2383
2385
|
|
|
2384
2386
|
function sessionPayload(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Durable Console operators under `.oke/console.sqlite`.
|
|
2
|
+
* Durable Console operators + sessions under `.oke/console.sqlite`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
@@ -7,7 +7,8 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { createOperator } from "../../auth/operator.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { issueSession, verifyAccess } from "../../auth/sessions.ts";
|
|
11
|
+
import { bootConsoleApp, createConsoleApp } from "./app.ts";
|
|
11
12
|
import {
|
|
12
13
|
openConsolePersistence,
|
|
13
14
|
resolveConsoleSecret,
|
|
@@ -62,7 +63,9 @@ describe("console operator persistence", () => {
|
|
|
62
63
|
cwd,
|
|
63
64
|
secret: second.secret,
|
|
64
65
|
operators: second.operators,
|
|
66
|
+
sessions: second.sessions,
|
|
65
67
|
persistOperator: second.persistOperator,
|
|
68
|
+
persistSessions: second.persistSessions,
|
|
66
69
|
silentClaim: false,
|
|
67
70
|
});
|
|
68
71
|
expect(app.state.setupClosed).toBe(true);
|
|
@@ -72,4 +75,139 @@ describe("console operator persistence", () => {
|
|
|
72
75
|
second.close();
|
|
73
76
|
}
|
|
74
77
|
});
|
|
78
|
+
|
|
79
|
+
test("sessions survive reopen so access tokens still verify", async () => {
|
|
80
|
+
const cwd = await mkdtemp(join(tmpdir(), "oke-console-sess-"));
|
|
81
|
+
dirs.push(cwd);
|
|
82
|
+
|
|
83
|
+
const first = await openConsolePersistence(cwd);
|
|
84
|
+
const op = await createOperator(first.operators, {
|
|
85
|
+
email: "ops@example.com",
|
|
86
|
+
name: "Ops",
|
|
87
|
+
password: "password123",
|
|
88
|
+
});
|
|
89
|
+
first.persistOperator(op.id);
|
|
90
|
+
const issued = await issueSession(
|
|
91
|
+
first.sessions,
|
|
92
|
+
{ secret: first.secret },
|
|
93
|
+
{ id: op.id, plane: "operator", scopes: ["console:*"] },
|
|
94
|
+
);
|
|
95
|
+
first.persistSessions();
|
|
96
|
+
first.close();
|
|
97
|
+
|
|
98
|
+
const second = await openConsolePersistence(cwd);
|
|
99
|
+
expect(second.sessions.sessions.size).toBe(1);
|
|
100
|
+
const claims = await verifyAccess(
|
|
101
|
+
second.sessions,
|
|
102
|
+
second.secret,
|
|
103
|
+
issued.accessToken,
|
|
104
|
+
);
|
|
105
|
+
expect(claims.sub).toBe(op.id);
|
|
106
|
+
second.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("claim then reopen keeps session.me authorized", async () => {
|
|
110
|
+
const cwd = await mkdtemp(join(tmpdir(), "oke-console-roundtrip-"));
|
|
111
|
+
dirs.push(cwd);
|
|
112
|
+
|
|
113
|
+
const firstPersist = await openConsolePersistence(cwd);
|
|
114
|
+
const first = createConsoleApp({
|
|
115
|
+
cwd,
|
|
116
|
+
secret: firstPersist.secret,
|
|
117
|
+
operators: firstPersist.operators,
|
|
118
|
+
sessions: firstPersist.sessions,
|
|
119
|
+
persistOperator: firstPersist.persistOperator,
|
|
120
|
+
persistSessions: firstPersist.persistSessions,
|
|
121
|
+
silentClaim: true,
|
|
122
|
+
});
|
|
123
|
+
await bootConsoleApp(first);
|
|
124
|
+
let accessToken = "";
|
|
125
|
+
try {
|
|
126
|
+
const claimRes = await first.app.fetch(
|
|
127
|
+
new Request("http://console.test/console/setup/claim", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/json" },
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
claimCode: first.state.claim.code,
|
|
132
|
+
email: "ops@example.com",
|
|
133
|
+
name: "Ops",
|
|
134
|
+
password: "password123",
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
expect(claimRes.status).toBe(200);
|
|
139
|
+
const body = (await claimRes.json()) as {
|
|
140
|
+
data: { accessToken: string };
|
|
141
|
+
};
|
|
142
|
+
accessToken = body.data.accessToken;
|
|
143
|
+
} finally {
|
|
144
|
+
await first.app.stop();
|
|
145
|
+
firstPersist.close();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const secondPersist = await openConsolePersistence(cwd);
|
|
149
|
+
const second = createConsoleApp({
|
|
150
|
+
cwd,
|
|
151
|
+
secret: secondPersist.secret,
|
|
152
|
+
operators: secondPersist.operators,
|
|
153
|
+
sessions: secondPersist.sessions,
|
|
154
|
+
persistOperator: secondPersist.persistOperator,
|
|
155
|
+
persistSessions: secondPersist.persistSessions,
|
|
156
|
+
silentClaim: true,
|
|
157
|
+
});
|
|
158
|
+
await bootConsoleApp(second);
|
|
159
|
+
try {
|
|
160
|
+
const me = await second.app.fetch(
|
|
161
|
+
new Request("http://console.test/console/session/me", {
|
|
162
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
expect(me.status).toBe(200);
|
|
166
|
+
const meBody = (await me.json()) as {
|
|
167
|
+
data: { email: string };
|
|
168
|
+
};
|
|
169
|
+
expect(meBody.data.email).toBe("ops@example.com");
|
|
170
|
+
} finally {
|
|
171
|
+
await second.app.stop();
|
|
172
|
+
secondPersist.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("stale Bearer on setup.status does not 401", async () => {
|
|
177
|
+
const cwd = await mkdtemp(join(tmpdir(), "oke-console-stale-"));
|
|
178
|
+
dirs.push(cwd);
|
|
179
|
+
const persistence = await openConsolePersistence(cwd);
|
|
180
|
+
const op = await createOperator(persistence.operators, {
|
|
181
|
+
email: "ops@example.com",
|
|
182
|
+
name: "Ops",
|
|
183
|
+
password: "password123",
|
|
184
|
+
});
|
|
185
|
+
persistence.persistOperator(op.id);
|
|
186
|
+
|
|
187
|
+
const handle = createConsoleApp({
|
|
188
|
+
cwd,
|
|
189
|
+
secret: persistence.secret,
|
|
190
|
+
operators: persistence.operators,
|
|
191
|
+
// Empty sessions — token cannot verify; public flow must still succeed.
|
|
192
|
+
persistOperator: persistence.persistOperator,
|
|
193
|
+
persistSessions: persistence.persistSessions,
|
|
194
|
+
silentClaim: true,
|
|
195
|
+
});
|
|
196
|
+
await bootConsoleApp(handle);
|
|
197
|
+
try {
|
|
198
|
+
const res = await handle.app.fetch(
|
|
199
|
+
new Request("http://console.test/console/setup/status", {
|
|
200
|
+
headers: { authorization: "Bearer totally-forged-token" },
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
expect(res.status).toBe(200);
|
|
204
|
+
const body = (await res.json()) as {
|
|
205
|
+
data: { setupClosed: boolean };
|
|
206
|
+
};
|
|
207
|
+
expect(body.data.setupClosed).toBe(true);
|
|
208
|
+
} finally {
|
|
209
|
+
await handle.app.stop();
|
|
210
|
+
persistence.close();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
75
213
|
});
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Durable operator plane for Console — `.oke/console.sqlite` + secret file.
|
|
3
3
|
*
|
|
4
4
|
* Spec: wizard closes permanently once the first operator exists (console §2.5).
|
|
5
|
-
* Claim codes stay ephemeral; operators and the signing secret must
|
|
5
|
+
* Claim codes stay ephemeral; operators, sessions, and the signing secret must
|
|
6
|
+
* survive restarts.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { mkdirSync } from "node:fs";
|
|
@@ -12,10 +13,16 @@ import {
|
|
|
12
13
|
createOperatorStore,
|
|
13
14
|
type OperatorStore,
|
|
14
15
|
} from "../../auth/operator.ts";
|
|
16
|
+
import {
|
|
17
|
+
createSessionStore,
|
|
18
|
+
type SessionStore,
|
|
19
|
+
} from "../../auth/sessions.ts";
|
|
15
20
|
import type {
|
|
16
21
|
OperatorCredentialRow,
|
|
17
22
|
OperatorRow,
|
|
18
23
|
OperatorSsoLinkRow,
|
|
24
|
+
RefreshTokenRow,
|
|
25
|
+
SessionRow,
|
|
19
26
|
} from "../../auth/tables.ts";
|
|
20
27
|
import { AUTH_TABLES } from "../../auth/tables.ts";
|
|
21
28
|
|
|
@@ -30,8 +37,12 @@ export interface ConsolePersistence {
|
|
|
30
37
|
readonly secret: string;
|
|
31
38
|
/** Hydrated operator Maps. */
|
|
32
39
|
readonly operators: OperatorStore;
|
|
40
|
+
/** Hydrated session + refresh Maps. */
|
|
41
|
+
readonly sessions: SessionStore;
|
|
33
42
|
/** Persist (or update) one operator + credential + roles/sso. */
|
|
34
43
|
readonly persistOperator: (operatorId: string) => void;
|
|
44
|
+
/** Persist the full session store (issue / revoke). */
|
|
45
|
+
readonly persistSessions: () => void;
|
|
35
46
|
/** Close the SQLite connection. */
|
|
36
47
|
readonly close: () => void;
|
|
37
48
|
}
|
|
@@ -97,13 +108,18 @@ export async function openConsolePersistence(
|
|
|
97
108
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
98
109
|
migrateOperatorSchema(db);
|
|
99
110
|
const operators = loadOperatorStore(db);
|
|
111
|
+
const sessions = loadSessionStore(db);
|
|
100
112
|
|
|
101
113
|
return {
|
|
102
114
|
secret,
|
|
103
115
|
operators,
|
|
116
|
+
sessions,
|
|
104
117
|
persistOperator(operatorId: string) {
|
|
105
118
|
persistOperator(db, operators, operatorId);
|
|
106
119
|
},
|
|
120
|
+
persistSessions() {
|
|
121
|
+
persistSessions(db, sessions);
|
|
122
|
+
},
|
|
107
123
|
close() {
|
|
108
124
|
db.close();
|
|
109
125
|
},
|
|
@@ -145,9 +161,139 @@ export function migrateOperatorSchema(db: Database): void {
|
|
|
145
161
|
PRIMARY KEY (operator_id, role),
|
|
146
162
|
FOREIGN KEY (operator_id) REFERENCES ${AUTH_TABLES.operators}(id)
|
|
147
163
|
);
|
|
164
|
+
CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.sessions} (
|
|
165
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
166
|
+
plane TEXT NOT NULL,
|
|
167
|
+
principal_id TEXT NOT NULL,
|
|
168
|
+
family_id TEXT NOT NULL,
|
|
169
|
+
revoked_at INTEGER,
|
|
170
|
+
created_at INTEGER NOT NULL,
|
|
171
|
+
expires_at INTEGER NOT NULL
|
|
172
|
+
);
|
|
173
|
+
CREATE TABLE IF NOT EXISTS ${AUTH_TABLES.refreshTokens} (
|
|
174
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
175
|
+
session_id TEXT NOT NULL,
|
|
176
|
+
family_id TEXT NOT NULL,
|
|
177
|
+
hash TEXT NOT NULL,
|
|
178
|
+
expires_at INTEGER NOT NULL,
|
|
179
|
+
used_at INTEGER,
|
|
180
|
+
revoked_at INTEGER
|
|
181
|
+
);
|
|
148
182
|
`);
|
|
149
183
|
}
|
|
150
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Hydrate a {@link SessionStore} from SQLite.
|
|
187
|
+
*
|
|
188
|
+
* @param db - Open database
|
|
189
|
+
*/
|
|
190
|
+
export function loadSessionStore(db: Database): SessionStore {
|
|
191
|
+
const store = createSessionStore();
|
|
192
|
+
|
|
193
|
+
const sessionRows = db
|
|
194
|
+
.query(
|
|
195
|
+
`SELECT id, plane, principal_id, family_id, revoked_at, created_at, expires_at
|
|
196
|
+
FROM ${AUTH_TABLES.sessions}`,
|
|
197
|
+
)
|
|
198
|
+
.all() as Array<{
|
|
199
|
+
id: string;
|
|
200
|
+
plane: SessionRow["plane"];
|
|
201
|
+
principal_id: string;
|
|
202
|
+
family_id: string;
|
|
203
|
+
revoked_at: number | null;
|
|
204
|
+
created_at: number;
|
|
205
|
+
expires_at: number;
|
|
206
|
+
}>;
|
|
207
|
+
|
|
208
|
+
for (const row of sessionRows) {
|
|
209
|
+
const session: SessionRow = {
|
|
210
|
+
id: row.id,
|
|
211
|
+
plane: row.plane,
|
|
212
|
+
principalId: row.principal_id,
|
|
213
|
+
familyId: row.family_id,
|
|
214
|
+
revokedAt: row.revoked_at,
|
|
215
|
+
createdAt: row.created_at,
|
|
216
|
+
expiresAt: row.expires_at,
|
|
217
|
+
};
|
|
218
|
+
store.sessions.set(session.id, session);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const refreshRows = db
|
|
222
|
+
.query(
|
|
223
|
+
`SELECT id, session_id, family_id, hash, expires_at, used_at, revoked_at
|
|
224
|
+
FROM ${AUTH_TABLES.refreshTokens}`,
|
|
225
|
+
)
|
|
226
|
+
.all() as Array<{
|
|
227
|
+
id: string;
|
|
228
|
+
session_id: string;
|
|
229
|
+
family_id: string;
|
|
230
|
+
hash: string;
|
|
231
|
+
expires_at: number;
|
|
232
|
+
used_at: number | null;
|
|
233
|
+
revoked_at: number | null;
|
|
234
|
+
}>;
|
|
235
|
+
|
|
236
|
+
for (const row of refreshRows) {
|
|
237
|
+
const token: RefreshTokenRow = {
|
|
238
|
+
id: row.id,
|
|
239
|
+
sessionId: row.session_id,
|
|
240
|
+
familyId: row.family_id,
|
|
241
|
+
hash: row.hash,
|
|
242
|
+
expiresAt: row.expires_at,
|
|
243
|
+
usedAt: row.used_at,
|
|
244
|
+
revokedAt: row.revoked_at,
|
|
245
|
+
};
|
|
246
|
+
store.refresh.set(token.id, token);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return store;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Replace session tables with the in-memory store snapshot.
|
|
254
|
+
*
|
|
255
|
+
* @param db - Open database
|
|
256
|
+
* @param store - In-memory session store
|
|
257
|
+
*/
|
|
258
|
+
export function persistSessions(db: Database, store: SessionStore): void {
|
|
259
|
+
db.exec(`DELETE FROM ${AUTH_TABLES.refreshTokens}`);
|
|
260
|
+
db.exec(`DELETE FROM ${AUTH_TABLES.sessions}`);
|
|
261
|
+
|
|
262
|
+
const insertSession = db.query(
|
|
263
|
+
`INSERT INTO ${AUTH_TABLES.sessions}
|
|
264
|
+
(id, plane, principal_id, family_id, revoked_at, created_at, expires_at)
|
|
265
|
+
VALUES ($id, $plane, $principal, $family, $revoked, $created, $expires)`,
|
|
266
|
+
);
|
|
267
|
+
for (const session of store.sessions.values()) {
|
|
268
|
+
insertSession.run({
|
|
269
|
+
$id: session.id,
|
|
270
|
+
$plane: session.plane,
|
|
271
|
+
$principal: session.principalId,
|
|
272
|
+
$family: session.familyId,
|
|
273
|
+
$revoked: session.revokedAt,
|
|
274
|
+
$created: session.createdAt,
|
|
275
|
+
$expires: session.expiresAt,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const insertRefresh = db.query(
|
|
280
|
+
`INSERT INTO ${AUTH_TABLES.refreshTokens}
|
|
281
|
+
(id, session_id, family_id, hash, expires_at, used_at, revoked_at)
|
|
282
|
+
VALUES ($id, $session, $family, $hash, $expires, $used, $revoked)`,
|
|
283
|
+
);
|
|
284
|
+
for (const token of store.refresh.values()) {
|
|
285
|
+
insertRefresh.run({
|
|
286
|
+
$id: token.id,
|
|
287
|
+
$session: token.sessionId,
|
|
288
|
+
$family: token.familyId,
|
|
289
|
+
$hash: token.hash,
|
|
290
|
+
$expires: token.expiresAt,
|
|
291
|
+
$used: token.usedAt,
|
|
292
|
+
$revoked: token.revokedAt,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
151
297
|
/**
|
|
152
298
|
* Hydrate an {@link OperatorStore} from SQLite.
|
|
153
299
|
*
|
|
@@ -30,6 +30,26 @@ export function consolePlugin(): PluginDef {
|
|
|
30
30
|
entry: "./panels/traces.js",
|
|
31
31
|
})
|
|
32
32
|
.table("oke_console_prefs", { plane: "operator" })
|
|
33
|
+
// Public flows must not 401 when a stale Bearer/cookie is present —
|
|
34
|
+
// kernel onAuth verifies before beforeHandle's public-flow exemption.
|
|
35
|
+
// Rebuild from URL: `new Request(req, { headers })` re-copies Authorization.
|
|
36
|
+
.hook("onRequest", (ctx) => {
|
|
37
|
+
if (!PUBLIC_CONSOLE_FLOWS.has(ctx.flow.name) || !ctx.request) return;
|
|
38
|
+
const auth = ctx.request.headers.get("authorization");
|
|
39
|
+
if (!auth?.startsWith("Bearer ")) return;
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
for (const [key, value] of ctx.request.headers) {
|
|
42
|
+
if (key.toLowerCase() === "authorization") continue;
|
|
43
|
+
headers.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
const method = ctx.request.method;
|
|
46
|
+
const init: RequestInit = { method, headers };
|
|
47
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
48
|
+
init.body = ctx.request.body;
|
|
49
|
+
(init as RequestInit & { duplex: "half" }).duplex = "half";
|
|
50
|
+
}
|
|
51
|
+
ctx.request = new Request(ctx.request.url, init);
|
|
52
|
+
})
|
|
33
53
|
.hook("beforeHandle", (ctx, fxOrErr) => {
|
|
34
54
|
const fx = fxOrErr as Fx;
|
|
35
55
|
const name = ctx.flow.name;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveAllowedHosts,
|
|
12
12
|
secureFetch,
|
|
13
13
|
} from "../../runtime/security.ts";
|
|
14
|
+
import { runWithDevSurface } from "../../runtime/dev-request-log.ts";
|
|
14
15
|
import { CONSOLE_PORT, type ServerHandle } from "../../runtime/types.ts";
|
|
15
16
|
import {
|
|
16
17
|
bindManifestSignalBus,
|
|
@@ -75,7 +76,9 @@ export async function serveConsole(
|
|
|
75
76
|
? {
|
|
76
77
|
secret: options.secret ?? persistence.secret,
|
|
77
78
|
operators: persistence.operators,
|
|
79
|
+
sessions: persistence.sessions,
|
|
78
80
|
persistOperator: persistence.persistOperator,
|
|
81
|
+
persistSessions: persistence.persistSessions,
|
|
79
82
|
}
|
|
80
83
|
: {}),
|
|
81
84
|
});
|
|
@@ -109,7 +112,9 @@ export async function serveConsole(
|
|
|
109
112
|
const authed = withCookieAuth(request);
|
|
110
113
|
|
|
111
114
|
if (url.pathname.startsWith("/console/")) {
|
|
112
|
-
const response = await
|
|
115
|
+
const response = await runWithDevSurface("Console", () =>
|
|
116
|
+
handle.app.fetch(authed),
|
|
117
|
+
);
|
|
113
118
|
const withCookies = await attachSessionCookies(authed, response);
|
|
114
119
|
return withConsoleSecurityHeaders(withCookies);
|
|
115
120
|
}
|
|
@@ -213,7 +218,9 @@ export async function startConsoleApp(
|
|
|
213
218
|
? {
|
|
214
219
|
secret: options.secret ?? persistence.secret,
|
|
215
220
|
operators: persistence.operators,
|
|
221
|
+
sessions: persistence.sessions,
|
|
216
222
|
persistOperator: persistence.persistOperator,
|
|
223
|
+
persistSessions: persistence.persistSessions,
|
|
217
224
|
}
|
|
218
225
|
: {}),
|
|
219
226
|
});
|
|
@@ -350,6 +350,11 @@ export interface ConsoleState {
|
|
|
350
350
|
* No-op when persistence is disabled (tests / memory-only).
|
|
351
351
|
*/
|
|
352
352
|
persistOperator: (operatorId: string) => void;
|
|
353
|
+
/**
|
|
354
|
+
* Persist sessions after issue / revoke (SQLite under `.oke/`).
|
|
355
|
+
* No-op when persistence is disabled (tests / memory-only).
|
|
356
|
+
*/
|
|
357
|
+
persistSessions: () => void;
|
|
353
358
|
/**
|
|
354
359
|
* Per-email login attempt timestamps for credential-check rate limiting
|
|
355
360
|
* (console §10.4 — same 5/60s strategy as setup-claim).
|
|
@@ -405,8 +410,12 @@ export interface CreateConsoleStateOptions {
|
|
|
405
410
|
readonly pluginRegistry?: PluginRegistry | null;
|
|
406
411
|
/** Pre-hydrated operator store (from `.oke/console.sqlite`). */
|
|
407
412
|
readonly operators?: OperatorStore;
|
|
413
|
+
/** Pre-hydrated session store (from `.oke/console.sqlite`). */
|
|
414
|
+
readonly sessions?: SessionStore;
|
|
408
415
|
/** Persist hook after claim/create. */
|
|
409
416
|
readonly persistOperator?: (operatorId: string) => void;
|
|
417
|
+
/** Persist hook after session issue / revoke. */
|
|
418
|
+
readonly persistSessions?: () => void;
|
|
410
419
|
}
|
|
411
420
|
|
|
412
421
|
/**
|
|
@@ -424,10 +433,11 @@ export function createConsoleState(
|
|
|
424
433
|
const now = options.now ?? (() => Date.now());
|
|
425
434
|
const operators = options.operators ?? createOperatorStore();
|
|
426
435
|
const claim = mintClaimCode(now);
|
|
427
|
-
const sessions = createSessionStore();
|
|
436
|
+
const sessions = options.sessions ?? createSessionStore();
|
|
428
437
|
const liveSubscribers = new Set<(msg: ConsoleLiveMessage) => void>();
|
|
429
438
|
const persistOperator =
|
|
430
439
|
options.persistOperator ?? ((_operatorId: string) => {});
|
|
440
|
+
const persistSessions = options.persistSessions ?? (() => {});
|
|
431
441
|
|
|
432
442
|
const signalConfig = createMemorySignalConfigStore();
|
|
433
443
|
const gateAuth = createDefaultGateAuthStores();
|
|
@@ -855,6 +865,7 @@ export function createConsoleState(
|
|
|
855
865
|
return operators.operators.size > 0;
|
|
856
866
|
},
|
|
857
867
|
persistOperator,
|
|
868
|
+
persistSessions,
|
|
858
869
|
};
|
|
859
870
|
|
|
860
871
|
return state;
|