gestalt-mobile 0.12.0 → 0.14.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 +42 -11
- package/dist/client/assets/index-C5qFcFUg.css +1 -0
- package/dist/client/assets/index-tMyOolAm.js +24 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
- package/dist/client/index.html +3 -2
- package/dist/server/server/app.js +22 -227
- package/dist/server/server/cli.js +3 -1
- package/dist/server/server/composition.js +25 -26
- package/dist/server/server/features/auth/application/ceremony-attempts.js +22 -0
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
- package/dist/server/server/features/auth/domain/errors.js +7 -0
- package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
- package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
- package/dist/server/server/features/auth/http/cookies.js +20 -0
- package/dist/server/server/features/auth/login/options/endpoint.js +22 -20
- package/dist/server/server/features/auth/login/verify/endpoint.js +6 -14
- package/dist/server/server/features/auth/logout/endpoint.js +2 -6
- package/dist/server/server/features/auth/register/options/endpoint.js +43 -11
- package/dist/server/server/features/auth/register/verify/endpoint.js +74 -18
- package/dist/server/server/features/auth/register-routes.js +41 -0
- package/dist/server/server/features/git/checkout-branch/endpoint.js +1 -1
- package/dist/server/server/features/git/clone-repository/endpoint.js +1 -1
- package/dist/server/server/features/git/register-routes.js +34 -0
- package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
- package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
- package/dist/server/server/features/plans/register-routes.js +22 -0
- package/dist/server/server/features/sessions/interaction/kind.js +7 -1
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
- package/dist/server/server/features/sessions/model/relay-session.js +3 -1
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +1 -1
- package/dist/server/server/features/sessions/register-routes.js +138 -0
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/endpoint.js +5 -2
- package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
- package/dist/server/server/features/sessions/start-turn/endpoint.js +2 -1
- package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
- package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
- package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
- package/dist/server/server/features/skills/model/skill-profile.js +23 -3
- package/dist/server/server/features/skills/register-routes.js +17 -0
- package/dist/server/server/features/skills/replace-profile/endpoint.js +20 -5
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +23 -3
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +29 -6
- package/dist/server/server/platform/catalog/profile-command.js +8 -1
- package/dist/server/server/platform/codex/normalizer.js +50 -6
- package/dist/server/server/platform/codex/protocol-compatibility.js +19 -1
- package/dist/server/server/platform/codex/server-request.js +2 -0
- package/dist/server/server/platform/codex/session-runtime.js +235 -126
- package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
- package/dist/server/server/platform/http/problem-handler.js +10 -0
- package/dist/server/server/platform/http/problem-reply.js +13 -0
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
- package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
- package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
- package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
- package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
- package/dist/server/shared/contracts/quiz.js +30 -9
- package/package.json +4 -1
- package/dist/client/assets/index-B7KwEjTE.js +0 -19
- package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
- package/dist/server/server/features/plans/application/native-plan-alignment.js +0 -39
|
Binary file
|
|
Binary file
|
package/dist/client/index.html
CHANGED
|
@@ -9,9 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
9
9
|
<head>
|
|
10
10
|
<meta charset="UTF-8" />
|
|
11
11
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
<meta name="color-scheme" content="light dark" />
|
|
12
13
|
<title>Codex Relay</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-tMyOolAm.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C5qFcFUg.css">
|
|
15
16
|
</head>
|
|
16
17
|
<body>
|
|
17
18
|
<div id="app"></div>
|
|
@@ -8,53 +8,25 @@ import fastifyCookie from '@fastify/cookie';
|
|
|
8
8
|
import fastify, {} from 'fastify';
|
|
9
9
|
import { registerGetHealth } from './features/health/get-health/endpoint.js';
|
|
10
10
|
import { registerGetBootstrap } from './features/catalog/get-bootstrap/endpoint.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { registerCheckoutBranch } from './features/git/checkout-branch/endpoint.js';
|
|
16
|
-
import { registerCloneRepository } from './features/git/clone-repository/endpoint.js';
|
|
17
|
-
import { registerGetSession } from './features/sessions/get-session/endpoint.js';
|
|
18
|
-
import { registerListSessions } from './features/sessions/list-sessions/endpoint.js';
|
|
19
|
-
import { registerListRecentThreads, } from './features/sessions/list-recent-threads/endpoint.js';
|
|
20
|
-
import { registerPromoteRecentThread } from './features/sessions/promote-recent-thread/endpoint.js';
|
|
21
|
-
import { registerGetHistory } from './features/sessions/get-history/endpoint.js';
|
|
22
|
-
import { registerStartSession } from './features/sessions/start-session/endpoint.js';
|
|
23
|
-
import { registerStartTurn } from './features/sessions/start-turn/endpoint.js';
|
|
24
|
-
import { registerSelectModel } from './features/sessions/select-model/endpoint.js';
|
|
25
|
-
import { registerInterruptTurn } from './features/sessions/interrupt-turn/endpoint.js';
|
|
26
|
-
import { registerStopSession } from './features/sessions/stop-session/endpoint.js';
|
|
27
|
-
import { registerRestoreSession } from './features/sessions/restore-session/endpoint.js';
|
|
28
|
-
import { registerReleaseSession } from './features/sessions/release-session/endpoint.js';
|
|
29
|
-
import { registerForgetSession } from './features/sessions/forget-session/endpoint.js';
|
|
30
|
-
import { registerRespondInteraction } from './features/sessions/respond-interaction/endpoint.js';
|
|
31
|
-
import { stopSession } from './features/sessions/lifecycle/use-case.js';
|
|
32
|
-
import { registerSessionEvents } from './features/sessions/session-events/endpoint.js';
|
|
33
|
-
import { registerGetPlan } from './features/plans/get-plan/endpoint.js';
|
|
34
|
-
import { registerGetWorkspacePlan } from './features/plans/get-workspace-plan/endpoint.js';
|
|
35
|
-
import { registerListWorkspacePlans } from './features/plans/list-workspace-plans/endpoint.js';
|
|
36
|
-
import { registerClosePlan } from './features/plans/close-plan/endpoint.js';
|
|
37
|
-
import { registerGetPlanMeasurement } from './features/plans/get-measurement/endpoint.js';
|
|
11
|
+
import { registerAuthRoutes } from './features/auth/register-routes.js';
|
|
12
|
+
import { registerGitRoutes } from './features/git/register-routes.js';
|
|
13
|
+
import { registerPlanRoutes } from './features/plans/register-routes.js';
|
|
14
|
+
import { registerSessionRoutes } from './features/sessions/register-routes.js';
|
|
38
15
|
import { registerProblemHandler } from './platform/http/problem-handler.js';
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { registerListSkillProfiles, } from './features/skills/list-profiles/endpoint.js';
|
|
42
|
-
import { registerReplaceSkillProfile, } from './features/skills/replace-profile/endpoint.js';
|
|
43
|
-
import { registerDeleteSkillProfile, } from './features/skills/delete-profile/endpoint.js';
|
|
44
|
-
import { registerRegistrationOptions } from './features/auth/register/options/endpoint.js';
|
|
45
|
-
import { registerRegistrationVerification } from './features/auth/register/verify/endpoint.js';
|
|
46
|
-
import { registerLoginOptions } from './features/auth/login/options/endpoint.js';
|
|
47
|
-
import { registerLoginVerification } from './features/auth/login/verify/endpoint.js';
|
|
48
|
-
import { registerAuthStatus, registerDisabledAuthStatus, } from './features/auth/status/endpoint.js';
|
|
49
|
-
import { registerLogout } from './features/auth/logout/endpoint.js';
|
|
50
|
-
import { registerListAuthorizedDevices } from './features/auth/devices/list/endpoint.js';
|
|
51
|
-
import { registerRenameAuthorizedDevice } from './features/auth/devices/rename/endpoint.js';
|
|
52
|
-
import { registerRevokeAuthorizedDevice } from './features/auth/devices/revoke/endpoint.js';
|
|
53
|
-
import { registerCreateEnrollmentTicket } from './features/auth/enrollment-tickets/endpoint.js';
|
|
54
|
-
import { registerEnrollmentTicketStatus } from './features/auth/enrollment-tickets/status/endpoint.js';
|
|
55
|
-
import { registerCancelEnrollmentTicket } from './features/auth/enrollment-tickets/cancel/endpoint.js';
|
|
16
|
+
import { registerAuthorizationBoundary } from './platform/http/authorization-boundary.js';
|
|
17
|
+
import { registerSkillRoutes } from './features/skills/register-routes.js';
|
|
56
18
|
export async function buildApp(deps) {
|
|
57
|
-
|
|
19
|
+
// Keep the default boundary finite even when an endpoint forgot a narrower schema.
|
|
20
|
+
const app = fastify({ logger: false, bodyLimit: 1024 * 1024 });
|
|
21
|
+
app.addHook('onSend', async (_request, reply, payload) => {
|
|
22
|
+
reply.header('Content-Security-Policy', "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'");
|
|
23
|
+
reply.header('X-Frame-Options', 'DENY');
|
|
24
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
25
|
+
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
26
|
+
// WebAuthn is same-origin; explicitly allow only this origin's ceremony calls.
|
|
27
|
+
reply.header('Permissions-Policy', 'publickey-credentials-get=(self), publickey-credentials-create=(self)');
|
|
28
|
+
return payload;
|
|
29
|
+
});
|
|
58
30
|
await app.register(fastifyCookie);
|
|
59
31
|
if (deps.staticDir)
|
|
60
32
|
await app.register(fastifyStatic, { root: deps.staticDir });
|
|
@@ -65,190 +37,13 @@ export async function buildApp(deps) {
|
|
|
65
37
|
publicOrigin: deps.auth.relyingParty.publicOrigin,
|
|
66
38
|
});
|
|
67
39
|
registerGetHealth(app, deps.health);
|
|
68
|
-
|
|
69
|
-
registerRegistrationOptions(app, deps.auth);
|
|
70
|
-
registerRegistrationVerification(app, deps.auth);
|
|
71
|
-
registerLoginOptions(app, deps.auth);
|
|
72
|
-
registerLoginVerification(app, deps.auth);
|
|
73
|
-
registerAuthStatus(app, deps.auth);
|
|
74
|
-
registerLogout(app, deps.auth);
|
|
75
|
-
registerListAuthorizedDevices(app, deps.auth);
|
|
76
|
-
registerRenameAuthorizedDevice(app, deps.auth);
|
|
77
|
-
registerRevokeAuthorizedDevice(app, deps.auth);
|
|
78
|
-
registerCreateEnrollmentTicket(app, deps.auth);
|
|
79
|
-
registerEnrollmentTicketStatus(app, deps.auth);
|
|
80
|
-
registerCancelEnrollmentTicket(app, deps.auth);
|
|
81
|
-
}
|
|
82
|
-
else if (deps.passkeyAuthDisabled)
|
|
83
|
-
registerDisabledAuthStatus(app);
|
|
40
|
+
registerAuthRoutes(app, deps);
|
|
84
41
|
if (deps.bootstrap)
|
|
85
42
|
registerGetBootstrap(app, deps.bootstrap);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const session = deps.sessionRoutes
|
|
91
|
-
?.list?.()
|
|
92
|
-
.find((candidate) => candidate.threadId === threadId);
|
|
93
|
-
if (!session)
|
|
94
|
-
return null;
|
|
95
|
-
return {
|
|
96
|
-
...(session.model === undefined ? {} : { model: session.model }),
|
|
97
|
-
...(session.effectiveSkillSelection?.selectedProfileName === undefined
|
|
98
|
-
? {}
|
|
99
|
-
: { skillProfile: session.effectiveSkillSelection.selectedProfileName }),
|
|
100
|
-
...(session.lastOrgPlan === undefined
|
|
101
|
-
? {}
|
|
102
|
-
: { orgPlanFilename: session.lastOrgPlan.filename }),
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
if (deps.sessionRoutes) {
|
|
107
|
-
registerStartSession(app, {
|
|
108
|
-
...deps.sessionRoutes,
|
|
109
|
-
reportFailure: (operation, error) => deps.logger.error(`${operation} failed: ${safeErrorLabel(error)}`),
|
|
110
|
-
});
|
|
111
|
-
registerGetSession(app, deps.sessionRoutes.find);
|
|
112
|
-
if (deps.sessionRoutes.list)
|
|
113
|
-
registerListSessions(app, { list: deps.sessionRoutes.list });
|
|
114
|
-
if (deps.sessionRoutes.readHistory)
|
|
115
|
-
registerGetHistory(app, {
|
|
116
|
-
find: deps.sessionRoutes.find,
|
|
117
|
-
read: deps.sessionRoutes.readHistory,
|
|
118
|
-
currentSequence: deps.sessionRoutes.currentSequence ?? (() => 0),
|
|
119
|
-
});
|
|
120
|
-
if (deps.sessionRoutes.startTurn)
|
|
121
|
-
registerStartTurn(app, {
|
|
122
|
-
find: deps.sessionRoutes.find,
|
|
123
|
-
start: deps.sessionRoutes.startTurn,
|
|
124
|
-
save: deps.sessionRoutes.save,
|
|
125
|
-
onStarted: deps.sessionRoutes.onTurnStarted,
|
|
126
|
-
});
|
|
127
|
-
if (deps.sessionRoutes.models)
|
|
128
|
-
registerSelectModel(app, {
|
|
129
|
-
find: deps.sessionRoutes.find,
|
|
130
|
-
models: () => deps.sessionRoutes.models.list(),
|
|
131
|
-
now: deps.sessionRoutes.now,
|
|
132
|
-
save: deps.sessionRoutes.save,
|
|
133
|
-
});
|
|
134
|
-
if (deps.sessionRoutes.interruptTurn)
|
|
135
|
-
registerInterruptTurn(app, {
|
|
136
|
-
find: deps.sessionRoutes.find,
|
|
137
|
-
interrupt: deps.sessionRoutes.interruptTurn,
|
|
138
|
-
});
|
|
139
|
-
if (deps.sessionRoutes.restore)
|
|
140
|
-
registerRestoreSession(app, {
|
|
141
|
-
find: deps.sessionRoutes.find,
|
|
142
|
-
restore: deps.sessionRoutes.restore,
|
|
143
|
-
save: deps.sessionRoutes.save,
|
|
144
|
-
idempotency: deps.sessionRoutes.idempotency,
|
|
145
|
-
});
|
|
146
|
-
if (deps.recentThreads && deps.sessionRoutes.promoteRecent)
|
|
147
|
-
registerPromoteRecentThread(app, {
|
|
148
|
-
list: () => deps.recentThreads.list(),
|
|
149
|
-
promote: deps.sessionRoutes.promoteRecent,
|
|
150
|
-
});
|
|
151
|
-
if (deps.sessionRoutes.release && deps.sessionRoutes.close)
|
|
152
|
-
registerReleaseSession(app, {
|
|
153
|
-
find: deps.sessionRoutes.find,
|
|
154
|
-
release: deps.sessionRoutes.release,
|
|
155
|
-
save: deps.sessionRoutes.save,
|
|
156
|
-
close: deps.sessionRoutes.close,
|
|
157
|
-
idempotency: deps.sessionRoutes.idempotency,
|
|
158
|
-
});
|
|
159
|
-
if (deps.sessionRoutes.remove && deps.sessionRoutes.close)
|
|
160
|
-
registerForgetSession(app, {
|
|
161
|
-
find: deps.sessionRoutes.find,
|
|
162
|
-
close: deps.sessionRoutes.close,
|
|
163
|
-
remove: deps.sessionRoutes.remove,
|
|
164
|
-
});
|
|
165
|
-
if (deps.sessionRoutes.close)
|
|
166
|
-
registerStopSession(app, {
|
|
167
|
-
find: deps.sessionRoutes.find,
|
|
168
|
-
stop: (session) => stopSession(session, deps.sessionRoutes.now()),
|
|
169
|
-
save: deps.sessionRoutes.save,
|
|
170
|
-
close: deps.sessionRoutes.close,
|
|
171
|
-
idempotency: deps.sessionRoutes.idempotency,
|
|
172
|
-
});
|
|
173
|
-
if (deps.sessionRoutes.replyInteraction && deps.interactions)
|
|
174
|
-
registerRespondInteraction(app, {
|
|
175
|
-
exists: (id) => deps.sessionRoutes.find(id) !== null,
|
|
176
|
-
resolve: (sessionId, requestId, resolvedAt) => deps.interactions.resolve(sessionId, requestId, resolvedAt),
|
|
177
|
-
validate: deps.interactions.validate,
|
|
178
|
-
reply: deps.sessionRoutes.replyInteraction,
|
|
179
|
-
resolved: deps.sessionRoutes.interactionResolved,
|
|
180
|
-
now: deps.sessionRoutes.now,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
if (deps.sessionEvents)
|
|
184
|
-
registerSessionEvents(app, {
|
|
185
|
-
...deps.sessionEvents,
|
|
186
|
-
...(deps.auth
|
|
187
|
-
? {
|
|
188
|
-
publicOrigin: deps.auth.relyingParty.publicOrigin,
|
|
189
|
-
authorized: (cookieHeader) => authorizationSessionDevice(cookieHeader, deps.auth) !== null,
|
|
190
|
-
}
|
|
191
|
-
: {}),
|
|
192
|
-
});
|
|
193
|
-
if (deps.planRoutes) {
|
|
194
|
-
registerGetPlan(app, deps.planRoutes);
|
|
195
|
-
registerClosePlan(app, deps.planRoutes);
|
|
196
|
-
}
|
|
197
|
-
if (deps.workspacePlanRoutes) {
|
|
198
|
-
registerListWorkspacePlans(app, deps.workspacePlanRoutes);
|
|
199
|
-
registerGetWorkspacePlan(app, deps.workspacePlanRoutes);
|
|
200
|
-
}
|
|
201
|
-
if (deps.planMeasurementRoutes)
|
|
202
|
-
registerGetPlanMeasurement(app, deps.planMeasurementRoutes);
|
|
203
|
-
if (deps.gitSummary)
|
|
204
|
-
registerGetGitSummary(app, {
|
|
205
|
-
workspaces: deps.gitSummary.workspaces,
|
|
206
|
-
inspect: deps.gitSummary.inspect,
|
|
207
|
-
});
|
|
208
|
-
if (deps.gitSummary)
|
|
209
|
-
registerPushUpstream(app, {
|
|
210
|
-
workspaces: deps.gitSummary.workspaces,
|
|
211
|
-
inspect: deps.gitSummary.inspectForPush ?? deps.gitSummary.inspect,
|
|
212
|
-
push: deps.gitSummary.push,
|
|
213
|
-
idempotency: deps.gitSummary.idempotency,
|
|
214
|
-
});
|
|
215
|
-
if (deps.gitSummary)
|
|
216
|
-
registerRefreshGit(app, {
|
|
217
|
-
workspaces: deps.gitSummary.workspaces,
|
|
218
|
-
refresh: deps.gitSummary.refresh,
|
|
219
|
-
idempotency: deps.gitSummary.idempotency,
|
|
220
|
-
});
|
|
221
|
-
if (deps.gitSummary?.pull && deps.gitSummary.checkout) {
|
|
222
|
-
registerPullRebase(app, {
|
|
223
|
-
workspaces: deps.gitSummary.workspaces,
|
|
224
|
-
pull: deps.gitSummary.pull,
|
|
225
|
-
});
|
|
226
|
-
registerCheckoutBranch(app, {
|
|
227
|
-
workspaces: deps.gitSummary.workspaces,
|
|
228
|
-
checkout: deps.gitSummary.checkout,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
if (deps.gitSummary?.clone)
|
|
232
|
-
registerCloneRepository(app, {
|
|
233
|
-
workspaces: deps.gitSummary.workspaces,
|
|
234
|
-
clone: deps.gitSummary.clone,
|
|
235
|
-
});
|
|
236
|
-
if (deps.skills) {
|
|
237
|
-
registerListAvailableSkills(app, deps.skills);
|
|
238
|
-
registerListSkillProfiles(app, deps.skills);
|
|
239
|
-
registerReplaceSkillProfile(app, deps.skills);
|
|
240
|
-
registerDeleteSkillProfile(app, deps.skills);
|
|
241
|
-
}
|
|
43
|
+
registerSessionRoutes(app, deps);
|
|
44
|
+
registerPlanRoutes(app, deps);
|
|
45
|
+
registerGitRoutes(app, deps);
|
|
46
|
+
registerSkillRoutes(app, deps);
|
|
242
47
|
registerProblemHandler(app, Boolean(deps.staticDir));
|
|
243
48
|
return app;
|
|
244
49
|
}
|
|
245
|
-
function safeErrorLabel(error) {
|
|
246
|
-
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
247
|
-
const code = error.code;
|
|
248
|
-
if (typeof code === 'string' && /^[A-Z0-9_]+$/.test(code))
|
|
249
|
-
return code;
|
|
250
|
-
}
|
|
251
|
-
if (error instanceof Error && /^[A-Z0-9_]+$/.test(error.message))
|
|
252
|
-
return error.message;
|
|
253
|
-
return error instanceof Error ? error.name : 'UNKNOWN_ERROR';
|
|
254
|
-
}
|
|
@@ -99,7 +99,9 @@ async function listSkillProfiles(store, stdout, stderr) {
|
|
|
99
99
|
if (!profile)
|
|
100
100
|
continue;
|
|
101
101
|
stdout.write(`${profile.name}\n ${store.globalProfilePath(name)}\n`);
|
|
102
|
-
for (const skill of [...profile.skills]
|
|
102
|
+
for (const skill of [...profile.skills]
|
|
103
|
+
.filter((entry) => entry.enabled)
|
|
104
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)))
|
|
103
105
|
stdout.write(` ${skill.name}\t${skill.path}\n`);
|
|
104
106
|
}
|
|
105
107
|
catch (error) {
|
|
@@ -40,12 +40,11 @@ import { CodexSkillCatalog } from './platform/skills/codex-skill-catalog.js';
|
|
|
40
40
|
import { CachedSkillCatalog } from './platform/skills/cached-skill-catalog.js';
|
|
41
41
|
import { compileSkillOverride } from './features/skills/model/skill-profile.js';
|
|
42
42
|
import { SupervisedPlanRegistry } from './features/plans/application/supervised-plan-registry.js';
|
|
43
|
-
import { NativePlanAlignmentRegistry } from './features/plans/application/native-plan-alignment.js';
|
|
44
43
|
import { FilesystemPlanStatusSource } from './platform/plans/filesystem-plan-status-source.js';
|
|
45
44
|
import { FilesystemWorkspacePlanCatalog } from './platform/plans/filesystem-workspace-plan-catalog.js';
|
|
46
45
|
import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-command.js';
|
|
47
46
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
48
|
-
import { createRelyingPartyConfig
|
|
47
|
+
import { createRelyingPartyConfig } from './config.js';
|
|
49
48
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
50
49
|
export async function composeRelayApp(options) {
|
|
51
50
|
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
@@ -74,7 +73,6 @@ export async function composeRelayApp(options) {
|
|
|
74
73
|
const interactions = new SqlitePendingInteractionStore(database);
|
|
75
74
|
const idempotency = new SqliteIdempotencyStore(database);
|
|
76
75
|
const supervisedPlans = new SupervisedPlanRegistry();
|
|
77
|
-
const nativePlanAlignment = new NativePlanAlignmentRegistry();
|
|
78
76
|
const planStatusSource = new FilesystemPlanStatusSource(join(dirname(databasePath), 'plans'));
|
|
79
77
|
const workspacePlanCatalog = new FilesystemWorkspacePlanCatalog();
|
|
80
78
|
const planMeasurementHelperPath = options.planMeasurementHelperPath ?? process.env.GESTALT_MOBILE_ORG_PLAN_HELPER;
|
|
@@ -114,8 +112,6 @@ export async function composeRelayApp(options) {
|
|
|
114
112
|
const runtime = options.startAppServers
|
|
115
113
|
? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification) => {
|
|
116
114
|
const occurredAt = new Date().toISOString();
|
|
117
|
-
if (notification.method === 'turn/plan/updated')
|
|
118
|
-
nativePlanAlignment.observe(sessionId, sessions.find(sessionId)?.threadId ?? null, notification.params);
|
|
119
115
|
const normalized = normalizeCodexNotification(sessionId, 0, occurredAt, notification, sessions.find(sessionId)?.workspacePath);
|
|
120
116
|
if (!normalized)
|
|
121
117
|
return;
|
|
@@ -141,7 +137,6 @@ export async function composeRelayApp(options) {
|
|
|
141
137
|
supervisedPlans.accept(sessionId, update);
|
|
142
138
|
planMeasurementRefresh?.accept(sessionId, update);
|
|
143
139
|
if (update.kind === 'updated') {
|
|
144
|
-
nativePlanAlignment.replace(sessionId, update.plan);
|
|
145
140
|
void runtime?.syncThreadPlanName(sessionId, update.plan);
|
|
146
141
|
const occurredAt = new Date().toISOString();
|
|
147
142
|
const session = sessions.find(sessionId);
|
|
@@ -199,29 +194,33 @@ export async function composeRelayApp(options) {
|
|
|
199
194
|
let app;
|
|
200
195
|
try {
|
|
201
196
|
app = await buildApp({
|
|
202
|
-
...(authorization
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
197
|
+
...(authorization
|
|
198
|
+
? {
|
|
199
|
+
auth: {
|
|
200
|
+
repository: authorization,
|
|
201
|
+
clock: { now: options.authorizationClock ?? (() => new Date()) },
|
|
202
|
+
random: {
|
|
203
|
+
bytes: (length) => {
|
|
204
|
+
const value = authorizationRandom(length);
|
|
205
|
+
if (length !== 32 || value.length !== 32)
|
|
206
|
+
throw new Error('Authorization randomness must return exactly 32 bytes');
|
|
207
|
+
return value;
|
|
208
|
+
},
|
|
211
209
|
},
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
identifiers: {
|
|
211
|
+
sessionId: () => {
|
|
212
|
+
const value = authorizationRandom(32);
|
|
213
|
+
if (value.length !== 32)
|
|
214
|
+
throw new Error('Authorization randomness must return exactly 32 bytes');
|
|
215
|
+
return authorizationSessionId(Buffer.from(value).toString('base64url'));
|
|
216
|
+
},
|
|
217
|
+
deviceId: () => authorizedDeviceId(randomUUID()),
|
|
219
218
|
},
|
|
220
|
-
|
|
219
|
+
webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
|
|
220
|
+
relyingParty,
|
|
221
221
|
},
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
} } : { passkeyAuthDisabled: true }),
|
|
222
|
+
}
|
|
223
|
+
: { passkeyAuthDisabled: true }),
|
|
225
224
|
health: {
|
|
226
225
|
async read() {
|
|
227
226
|
return {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Process-local by design: the relay's authorization database is local state. */
|
|
2
|
+
export class ExpiringCeremonyAttemptGate {
|
|
3
|
+
limit;
|
|
4
|
+
windowMs;
|
|
5
|
+
#attempts = new Map();
|
|
6
|
+
constructor(limit = 8, windowMs = 10 * 60 * 1000) {
|
|
7
|
+
this.limit = limit;
|
|
8
|
+
this.windowMs = windowMs;
|
|
9
|
+
}
|
|
10
|
+
allow(key, now) {
|
|
11
|
+
const old = this.#attempts.get(key);
|
|
12
|
+
const expires = now.getTime() + this.windowMs;
|
|
13
|
+
if (!old || old.expires <= now.getTime()) {
|
|
14
|
+
this.#attempts.set(key, { count: 1, expires });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (old.count >= this.limit)
|
|
18
|
+
return false;
|
|
19
|
+
old.count++;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -28,7 +28,9 @@ export function registerRevokeAuthorizedDevice(app, deps) {
|
|
|
28
28
|
return unavailable(reply, 404, 'DEVICE_NOT_AVAILABLE');
|
|
29
29
|
if (current === id)
|
|
30
30
|
reply.clearCookie('gestalt_mobile_session', {
|
|
31
|
-
path: '/',
|
|
31
|
+
path: '/',
|
|
32
|
+
httpOnly: true,
|
|
33
|
+
sameSite: 'strict',
|
|
32
34
|
secure: deps.relyingParty.publicOrigin.startsWith('https://'),
|
|
33
35
|
});
|
|
34
36
|
return reply.code(204).send();
|
|
@@ -5,3 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export class AuthorizationDomainError extends Error {
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* A finite persistent ceremony pool prevents abandoned browser attempts from
|
|
10
|
+
* indefinitely consuming local authorization state. Endpoints intentionally
|
|
11
|
+
* map this to their existing non-enumerating failure response.
|
|
12
|
+
*/
|
|
13
|
+
export class CeremonyCapacityError extends AuthorizationDomainError {
|
|
14
|
+
}
|
|
@@ -4,7 +4,8 @@ export function registerCreateEnrollmentTicket(app, deps) {
|
|
|
4
4
|
app.post('/api/auth/enrollment-tickets', async (request, reply) => {
|
|
5
5
|
const rawSession = request.cookies.gestalt_mobile_session;
|
|
6
6
|
const session = parseAuthorizationSessionId(rawSession);
|
|
7
|
-
if (session === null ||
|
|
7
|
+
if (session === null ||
|
|
8
|
+
deps.repository.sessionDevice(session, deps.clock.now().toISOString()) === null)
|
|
8
9
|
return reply.code(401).send();
|
|
9
10
|
const entropy = deps.random.bytes(32);
|
|
10
11
|
if (entropy.length < 32)
|
|
@@ -12,6 +13,10 @@ export function registerCreateEnrollmentTicket(app, deps) {
|
|
|
12
13
|
const ticket = enrollmentTicketId(Buffer.from(entropy).toString('base64url'));
|
|
13
14
|
const expiresAt = new Date(deps.clock.now().getTime() + lifetimeMs).toISOString();
|
|
14
15
|
deps.repository.issueEnrollmentTicket(ticket, session, expiresAt);
|
|
15
|
-
return reply.code(201).send({
|
|
16
|
+
return reply.code(201).send({
|
|
17
|
+
ticket,
|
|
18
|
+
url: `${deps.relyingParty.publicOrigin.replace(/\/$/, '')}/#enroll=${ticket}`,
|
|
19
|
+
expiresAt,
|
|
20
|
+
});
|
|
16
21
|
});
|
|
17
22
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { parseAuthorizationSessionId } from '../../domain/identifiers.js';
|
|
3
|
-
const responseSchema = z
|
|
3
|
+
const responseSchema = z
|
|
4
|
+
.object({ status: z.enum(['none', 'pending', 'used', 'expired']) })
|
|
5
|
+
.strict();
|
|
4
6
|
export function registerEnrollmentTicketStatus(app, deps) {
|
|
5
7
|
app.get('/api/auth/enrollment-tickets/current', async (request, reply) => {
|
|
6
8
|
const rawSession = request.cookies.gestalt_mobile_session;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const seconds = {
|
|
2
|
+
gestalt_mobile_login: 600,
|
|
3
|
+
gestalt_mobile_registration: 600,
|
|
4
|
+
gestalt_mobile_session: 30 * 24 * 60 * 60,
|
|
5
|
+
};
|
|
6
|
+
function options(name, origin) {
|
|
7
|
+
return {
|
|
8
|
+
httpOnly: true,
|
|
9
|
+
sameSite: 'strict',
|
|
10
|
+
path: '/',
|
|
11
|
+
secure: origin.startsWith('https://'),
|
|
12
|
+
maxAge: seconds[name],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function setAuthCookie(reply, name, value, origin) {
|
|
16
|
+
reply.setCookie(name, value, options(name, origin));
|
|
17
|
+
}
|
|
18
|
+
export function clearAuthCookie(reply, name, origin) {
|
|
19
|
+
reply.clearCookie(name, options(name, origin));
|
|
20
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { sendProblem } from '../../../../platform/http/problem-reply.js';
|
|
3
3
|
import { passkeyCeremonyId } from '../../domain/identifiers.js';
|
|
4
|
+
import { CeremonyCapacityError } from '../../domain/errors.js';
|
|
5
|
+
import { setAuthCookie } from '../../http/cookies.js';
|
|
4
6
|
const lifetimeMs = 10 * 60 * 1000;
|
|
5
7
|
const responseSchema = z
|
|
6
8
|
.object({ options: z.object({ challenge: z.string().min(1) }).passthrough() })
|
|
7
9
|
.strict();
|
|
8
10
|
export function registerLoginOptions(app, deps) {
|
|
9
|
-
app.post('/api/auth/login/options', async (
|
|
11
|
+
app.post('/api/auth/login/options', { bodyLimit: 1024 }, async (request, reply) => {
|
|
12
|
+
if (!deps.ceremonyAttempts.allow(`login:${request.ip}`, deps.clock.now()))
|
|
13
|
+
return sendProblem(reply, 'AUTHENTICATION_FAILED', 400, 'Authentication could not be completed.');
|
|
10
14
|
if (deps.repository.listAuthorizedDevices().length === 0)
|
|
11
|
-
return reply
|
|
12
|
-
.code(400)
|
|
13
|
-
.type('application/problem+json')
|
|
14
|
-
.send(problem('AUTHENTICATION_FAILED', 400, 'Authentication could not be completed.'));
|
|
15
|
+
return sendProblem(reply, 'AUTHENTICATION_FAILED', 400, 'Authentication could not be completed.');
|
|
15
16
|
const correlation = deps.random.bytes(32);
|
|
16
17
|
const challenge = deps.random.bytes(32);
|
|
17
18
|
if (correlation.length !== 32 || challenge.length !== 32)
|
|
@@ -25,20 +26,21 @@ export function registerLoginOptions(app, deps) {
|
|
|
25
26
|
}),
|
|
26
27
|
});
|
|
27
28
|
const token = passkeyCeremonyId(Buffer.from(correlation).toString('base64url'));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
29
|
+
try {
|
|
30
|
+
deps.repository.saveCeremony(token, {
|
|
31
|
+
purpose: 'authentication',
|
|
32
|
+
challenge,
|
|
33
|
+
expectedOrigin: deps.relyingParty.publicOrigin,
|
|
34
|
+
rpId: deps.relyingParty.rpId,
|
|
35
|
+
expiresAt: new Date(now.getTime() + lifetimeMs).toISOString(),
|
|
36
|
+
}, now.toISOString());
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof CeremonyCapacityError)
|
|
40
|
+
return sendProblem(reply, 'AUTHENTICATION_FAILED', 400, 'Authentication could not be completed.');
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
setAuthCookie(reply, 'gestalt_mobile_login', token, deps.relyingParty.publicOrigin);
|
|
42
44
|
return reply.send(options);
|
|
43
45
|
});
|
|
44
46
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { sendProblem } from '../../../../platform/http/problem-reply.js';
|
|
3
3
|
import { PasskeyVerificationError, } from '../../application/ports.js';
|
|
4
4
|
import { passkeyCeremonyId, webAuthnCredentialId } from '../../domain/identifiers.js';
|
|
5
|
+
import { clearAuthCookie, setAuthCookie } from '../../http/cookies.js';
|
|
5
6
|
const responseSchema = z
|
|
6
7
|
.object({
|
|
7
8
|
id: z.string().min(1),
|
|
@@ -22,13 +23,10 @@ const responseSchema = z
|
|
|
22
23
|
const requestSchema = z.object({ response: responseSchema }).strict();
|
|
23
24
|
const lifetimeMs = 30 * 24 * 60 * 60 * 1000;
|
|
24
25
|
export function registerLoginVerification(app, deps) {
|
|
25
|
-
app.post('/api/auth/login/verify', async (request, reply) => {
|
|
26
|
+
app.post('/api/auth/login/verify', { bodyLimit: 64 * 1024 }, async (request, reply) => {
|
|
26
27
|
const parsed = requestSchema.safeParse(request.body);
|
|
27
28
|
const token = request.cookies.gestalt_mobile_login;
|
|
28
|
-
const fail = () => reply
|
|
29
|
-
.code(400)
|
|
30
|
-
.type('application/problem+json')
|
|
31
|
-
.send(problem('AUTHENTICATION_FAILED', 400, 'Authentication could not be completed.'));
|
|
29
|
+
const fail = () => sendProblem(reply, 'AUTHENTICATION_FAILED', 400, 'Authentication could not be completed.');
|
|
32
30
|
if (!parsed.success || typeof token !== 'string')
|
|
33
31
|
return fail();
|
|
34
32
|
const now = deps.clock.now();
|
|
@@ -69,14 +67,8 @@ export function registerLoginVerification(app, deps) {
|
|
|
69
67
|
});
|
|
70
68
|
if (!authenticated)
|
|
71
69
|
return fail();
|
|
72
|
-
reply
|
|
73
|
-
|
|
74
|
-
sameSite: 'strict',
|
|
75
|
-
path: '/',
|
|
76
|
-
secure: deps.relyingParty.publicOrigin.startsWith('https://'),
|
|
77
|
-
maxAge: 2592000,
|
|
78
|
-
});
|
|
79
|
-
reply.clearCookie('gestalt_mobile_login', { path: '/' });
|
|
70
|
+
setAuthCookie(reply, 'gestalt_mobile_session', session, deps.relyingParty.publicOrigin);
|
|
71
|
+
clearAuthCookie(reply, 'gestalt_mobile_login', deps.relyingParty.publicOrigin);
|
|
80
72
|
return reply.send({ status: 'authenticated' });
|
|
81
73
|
}
|
|
82
74
|
catch (error) {
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { parseAuthorizationSessionId } from '../domain/identifiers.js';
|
|
2
|
+
import { clearAuthCookie } from '../http/cookies.js';
|
|
2
3
|
export function registerLogout(app, deps) {
|
|
3
4
|
app.post('/api/auth/logout', async (request, reply) => {
|
|
4
5
|
const token = request.cookies.gestalt_mobile_session;
|
|
5
6
|
const session = parseAuthorizationSessionId(token);
|
|
6
7
|
if (session !== null)
|
|
7
8
|
deps.repository.revokeSession(session, deps.clock.now().toISOString());
|
|
8
|
-
reply
|
|
9
|
-
path: '/',
|
|
10
|
-
httpOnly: true,
|
|
11
|
-
sameSite: 'strict',
|
|
12
|
-
secure: deps.relyingParty.publicOrigin.startsWith('https://'),
|
|
13
|
-
});
|
|
9
|
+
clearAuthCookie(reply, 'gestalt_mobile_session', deps.relyingParty.publicOrigin);
|
|
14
10
|
return reply.code(204).send();
|
|
15
11
|
});
|
|
16
12
|
}
|