gestalt-mobile 0.13.0 → 0.15.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-2L2KZzwK.css +1 -0
- package/dist/client/assets/index-Dnm_kS5p.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/icons/gestalt-mobile-180.png +0 -0
- package/dist/client/icons/gestalt-mobile-192.png +0 -0
- package/dist/client/icons/gestalt-mobile-512.png +0 -0
- package/dist/client/icons/gestalt-mobile-maskable-512.png +0 -0
- package/dist/client/icons/gestalt-mobile-maskable.svg +10 -0
- package/dist/client/icons/gestalt-mobile.svg +10 -0
- package/dist/client/index.html +9 -3
- package/dist/client/manifest.webmanifest +32 -0
- package/dist/client/service-worker.js +9 -0
- package/dist/server/server/cli.js +3 -1
- package/dist/server/server/composition.js +25 -21
- package/dist/server/server/features/auth/application/ports.js +7 -1
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
- 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 +12 -2
- package/dist/server/server/features/auth/login/verify/endpoint.js +1 -1
- package/dist/server/server/features/auth/register/verify/endpoint.js +89 -19
- package/dist/server/server/features/auth/register-routes.js +4 -1
- 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/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/start-session/use-case.js +1 -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/platform/auth/simple-webauthn-adapter.js +55 -9
- package/dist/server/server/platform/catalog/profile-command.js +8 -1
- package/dist/server/server/platform/codex/normalizer.js +22 -4
- package/dist/server/server/platform/codex/session-runtime.js +52 -11
- package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
- package/dist/server/server/platform/http/problem-reply.js +4 -1
- 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 +3 -1
- package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
- package/dist/client/assets/index-D2sT9ZHY.js +0 -19
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 512 512">
|
|
8
|
+
<rect width="512" height="512" fill="#141414" />
|
|
9
|
+
<image href="../branding/p_glogo_white.svg" x="102" y="102" width="308" height="308" />
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 512 512">
|
|
8
|
+
<rect width="512" height="512" fill="#141414" />
|
|
9
|
+
<image href="../branding/p_glogo_white.svg" x="64" y="64" width="384" height="384" />
|
|
10
|
+
</svg>
|
package/dist/client/index.html
CHANGED
|
@@ -9,9 +9,15 @@ 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
|
-
<
|
|
13
|
-
<
|
|
14
|
-
<
|
|
12
|
+
<meta name="color-scheme" content="light dark" />
|
|
13
|
+
<meta name="theme-color" content="#141414" />
|
|
14
|
+
<meta name="application-name" content="Gestalt Mobile" />
|
|
15
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
16
|
+
<link rel="icon" href="/icons/gestalt-mobile-192.png" />
|
|
17
|
+
<link rel="apple-touch-icon" href="/icons/gestalt-mobile-180.png" />
|
|
18
|
+
<title>Gestalt Mobile</title>
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-Dnm_kS5p.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-2L2KZzwK.css">
|
|
15
21
|
</head>
|
|
16
22
|
<body>
|
|
17
23
|
<div id="app"></div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/",
|
|
3
|
+
"name": "Gestalt Mobile",
|
|
4
|
+
"short_name": "Gestalt",
|
|
5
|
+
"description": "A mobile interface for durable Codex development sessions.",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#141414",
|
|
10
|
+
"theme_color": "#141414",
|
|
11
|
+
"categories": ["productivity", "utilities"],
|
|
12
|
+
"icons": [
|
|
13
|
+
{
|
|
14
|
+
"src": "/icons/gestalt-mobile-192.png",
|
|
15
|
+
"sizes": "192x192",
|
|
16
|
+
"type": "image/png",
|
|
17
|
+
"purpose": "any"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"src": "/icons/gestalt-mobile-512.png",
|
|
21
|
+
"sizes": "512x512",
|
|
22
|
+
"type": "image/png",
|
|
23
|
+
"purpose": "any"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"src": "/icons/gestalt-mobile-maskable-512.png",
|
|
27
|
+
"sizes": "512x512",
|
|
28
|
+
"type": "image/png",
|
|
29
|
+
"purpose": "maskable"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
|
|
7
|
+
// Installability only: authenticated relay data must always remain network-fresh.
|
|
8
|
+
self.addEventListener('install', () => self.skipWaiting());
|
|
9
|
+
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
|
|
@@ -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) {
|
|
@@ -44,7 +44,7 @@ import { FilesystemPlanStatusSource } from './platform/plans/filesystem-plan-sta
|
|
|
44
44
|
import { FilesystemWorkspacePlanCatalog } from './platform/plans/filesystem-workspace-plan-catalog.js';
|
|
45
45
|
import { checkpointPlanMeasurement } from './platform/plans/plan-measurement-command.js';
|
|
46
46
|
import { PlanMeasurementRefresh } from './platform/plans/plan-measurement-refresh.js';
|
|
47
|
-
import { createRelyingPartyConfig
|
|
47
|
+
import { createRelyingPartyConfig } from './config.js';
|
|
48
48
|
const generatedProtocolVersion = 'codex-cli 0.144.3';
|
|
49
49
|
export async function composeRelayApp(options) {
|
|
50
50
|
const passkeyAuthEnabled = options.passkeyAuthEnabled ?? true;
|
|
@@ -194,29 +194,33 @@ export async function composeRelayApp(options) {
|
|
|
194
194
|
let app;
|
|
195
195
|
try {
|
|
196
196
|
app = await buildApp({
|
|
197
|
-
...(authorization
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
},
|
|
206
209
|
},
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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()),
|
|
214
218
|
},
|
|
215
|
-
|
|
219
|
+
webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
|
|
220
|
+
relyingParty,
|
|
216
221
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
} } : { passkeyAuthDisabled: true }),
|
|
222
|
+
}
|
|
223
|
+
: { passkeyAuthDisabled: true }),
|
|
220
224
|
health: {
|
|
221
225
|
async read() {
|
|
222
226
|
return {
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
-
/** Expected proof failure; adapters
|
|
6
|
+
/** Expected proof failure; adapters translate library details to a safe diagnostic category. */
|
|
7
7
|
export class PasskeyVerificationError extends Error {
|
|
8
|
+
reason;
|
|
9
|
+
constructor(reason = 'VERIFIER_REJECTED') {
|
|
10
|
+
super('PASSKEY_VERIFICATION_FAILED');
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
this.name = 'PasskeyVerificationError';
|
|
13
|
+
}
|
|
8
14
|
}
|
|
@@ -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();
|
|
@@ -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;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
const seconds = {
|
|
1
|
+
const seconds = {
|
|
2
|
+
gestalt_mobile_login: 600,
|
|
3
|
+
gestalt_mobile_registration: 600,
|
|
4
|
+
gestalt_mobile_session: 30 * 24 * 60 * 60,
|
|
5
|
+
};
|
|
2
6
|
function options(name, origin) {
|
|
3
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
httpOnly: true,
|
|
9
|
+
sameSite: 'strict',
|
|
10
|
+
path: '/',
|
|
11
|
+
secure: origin.startsWith('https://'),
|
|
12
|
+
maxAge: seconds[name],
|
|
13
|
+
};
|
|
4
14
|
}
|
|
5
15
|
export function setAuthCookie(reply, name, value, origin) {
|
|
6
16
|
reply.setCookie(name, value, options(name, origin));
|
|
@@ -50,7 +50,7 @@ export function registerLoginVerification(app, deps) {
|
|
|
50
50
|
},
|
|
51
51
|
});
|
|
52
52
|
if (!verified.userVerified || verified.credentialId !== device.credentialId)
|
|
53
|
-
throw new PasskeyVerificationError('
|
|
53
|
+
throw new PasskeyVerificationError('USER_VERIFICATION_REQUIRED');
|
|
54
54
|
const session = deps.identifiers.sessionId();
|
|
55
55
|
if (Buffer.from(session, 'base64url').length !== 32)
|
|
56
56
|
throw new Error('AUTHORIZATION_RANDOMNESS_INVALID');
|
|
@@ -1,56 +1,126 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { problem } from '../../../../platform/http/problem.js';
|
|
3
|
-
import { PasskeyVerificationError } from '../../application/ports.js';
|
|
3
|
+
import { PasskeyVerificationError, } from '../../application/ports.js';
|
|
4
4
|
import { deviceNickname } from '../../domain/device-nickname.js';
|
|
5
5
|
import { passkeyCeremonyId } from '../../domain/identifiers.js';
|
|
6
6
|
import { AuthorizationDomainError } from '../../domain/errors.js';
|
|
7
7
|
import { clearAuthCookie, setAuthCookie } from '../../http/cookies.js';
|
|
8
|
-
const registrationResponseSchema = z
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const registrationResponseSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
id: z.string().min(1),
|
|
11
|
+
rawId: z.string().min(1),
|
|
12
|
+
type: z.literal('public-key'),
|
|
13
|
+
response: z
|
|
14
|
+
.object({
|
|
15
|
+
clientDataJSON: z.string().min(1),
|
|
16
|
+
attestationObject: z.string().min(1),
|
|
17
|
+
authenticatorData: z.string().min(1).optional(),
|
|
18
|
+
transports: z
|
|
19
|
+
.array(z.enum(['ble', 'hybrid', 'internal', 'nfc', 'usb', 'cable', 'smart-card']))
|
|
20
|
+
.optional(),
|
|
21
|
+
publicKeyAlgorithm: z.number().int().optional(),
|
|
22
|
+
publicKey: z.string().min(1).optional(),
|
|
23
|
+
})
|
|
24
|
+
.strict(),
|
|
25
|
+
authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(),
|
|
26
|
+
clientExtensionResults: z.record(z.string(), z.unknown()),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
const requestSchema = z
|
|
30
|
+
.object({ response: registrationResponseSchema, nickname: z.string() })
|
|
31
|
+
.strict();
|
|
14
32
|
const successSchema = z.object({ status: z.literal('authenticated') }).strict();
|
|
15
33
|
const sessionLifetimeMs = 30 * 24 * 60 * 60 * 1000;
|
|
16
34
|
export function registerRegistrationVerification(app, deps) {
|
|
17
35
|
app.post('/api/auth/register/verify', { bodyLimit: 128 * 1024 }, async (request, reply) => {
|
|
18
36
|
const parsed = requestSchema.safeParse(request.body);
|
|
19
37
|
const token = request.cookies.gestalt_mobile_registration;
|
|
20
|
-
if (!parsed.success
|
|
21
|
-
|
|
38
|
+
if (!parsed.success) {
|
|
39
|
+
deps.reportVerificationFailure?.('INVALID_RESPONSE_SHAPE');
|
|
40
|
+
return reply
|
|
41
|
+
.code(400)
|
|
42
|
+
.type('application/problem+json')
|
|
43
|
+
.send(problem('INVALID_REGISTRATION_REQUEST', 400, 'The registration request is invalid.'));
|
|
44
|
+
}
|
|
45
|
+
if (typeof token !== 'string') {
|
|
46
|
+
deps.reportVerificationFailure?.('MISSING_CEREMONY_COOKIE');
|
|
47
|
+
return reply
|
|
48
|
+
.code(400)
|
|
49
|
+
.type('application/problem+json')
|
|
50
|
+
.send(problem('REGISTRATION_COOKIE_MISSING', 400, 'The registration cookie is missing.'));
|
|
51
|
+
}
|
|
22
52
|
let nickname;
|
|
23
53
|
try {
|
|
24
54
|
nickname = deviceNickname(parsed.data.nickname);
|
|
25
55
|
}
|
|
26
56
|
catch (error) {
|
|
27
57
|
if (error instanceof AuthorizationDomainError)
|
|
28
|
-
return reply
|
|
58
|
+
return reply
|
|
59
|
+
.code(400)
|
|
60
|
+
.type('application/problem+json')
|
|
61
|
+
.send(problem('INVALID_DEVICE_NICKNAME', 400, 'The device nickname is invalid.'));
|
|
29
62
|
throw error;
|
|
30
63
|
}
|
|
31
64
|
const now = deps.clock.now();
|
|
32
65
|
const ceremony = deps.repository.readCeremony(passkeyCeremonyId(token), now.toISOString());
|
|
33
|
-
if (!ceremony || ceremony.purpose !== 'registration')
|
|
34
|
-
|
|
66
|
+
if (!ceremony || ceremony.purpose !== 'registration') {
|
|
67
|
+
deps.reportVerificationFailure?.('CEREMONY_NOT_AVAILABLE');
|
|
68
|
+
return reply
|
|
69
|
+
.code(400)
|
|
70
|
+
.type('application/problem+json')
|
|
71
|
+
.send(problem('REGISTRATION_NOT_AVAILABLE', 400, 'Registration could not be completed.'));
|
|
72
|
+
}
|
|
35
73
|
try {
|
|
36
|
-
const verified = await deps.webauthn.verifyRegistration({
|
|
74
|
+
const verified = await deps.webauthn.verifyRegistration({
|
|
75
|
+
response: parsed.data.response,
|
|
76
|
+
challenge: ceremony.challenge,
|
|
77
|
+
expectedOrigin: ceremony.expectedOrigin,
|
|
78
|
+
rpId: ceremony.rpId,
|
|
79
|
+
});
|
|
37
80
|
if (!verified.userVerified)
|
|
38
|
-
throw new PasskeyVerificationError('
|
|
39
|
-
const device = {
|
|
81
|
+
throw new PasskeyVerificationError('USER_VERIFICATION_REQUIRED');
|
|
82
|
+
const device = {
|
|
83
|
+
id: deps.identifiers.deviceId(),
|
|
84
|
+
credentialId: verified.credentialId,
|
|
85
|
+
publicKey: verified.publicKey,
|
|
86
|
+
counter: verified.counter,
|
|
87
|
+
transports: verified.transports,
|
|
88
|
+
deviceType: verified.deviceType,
|
|
89
|
+
backedUp: verified.backedUp,
|
|
90
|
+
nickname,
|
|
91
|
+
createdAt: now.toISOString(),
|
|
92
|
+
};
|
|
40
93
|
const session = deps.identifiers.sessionId();
|
|
41
94
|
if (Buffer.from(session, 'base64url').length !== 32)
|
|
42
95
|
throw new Error('AUTHORIZATION_RANDOMNESS_INVALID');
|
|
43
96
|
const expiresAt = new Date(now.getTime() + sessionLifetimeMs).toISOString();
|
|
44
|
-
const outcome = deps.repository.completeRegistration({
|
|
97
|
+
const outcome = deps.repository.completeRegistration({
|
|
98
|
+
ceremony: passkeyCeremonyId(token),
|
|
99
|
+
now: now.toISOString(),
|
|
100
|
+
device,
|
|
101
|
+
session: { id: session, deviceId: device.id, expiresAt },
|
|
102
|
+
});
|
|
45
103
|
if (outcome !== 'registered')
|
|
46
|
-
return reply
|
|
104
|
+
return reply
|
|
105
|
+
.code(outcome === 'bootstrapAlreadyClaimed' ? 409 : 400)
|
|
106
|
+
.type('application/problem+json')
|
|
107
|
+
.send(problem(outcome === 'bootstrapAlreadyClaimed'
|
|
108
|
+
? 'BOOTSTRAP_ALREADY_CLAIMED'
|
|
109
|
+
: 'REGISTRATION_NOT_AVAILABLE', outcome === 'bootstrapAlreadyClaimed' ? 409 : 400, 'Registration could not be completed.'));
|
|
47
110
|
setAuthCookie(reply, 'gestalt_mobile_session', session, deps.relyingParty.publicOrigin);
|
|
48
111
|
clearAuthCookie(reply, 'gestalt_mobile_registration', deps.relyingParty.publicOrigin);
|
|
49
112
|
return reply.code(201).send(successSchema.parse({ status: 'authenticated' }));
|
|
50
113
|
}
|
|
51
114
|
catch (error) {
|
|
52
|
-
if (error instanceof PasskeyVerificationError || error instanceof AuthorizationDomainError)
|
|
53
|
-
|
|
115
|
+
if (error instanceof PasskeyVerificationError || error instanceof AuthorizationDomainError) {
|
|
116
|
+
deps.reportVerificationFailure?.(error instanceof PasskeyVerificationError
|
|
117
|
+
? `WEBAUTHN_${error.reason}`
|
|
118
|
+
: 'AUTHORIZATION_REJECTED');
|
|
119
|
+
return reply
|
|
120
|
+
.code(400)
|
|
121
|
+
.type('application/problem+json')
|
|
122
|
+
.send(problem('REGISTRATION_VERIFICATION_FAILED', 400, 'Registration could not be completed.'));
|
|
123
|
+
}
|
|
54
124
|
throw error;
|
|
55
125
|
}
|
|
56
126
|
});
|
|
@@ -24,7 +24,10 @@ export function registerAuthRoutes(app, deps) {
|
|
|
24
24
|
ceremonyAttempts: deps.auth.ceremonyAttempts ?? new ExpiringCeremonyAttemptGate(),
|
|
25
25
|
};
|
|
26
26
|
registerRegistrationOptions(app, auth);
|
|
27
|
-
registerRegistrationVerification(app,
|
|
27
|
+
registerRegistrationVerification(app, {
|
|
28
|
+
...deps.auth,
|
|
29
|
+
reportVerificationFailure: (reason) => deps.logger.warn(`Passkey registration verification failed: ${reason}`),
|
|
30
|
+
});
|
|
28
31
|
registerLoginOptions(app, auth);
|
|
29
32
|
registerLoginVerification(app, deps.auth);
|
|
30
33
|
registerAuthStatus(app, deps.auth);
|
|
@@ -19,7 +19,7 @@ export function weeklyRateLimitWindow(windows) {
|
|
|
19
19
|
Number.isFinite(window.usedPercent) &&
|
|
20
20
|
window.usedPercent >= 0 &&
|
|
21
21
|
window.usedPercent <= 100);
|
|
22
|
-
return valid.reduce((weekly, window) =>
|
|
22
|
+
return valid.reduce((weekly, window) => !weekly || window.durationSeconds > weekly.durationSeconds ? window : weekly, null);
|
|
23
23
|
}
|
|
24
24
|
export function weeklyRemainingPercent(windows) {
|
|
25
25
|
const weekly = weeklyRateLimitWindow(windows);
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
export function registerGetPlanMeasurement(app, deps) {
|
|
7
7
|
app.get('/api/sessions/:id/plan-measurement', async (request, reply) => {
|
|
8
8
|
const id = request.params.id;
|
|
9
|
-
const authorization = typeof request.headers.authorization === 'string'
|
|
10
|
-
? request.headers.authorization
|
|
11
|
-
: undefined;
|
|
9
|
+
const authorization = typeof request.headers.authorization === 'string' ? request.headers.authorization : undefined;
|
|
12
10
|
if (!deps.exists(id) || !deps.authorize(id, authorization))
|
|
13
11
|
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
14
12
|
try {
|
|
@@ -4,5 +4,11 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
export function isInteractionKind(value) {
|
|
7
|
-
return [
|
|
7
|
+
return [
|
|
8
|
+
'commandApproval',
|
|
9
|
+
'fileChangeApproval',
|
|
10
|
+
'permissionsApproval',
|
|
11
|
+
'userInput',
|
|
12
|
+
'quiz',
|
|
13
|
+
].includes(value);
|
|
8
14
|
}
|
|
@@ -15,7 +15,9 @@ export function registerListRecentThreads(app, deps) {
|
|
|
15
15
|
recencyAt,
|
|
16
16
|
...(metadata?.model === undefined ? {} : { model: metadata.model }),
|
|
17
17
|
...(metadata?.skillProfile === undefined ? {} : { skillProfile: metadata.skillProfile }),
|
|
18
|
-
...(metadata?.orgPlanFilename === undefined
|
|
18
|
+
...(metadata?.orgPlanFilename === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { orgPlanFilename: metadata.orgPlanFilename }),
|
|
19
21
|
resumeCommand: buildResumeCommand({ profile, threadId: id, workspacePath: cwd }),
|
|
20
22
|
};
|
|
21
23
|
});
|
|
@@ -136,7 +136,9 @@ function copy(snapshot) {
|
|
|
136
136
|
...snapshot,
|
|
137
137
|
...(snapshot.effectiveSkillSelection === undefined
|
|
138
138
|
? {}
|
|
139
|
-
: {
|
|
139
|
+
: {
|
|
140
|
+
effectiveSkillSelection: createEffectiveSkillSelection(snapshot.effectiveSkillSelection),
|
|
141
|
+
}),
|
|
140
142
|
pendingInteractions: snapshot.pendingInteractions.map((item) => ({ ...item })),
|
|
141
143
|
};
|
|
142
144
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
6
|
import { RelaySession } from '../model/relay-session.js';
|
|
7
|
-
import { DEFAULT_SESSION_MODEL
|
|
7
|
+
import { DEFAULT_SESSION_MODEL } from '../application/start-settings.js';
|
|
8
8
|
import { applySkillSelectionSnapshot, } from '../../skills/model/skill-profile.js';
|
|
9
9
|
import { SkillProfileError } from '../../skills/model/errors.js';
|
|
10
10
|
export async function startSession(input, deps) {
|
|
@@ -12,13 +12,22 @@ export function registerDeleteSkillProfile(app, deps) {
|
|
|
12
12
|
try {
|
|
13
13
|
const name = normalizeSkillProfileName(request.params.name);
|
|
14
14
|
if (!(await deps.deleteGlobalProfile(name)))
|
|
15
|
-
return reply
|
|
15
|
+
return reply
|
|
16
|
+
.code(404)
|
|
17
|
+
.type('application/problem+json')
|
|
18
|
+
.send(problem('SKILL_PROFILE_NOT_FOUND', 404, 'The skill profile was not found.'));
|
|
16
19
|
return reply.code(204).send();
|
|
17
20
|
}
|
|
18
21
|
catch (error) {
|
|
19
22
|
if (error instanceof SkillProfileError)
|
|
20
|
-
return reply
|
|
21
|
-
|
|
23
|
+
return reply
|
|
24
|
+
.code(400)
|
|
25
|
+
.type('application/problem+json')
|
|
26
|
+
.send(problem('INVALID_SKILL_PROFILE', 400, 'The skill profile could not be deleted.'));
|
|
27
|
+
return reply
|
|
28
|
+
.code(500)
|
|
29
|
+
.type('application/problem+json')
|
|
30
|
+
.send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'The skill profile could not be deleted.', true));
|
|
22
31
|
}
|
|
23
32
|
});
|
|
24
33
|
}
|
|
@@ -7,13 +7,22 @@ import { z } from 'zod';
|
|
|
7
7
|
import { applySkillSelectionSnapshot } from '../model/skill-profile.js';
|
|
8
8
|
import { SkillProfileError } from '../model/errors.js';
|
|
9
9
|
import { problem } from '../../../platform/http/problem.js';
|
|
10
|
-
const querySchema = z
|
|
10
|
+
const querySchema = z
|
|
11
|
+
.object({
|
|
12
|
+
workspaceId: z.string().min(1),
|
|
13
|
+
profile: z.string().min(1),
|
|
14
|
+
refresh: z.enum(['true']).optional(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
11
17
|
/** Register the workspace-scoped skill discovery REPR slice. */
|
|
12
18
|
export function registerListAvailableSkills(app, deps) {
|
|
13
19
|
app.get('/api/skills', async (request, reply) => {
|
|
14
20
|
const parsed = querySchema.safeParse(request.query);
|
|
15
21
|
if (!parsed.success)
|
|
16
|
-
return reply
|
|
22
|
+
return reply
|
|
23
|
+
.code(400)
|
|
24
|
+
.type('application/problem+json')
|
|
25
|
+
.send(problem('INVALID_SKILL_REQUEST', 400, 'workspaceId and profile are required.'));
|
|
17
26
|
try {
|
|
18
27
|
const [workspace, profile] = await Promise.all([
|
|
19
28
|
deps.workspaces.resolve(parsed.data.workspaceId),
|
|
@@ -52,10 +61,19 @@ export function registerListAvailableSkills(app, deps) {
|
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
catch (error) {
|
|
55
|
-
if (error instanceof Error &&
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
if (error instanceof Error &&
|
|
65
|
+
(error.message === 'WORKSPACE_NOT_FOUND' || error.message === 'PROFILE_NOT_FOUND'))
|
|
66
|
+
return reply
|
|
67
|
+
.code(404)
|
|
68
|
+
.type('application/problem+json')
|
|
69
|
+
.send(problem(error.message, 404, 'The requested catalog entry was not found.'));
|
|
70
|
+
const code = error instanceof SkillProfileError && error.code === 'INVALID_SKILL_PROFILE'
|
|
71
|
+
? 'INVALID_SKILL_PROFILE'
|
|
72
|
+
: 'SKILL_DISCOVERY_FAILED';
|
|
73
|
+
return reply
|
|
74
|
+
.code(502)
|
|
75
|
+
.type('application/problem+json')
|
|
76
|
+
.send(problem(code, 502, 'Skill discovery could not be completed.', true));
|
|
59
77
|
}
|
|
60
78
|
});
|
|
61
79
|
}
|