gestalt-mobile 0.9.0 → 0.11.0
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 +83 -14
- package/dist/client/assets/{index-DrULMr7l.css → index-Bkm5GDCs.css} +1 -1
- package/dist/client/assets/index-DviggEIS.js +19 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +52 -3
- package/dist/server/server/cli.js +6 -0
- package/dist/server/server/composition.js +212 -150
- package/dist/server/server/config.js +64 -2
- package/dist/server/server/features/auth/application/ports.js +8 -0
- package/dist/server/server/features/auth/devices/application.js +26 -0
- package/dist/server/server/features/auth/devices/list/endpoint.js +12 -0
- package/dist/server/server/features/auth/devices/rename/endpoint.js +37 -0
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +48 -0
- package/dist/server/server/features/auth/domain/authorization.js +101 -0
- package/dist/server/server/features/auth/domain/device-nickname.js +13 -0
- package/dist/server/server/features/auth/domain/errors.js +7 -0
- package/dist/server/server/features/auth/domain/identifiers.js +45 -0
- package/dist/server/server/features/auth/enrollment-tickets/cancel/endpoint.js +14 -0
- package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +17 -0
- package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +13 -0
- package/dist/server/server/features/auth/login/options/endpoint.js +44 -0
- package/dist/server/server/features/auth/login/verify/endpoint.js +88 -0
- package/dist/server/server/features/auth/logout/endpoint.js +16 -0
- package/dist/server/server/features/auth/register/options/endpoint.js +34 -0
- package/dist/server/server/features/auth/register/verify/endpoint.js +56 -0
- package/dist/server/server/features/auth/status/endpoint.js +24 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +23 -1
- package/dist/server/server/features/sessions/restore-session/endpoint.js +24 -4
- package/dist/server/server/features/sessions/restore-session/use-case.js +20 -0
- package/dist/server/server/features/sessions/session-events/endpoint.js +7 -2
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +75 -0
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +478 -0
- package/dist/server/server/platform/codex/json-rpc-client.js +36 -2
- package/dist/server/server/platform/codex/session-runtime.js +64 -22
- package/dist/server/server/platform/events/websocket-heartbeat.js +5 -1
- package/dist/server/server/platform/http/authorization-boundary.js +60 -0
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +30 -11
- package/dist/server/shared/contracts/plan-signal.js +17 -0
- package/package.json +8 -2
- package/dist/client/assets/index-D7-LoAAR.js +0 -12
|
@@ -4,6 +4,38 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
|
+
export const CODEX_THREAD_NOT_FOUND = 'CODEX_THREAD_NOT_FOUND';
|
|
8
|
+
export const CODEX_JSON_RPC_ERROR = 'CODEX_JSON_RPC_ERROR';
|
|
9
|
+
/** A bounded representation of an app-server JSON-RPC failure. */
|
|
10
|
+
export class CodexJsonRpcError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
kind;
|
|
13
|
+
constructor(code, message, kind = classifyCodexJsonRpcError(code, message)) {
|
|
14
|
+
super(boundMessage(message));
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.kind = kind;
|
|
17
|
+
this.name = 'CodexJsonRpcError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Codex 0.146 reports a rollout removed by an upgrade as -32600. Keep the
|
|
22
|
+
* message match deliberately narrow: other invalid-request responses must not
|
|
23
|
+
* be treated as permission to replace a durable thread.
|
|
24
|
+
*/
|
|
25
|
+
export function isMissingCodexThreadRollout(error) {
|
|
26
|
+
return error instanceof CodexJsonRpcError && error.kind === CODEX_THREAD_NOT_FOUND;
|
|
27
|
+
}
|
|
28
|
+
function classifyCodexJsonRpcError(code, message) {
|
|
29
|
+
return code === -32600 && /^no rollout found for thread id\b/i.test(message)
|
|
30
|
+
? CODEX_THREAD_NOT_FOUND
|
|
31
|
+
: CODEX_JSON_RPC_ERROR;
|
|
32
|
+
}
|
|
33
|
+
function boundMessage(message) {
|
|
34
|
+
return message
|
|
35
|
+
.replace(/((?:authorization|token|api[_ -]?key|password)\s*[:=]\s*)\S+/gi, '$1[REDACTED]')
|
|
36
|
+
.replace(/(authentication failed:\s*).*/i, '$1[REDACTED]')
|
|
37
|
+
.slice(0, 256);
|
|
38
|
+
}
|
|
7
39
|
export class JsonRpcClient {
|
|
8
40
|
output;
|
|
9
41
|
sequence = 0;
|
|
@@ -59,8 +91,10 @@ export class JsonRpcClient {
|
|
|
59
91
|
if (!pending)
|
|
60
92
|
return;
|
|
61
93
|
this.pending.delete(message.id);
|
|
62
|
-
if (message.error)
|
|
63
|
-
|
|
94
|
+
if (message.error) {
|
|
95
|
+
const error = message.error;
|
|
96
|
+
pending.reject(new CodexJsonRpcError(typeof error.code === 'number' ? error.code : undefined, typeof error.message === 'string' ? error.message : 'JSON_RPC_ERROR'));
|
|
97
|
+
}
|
|
64
98
|
else
|
|
65
99
|
pending.resolve(message.result);
|
|
66
100
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { RelaySession, } from '../../features/sessions/model/relay-session.js';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { createPlanMeasurementSnapshot, } from '../../features/plans/application/measurement-snapshot.js';
|
|
9
|
+
import { canRebindMissingRollout, rebindMissingRollout, } from '../../features/sessions/restore-session/use-case.js';
|
|
9
10
|
import { gestaltQuizDynamicTool } from '../../../shared/contracts/quiz.js';
|
|
10
11
|
export class CodexSessionRuntime {
|
|
11
12
|
launch;
|
|
@@ -43,22 +44,13 @@ export class CodexSessionRuntime {
|
|
|
43
44
|
clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
|
|
44
45
|
capabilities: { experimentalApi: true },
|
|
45
46
|
});
|
|
46
|
-
const
|
|
47
|
-
cwd: session.workspacePath,
|
|
48
|
-
approvalPolicy: settings.approvalPolicy ?? 'on-request',
|
|
49
|
-
dynamicTools: [gestaltQuizDynamicTool],
|
|
50
|
-
...(settings.model ? { model: settings.model } : {}),
|
|
51
|
-
...(settings.sandbox ? { sandbox: settings.sandbox } : {}),
|
|
52
|
-
}));
|
|
53
|
-
if (!result.thread?.id)
|
|
54
|
-
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
47
|
+
const startedThreadId = await this.startThread(process, session, settings);
|
|
55
48
|
this.processes.set(session.id, process);
|
|
56
|
-
this.threadIds.set(session.id,
|
|
57
|
-
return RelaySession.rehydrate(session).bindThread(
|
|
49
|
+
this.threadIds.set(session.id, startedThreadId);
|
|
50
|
+
return RelaySession.rehydrate(session).bindThread(startedThreadId, now).snapshot;
|
|
58
51
|
}
|
|
59
52
|
catch (error) {
|
|
60
|
-
|
|
61
|
-
this.releasePlanStatus(session.id);
|
|
53
|
+
this.discardProcess(session.id, process);
|
|
62
54
|
throw error;
|
|
63
55
|
}
|
|
64
56
|
}
|
|
@@ -68,6 +60,8 @@ export class CodexSessionRuntime {
|
|
|
68
60
|
this.processes.get(sessionId)?.close();
|
|
69
61
|
this.processes.delete(sessionId);
|
|
70
62
|
this.threadIds.delete(sessionId);
|
|
63
|
+
this.clearPendingRequests(sessionId);
|
|
64
|
+
this.planMeasurementTokens.delete(sessionId);
|
|
71
65
|
this.releasePlanStatus(sessionId);
|
|
72
66
|
}
|
|
73
67
|
async release(sessionId) {
|
|
@@ -156,6 +150,9 @@ export class CodexSessionRuntime {
|
|
|
156
150
|
};
|
|
157
151
|
}
|
|
158
152
|
async restore(session, now) {
|
|
153
|
+
return (await this.restoreWithOutcome(session, now)).session;
|
|
154
|
+
}
|
|
155
|
+
async restoreWithOutcome(session, now) {
|
|
159
156
|
if (!session.threadId)
|
|
160
157
|
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
161
158
|
const process = await this.launchForSession(session);
|
|
@@ -167,18 +164,31 @@ export class CodexSessionRuntime {
|
|
|
167
164
|
clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
|
|
168
165
|
capabilities: { experimentalApi: true },
|
|
169
166
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
167
|
+
let result;
|
|
168
|
+
try {
|
|
169
|
+
await process.rpc.request('thread/resume', {
|
|
170
|
+
threadId: session.threadId,
|
|
171
|
+
cwd: session.workspacePath,
|
|
172
|
+
dynamicTools: [gestaltQuizDynamicTool],
|
|
173
|
+
});
|
|
174
|
+
result = {
|
|
175
|
+
session: RelaySession.rehydrate(session).restore(now).snapshot,
|
|
176
|
+
historyUnavailable: false,
|
|
177
|
+
replacementCreated: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!canRebindMissingRollout(session, error))
|
|
182
|
+
throw error;
|
|
183
|
+
const replacementThreadId = await this.startThread(process, session, { model: session.model });
|
|
184
|
+
result = rebindMissingRollout(session, error, replacementThreadId, now);
|
|
185
|
+
}
|
|
175
186
|
this.processes.set(session.id, process);
|
|
176
|
-
this.threadIds.set(session.id, session.threadId);
|
|
177
|
-
return
|
|
187
|
+
this.threadIds.set(session.id, result.session.threadId);
|
|
188
|
+
return result;
|
|
178
189
|
}
|
|
179
190
|
catch (error) {
|
|
180
|
-
|
|
181
|
-
this.releasePlanStatus(session.id);
|
|
191
|
+
this.discardProcess(session.id, process);
|
|
182
192
|
throw error;
|
|
183
193
|
}
|
|
184
194
|
}
|
|
@@ -192,12 +202,44 @@ export class CodexSessionRuntime {
|
|
|
192
202
|
this.exitUnsubscribers.set(sessionId, process.onExit?.(() => {
|
|
193
203
|
this.processes.delete(sessionId);
|
|
194
204
|
this.threadIds.delete(sessionId);
|
|
205
|
+
this.clearPendingRequests(sessionId);
|
|
195
206
|
this.planMeasurementTokens.delete(sessionId);
|
|
196
207
|
this.exitUnsubscribers.delete(sessionId);
|
|
197
208
|
this.releasePlanStatus(sessionId);
|
|
198
209
|
this.onProcessExit?.(sessionId);
|
|
199
210
|
}) ?? (() => { }));
|
|
200
211
|
}
|
|
212
|
+
async startThread(process, session, settings = {}) {
|
|
213
|
+
const result = (await process.rpc.request('thread/start', this.threadStartParams(session, settings)));
|
|
214
|
+
if (typeof result.thread?.id !== 'string' || !result.thread.id) {
|
|
215
|
+
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
216
|
+
}
|
|
217
|
+
return result.thread.id;
|
|
218
|
+
}
|
|
219
|
+
threadStartParams(session, settings) {
|
|
220
|
+
return {
|
|
221
|
+
cwd: session.workspacePath,
|
|
222
|
+
approvalPolicy: settings.approvalPolicy ?? 'on-request',
|
|
223
|
+
dynamicTools: [gestaltQuizDynamicTool],
|
|
224
|
+
...(settings.model ? { model: settings.model } : {}),
|
|
225
|
+
...(settings.sandbox ? { sandbox: settings.sandbox } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
discardProcess(sessionId, process) {
|
|
229
|
+
this.exitUnsubscribers.get(sessionId)?.();
|
|
230
|
+
this.exitUnsubscribers.delete(sessionId);
|
|
231
|
+
process.close();
|
|
232
|
+
this.clearPendingRequests(sessionId);
|
|
233
|
+
this.planMeasurementTokens.delete(sessionId);
|
|
234
|
+
this.releasePlanStatus(sessionId);
|
|
235
|
+
}
|
|
236
|
+
clearPendingRequests(sessionId) {
|
|
237
|
+
const prefix = `${sessionId}:`;
|
|
238
|
+
for (const key of this.pendingRequests.keys()) {
|
|
239
|
+
if (key.startsWith(prefix))
|
|
240
|
+
this.pendingRequests.delete(key);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
201
243
|
async launchForSession(session) {
|
|
202
244
|
const lease = this.planStatusSource
|
|
203
245
|
? await this.planStatusSource.open({ id: session.id, workspacePath: session.workspacePath }, (update) => this.onPlanStatus?.(session.id, update))
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
export function installWebSocketHeartbeat(socket, intervalMs = 25_000) {
|
|
6
|
+
export function installWebSocketHeartbeat(socket, intervalMs = 25_000, isAuthorized = () => true) {
|
|
7
7
|
let alive = true;
|
|
8
8
|
socket.on('pong', () => {
|
|
9
9
|
alive = true;
|
|
10
10
|
});
|
|
11
11
|
const interval = setInterval(() => {
|
|
12
|
+
if (!isAuthorized()) {
|
|
13
|
+
socket.terminate();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
12
16
|
if (!alive) {
|
|
13
17
|
socket.terminate();
|
|
14
18
|
return;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { parseAuthorizationSessionId } from '../../features/auth/domain/identifiers.js';
|
|
7
|
+
import { problem } from './problem.js';
|
|
8
|
+
const publicApi = new Set([
|
|
9
|
+
'GET /api/auth/status',
|
|
10
|
+
'POST /api/auth/login/options',
|
|
11
|
+
'POST /api/auth/login/verify',
|
|
12
|
+
'POST /api/auth/logout',
|
|
13
|
+
'POST /api/auth/register/options',
|
|
14
|
+
'POST /api/auth/register/verify',
|
|
15
|
+
]);
|
|
16
|
+
export function registerAuthorizationBoundary(app, deps) {
|
|
17
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
18
|
+
if (!request.url.startsWith('/api/'))
|
|
19
|
+
return;
|
|
20
|
+
if (isUnsafe(request.method) && request.headers.origin !== deps.publicOrigin)
|
|
21
|
+
return reply
|
|
22
|
+
.code(403)
|
|
23
|
+
.type('application/problem+json')
|
|
24
|
+
.send(problem('ORIGIN_NOT_ALLOWED', 403, 'The request origin is not allowed.'));
|
|
25
|
+
if (publicApi.has(`${request.method} ${request.routeOptions.url}`))
|
|
26
|
+
return;
|
|
27
|
+
if (authorizationSessionDevice(request.headers.cookie, deps) !== null)
|
|
28
|
+
return;
|
|
29
|
+
return reply
|
|
30
|
+
.code(401)
|
|
31
|
+
.type('application/problem+json')
|
|
32
|
+
.send(problem('AUTH_REQUIRED', 401, 'Authentication is required.'));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Shared by HTTP and raw WebSocket upgrades; the opaque cookie is never logged. */
|
|
36
|
+
export function authorizationSessionDevice(cookieHeader, deps) {
|
|
37
|
+
const token = cookieValue(cookieHeader, 'gestalt_mobile_session');
|
|
38
|
+
const session = parseAuthorizationSessionId(token);
|
|
39
|
+
return session === null
|
|
40
|
+
? null
|
|
41
|
+
: deps.repository.sessionDevice(session, deps.clock.now().toISOString());
|
|
42
|
+
}
|
|
43
|
+
function cookieValue(header, name) {
|
|
44
|
+
if (!header)
|
|
45
|
+
return undefined;
|
|
46
|
+
let found;
|
|
47
|
+
for (const part of header.split(';')) {
|
|
48
|
+
const [key, ...value] = part.trim().split('=');
|
|
49
|
+
if (key !== name)
|
|
50
|
+
continue;
|
|
51
|
+
const candidate = value.join('=');
|
|
52
|
+
if (!candidate || found !== undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
found = candidate;
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
function isUnsafe(method) {
|
|
59
|
+
return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';
|
|
60
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { chmod, mkdir, readFile, readdir, rename, rm, stat, watch, writeFile, realpath, } from 'node:fs/promises';
|
|
7
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { isPlanSignalReason } from '../../../shared/contracts/plan-signal.js';
|
|
8
9
|
import { basename, dirname, join } from 'node:path';
|
|
9
10
|
import { parseSupervisedPlan } from '../../features/plans/application/parse-supervised-plan.js';
|
|
10
11
|
import { isPlanPathWithinWorkspace } from '../../features/plans/application/parse-supervised-plan.js';
|
|
@@ -49,13 +50,21 @@ export class FilesystemPlanStatusSource {
|
|
|
49
50
|
async remove(sessionId, identity) {
|
|
50
51
|
const statusPath = this.activeStatusPaths.get(sessionId);
|
|
51
52
|
const signal = statusPath ? await this.readStatusForRollback(statusPath) : undefined;
|
|
52
|
-
if (statusPath)
|
|
53
|
-
|
|
53
|
+
if (statusPath) {
|
|
54
|
+
try {
|
|
55
|
+
await this.statusRemovalFilesystem.rm(statusPath, { force: true });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.leases.get(sessionId)?.allowRepeatedUpdate();
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
54
62
|
try {
|
|
55
63
|
if (identity)
|
|
56
64
|
await this.dismiss(sessionId, identity);
|
|
57
65
|
}
|
|
58
66
|
catch (error) {
|
|
67
|
+
this.leases.get(sessionId)?.allowRepeatedUpdate();
|
|
59
68
|
if (signal !== undefined && statusPath)
|
|
60
69
|
await this.restoreStatus(statusPath, signal);
|
|
61
70
|
throw error;
|
|
@@ -146,6 +155,7 @@ class ActiveLease {
|
|
|
146
155
|
debounce;
|
|
147
156
|
closed = false;
|
|
148
157
|
activeStatusPath;
|
|
158
|
+
lastEmittedUpdate;
|
|
149
159
|
constructor(statusDirectory, sessionId, workspacePath, listener, planReadFilesystem, isDismissed, onActiveStatusPath, onClose) {
|
|
150
160
|
this.statusDirectory = statusDirectory;
|
|
151
161
|
this.sessionId = sessionId;
|
|
@@ -163,7 +173,7 @@ class ActiveLease {
|
|
|
163
173
|
void this.consumeChanges();
|
|
164
174
|
}
|
|
165
175
|
catch {
|
|
166
|
-
this.
|
|
176
|
+
this.emitUnavailable();
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
179
|
close() {
|
|
@@ -179,6 +189,9 @@ class ActiveLease {
|
|
|
179
189
|
if (this.activeStatusPath)
|
|
180
190
|
await rm(this.activeStatusPath, { force: true });
|
|
181
191
|
}
|
|
192
|
+
allowRepeatedUpdate() {
|
|
193
|
+
this.lastEmittedUpdate = undefined;
|
|
194
|
+
}
|
|
182
195
|
async consumeChanges() {
|
|
183
196
|
try {
|
|
184
197
|
for await (const event of this.watcher) {
|
|
@@ -196,7 +209,7 @@ class ActiveLease {
|
|
|
196
209
|
}
|
|
197
210
|
catch {
|
|
198
211
|
if (!this.closed)
|
|
199
|
-
this.
|
|
212
|
+
this.emitUnavailable();
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
pendingStatusPath;
|
|
@@ -232,7 +245,7 @@ class ActiveLease {
|
|
|
232
245
|
}
|
|
233
246
|
catch {
|
|
234
247
|
if (!this.closed)
|
|
235
|
-
this.
|
|
248
|
+
this.emitUnavailable();
|
|
236
249
|
}
|
|
237
250
|
}
|
|
238
251
|
async refresh(statusPath, parsedSignal) {
|
|
@@ -259,13 +272,18 @@ class ActiveLease {
|
|
|
259
272
|
const previousStatusPath = this.activeStatusPath;
|
|
260
273
|
this.activeStatusPath = statusPath;
|
|
261
274
|
this.onActiveStatusPath(statusPath);
|
|
262
|
-
|
|
275
|
+
const update = { kind: 'updated', plan: result.plan, identity, planPath, reason: signal.reason };
|
|
276
|
+
const serializedUpdate = JSON.stringify(update);
|
|
277
|
+
if (serializedUpdate !== this.lastEmittedUpdate) {
|
|
278
|
+
this.lastEmittedUpdate = serializedUpdate;
|
|
279
|
+
this.listener(update);
|
|
280
|
+
}
|
|
263
281
|
if (previousStatusPath && previousStatusPath !== statusPath)
|
|
264
282
|
await rm(previousStatusPath, { force: true }).catch(() => { });
|
|
265
283
|
}
|
|
266
284
|
}
|
|
267
285
|
else if (!this.closed) {
|
|
268
|
-
this.
|
|
286
|
+
this.emitUnavailable();
|
|
269
287
|
}
|
|
270
288
|
}
|
|
271
289
|
catch (error) {
|
|
@@ -274,9 +292,13 @@ class ActiveLease {
|
|
|
274
292
|
statusPath !== this.activeStatusPath)
|
|
275
293
|
return;
|
|
276
294
|
if (!this.closed)
|
|
277
|
-
this.
|
|
295
|
+
this.emitUnavailable();
|
|
278
296
|
}
|
|
279
297
|
}
|
|
298
|
+
emitUnavailable() {
|
|
299
|
+
this.lastEmittedUpdate = undefined;
|
|
300
|
+
this.listener({ kind: 'unavailable', code: 'PLAN_STATUS_UNAVAILABLE' });
|
|
301
|
+
}
|
|
280
302
|
}
|
|
281
303
|
function parseSignal(source) {
|
|
282
304
|
try {
|
|
@@ -298,9 +320,6 @@ function parseSignal(source) {
|
|
|
298
320
|
return null;
|
|
299
321
|
}
|
|
300
322
|
}
|
|
301
|
-
function isPlanSignalReason(value) {
|
|
302
|
-
return value === 'authoring-start' || value === 'work-start' || value === 'checkpoint' || value === 'update';
|
|
303
|
-
}
|
|
304
323
|
function isRfc3339Utc(value) {
|
|
305
324
|
return (typeof value === 'string' &&
|
|
306
325
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value) &&
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
/** Every reason published by the bundled org-plan helper. */
|
|
7
|
+
export const planSignalReasons = [
|
|
8
|
+
'authoring-start',
|
|
9
|
+
'work-start',
|
|
10
|
+
'checkpoint',
|
|
11
|
+
'update',
|
|
12
|
+
'supervision-start',
|
|
13
|
+
'resync',
|
|
14
|
+
];
|
|
15
|
+
export function isPlanSignalReason(value) {
|
|
16
|
+
return typeof value === 'string' && planSignalReasons.includes(value);
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gestalt-mobile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Mobile-first web relay for durable Codex development sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"dev": "node scripts/dev.mjs",
|
|
36
|
-
"dev:server": "tsx watch src/server/main.ts -- --cwd ..",
|
|
36
|
+
"dev:server": "tsx watch src/server/main.ts -- --cwd .. --public-origin http://localhost:5173",
|
|
37
37
|
"start": "node dist/server/server/main.js",
|
|
38
38
|
"dev:client": "vite",
|
|
39
39
|
"build": "npm run build:server && npm run build:client",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"format": "prettier --write .",
|
|
46
46
|
"format:check": "prettier --check .",
|
|
47
47
|
"test": "vitest run",
|
|
48
|
+
"test:open-profile-smoke": "tsx scripts/open-profile-smoke.ts",
|
|
48
49
|
"test:watch": "vitest",
|
|
49
50
|
"test:package": "node scripts/smoke-packed-cli.mjs",
|
|
50
51
|
"test:e2e": "playwright test",
|
|
@@ -54,8 +55,12 @@
|
|
|
54
55
|
"license:apply": "node scripts/apply-license-headers.mjs"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
58
|
+
"@fastify/cookie": "11.1.2",
|
|
57
59
|
"@fastify/static": "^10.1.0",
|
|
60
|
+
"@simplewebauthn/browser": "13.3.0",
|
|
61
|
+
"@simplewebauthn/server": "13.3.2",
|
|
58
62
|
"fastify": "^5.10.0",
|
|
63
|
+
"qrcode": "^1.5.4",
|
|
59
64
|
"ws": "^8.21.0",
|
|
60
65
|
"yaml": "^2.9.0",
|
|
61
66
|
"zod": "^4.4.3"
|
|
@@ -65,6 +70,7 @@
|
|
|
65
70
|
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
66
71
|
"@testing-library/svelte": "^5.4.2",
|
|
67
72
|
"@types/node": "^24.13.3",
|
|
73
|
+
"@types/qrcode": "^1.5.6",
|
|
68
74
|
"@types/ws": "^8.18.1",
|
|
69
75
|
"eslint": "^10.7.0",
|
|
70
76
|
"jsdom": "^29.1.1",
|