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.
Files changed (65) hide show
  1. package/README.md +42 -11
  2. package/dist/client/assets/index-2L2KZzwK.css +1 -0
  3. package/dist/client/assets/index-Dnm_kS5p.js +24 -0
  4. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  5. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  6. package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
  7. package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
  8. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  9. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  10. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  11. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  12. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  13. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  14. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
  15. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  16. package/dist/client/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
  17. package/dist/client/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
  18. package/dist/client/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
  19. package/dist/client/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
  20. package/dist/client/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
  21. package/dist/client/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
  22. package/dist/client/icons/gestalt-mobile-180.png +0 -0
  23. package/dist/client/icons/gestalt-mobile-192.png +0 -0
  24. package/dist/client/icons/gestalt-mobile-512.png +0 -0
  25. package/dist/client/icons/gestalt-mobile-maskable-512.png +0 -0
  26. package/dist/client/icons/gestalt-mobile-maskable.svg +10 -0
  27. package/dist/client/icons/gestalt-mobile.svg +10 -0
  28. package/dist/client/index.html +9 -3
  29. package/dist/client/manifest.webmanifest +32 -0
  30. package/dist/client/service-worker.js +9 -0
  31. package/dist/server/server/cli.js +3 -1
  32. package/dist/server/server/composition.js +25 -21
  33. package/dist/server/server/features/auth/application/ports.js +7 -1
  34. package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
  35. package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
  36. package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
  37. package/dist/server/server/features/auth/http/cookies.js +12 -2
  38. package/dist/server/server/features/auth/login/verify/endpoint.js +1 -1
  39. package/dist/server/server/features/auth/register/verify/endpoint.js +89 -19
  40. package/dist/server/server/features/auth/register-routes.js +4 -1
  41. package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
  42. package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
  43. package/dist/server/server/features/sessions/interaction/kind.js +7 -1
  44. package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
  45. package/dist/server/server/features/sessions/model/relay-session.js +3 -1
  46. package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
  47. package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
  48. package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
  49. package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
  50. package/dist/server/server/features/skills/model/skill-profile.js +23 -3
  51. package/dist/server/server/platform/auth/simple-webauthn-adapter.js +55 -9
  52. package/dist/server/server/platform/catalog/profile-command.js +8 -1
  53. package/dist/server/server/platform/codex/normalizer.js +22 -4
  54. package/dist/server/server/platform/codex/session-runtime.js +52 -11
  55. package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
  56. package/dist/server/server/platform/http/problem-reply.js +4 -1
  57. package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
  58. package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
  59. package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
  60. package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
  61. package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
  62. package/dist/server/shared/contracts/quiz.js +30 -9
  63. package/package.json +3 -1
  64. package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
  65. package/dist/client/assets/index-D2sT9ZHY.js +0 -19
@@ -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>
@@ -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
- <title>Codex Relay</title>
13
- <script type="module" crossorigin src="/assets/index-D2sT9ZHY.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-CMs5ZzxJ.css">
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].filter((entry) => entry.enabled).sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)))
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, } from './config.js';
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 ? { auth: {
198
- repository: authorization,
199
- clock: { now: options.authorizationClock ?? (() => new Date()) },
200
- random: {
201
- bytes: (length) => {
202
- const value = authorizationRandom(length);
203
- if (length !== 32 || value.length !== 32)
204
- throw new Error('Authorization randomness must return exactly 32 bytes');
205
- return value;
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
- identifiers: {
209
- sessionId: () => {
210
- const value = authorizationRandom(32);
211
- if (value.length !== 32)
212
- throw new Error('Authorization randomness must return exactly 32 bytes');
213
- return authorizationSessionId(Buffer.from(value).toString('base64url'));
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
- deviceId: () => authorizedDeviceId(randomUUID()),
219
+ webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
220
+ relyingParty,
216
221
  },
217
- webauthn: options.authorizationWebauthn ?? new SimpleWebAuthnAdapter(),
218
- relyingParty,
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 must translate library-specific failures to this type. */
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: '/', httpOnly: true, sameSite: 'strict',
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 || deps.repository.sessionDevice(session, deps.clock.now().toISOString()) === 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({ ticket, url: `${deps.relyingParty.publicOrigin.replace(/\/$/, '')}/#enroll=${ticket}`, expiresAt });
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.object({ status: z.enum(['none', 'pending', 'used', 'expired']) }).strict();
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 = { gestalt_mobile_login: 600, gestalt_mobile_registration: 600, gestalt_mobile_session: 30 * 24 * 60 * 60 };
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 { httpOnly: true, sameSite: 'strict', path: '/', secure: origin.startsWith('https://'), maxAge: seconds[name] };
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('PASSKEY_VERIFICATION_FAILED');
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.object({
9
- id: z.string().min(1), rawId: z.string().min(1), type: z.literal('public-key'),
10
- response: z.object({ clientDataJSON: z.string().min(1), attestationObject: z.string().min(1), authenticatorData: z.string().min(1).optional(), transports: z.array(z.enum(['ble', 'hybrid', 'internal', 'nfc', 'usb', 'cable', 'smart-card'])).optional(), publicKeyAlgorithm: z.number().int().optional(), publicKey: z.string().min(1).optional() }).strict(),
11
- authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(), clientExtensionResults: z.record(z.string(), z.unknown()),
12
- }).strict();
13
- const requestSchema = z.object({ response: registrationResponseSchema, nickname: z.string() }).strict();
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 || typeof token !== 'string')
21
- return reply.code(400).type('application/problem+json').send(problem('INVALID_REGISTRATION_REQUEST', 400, 'The registration request is invalid.'));
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.code(400).type('application/problem+json').send(problem('INVALID_DEVICE_NICKNAME', 400, 'The device nickname is invalid.'));
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
- return reply.code(400).type('application/problem+json').send(problem('REGISTRATION_NOT_AVAILABLE', 400, 'Registration could not be completed.'));
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({ response: parsed.data.response, challenge: ceremony.challenge, expectedOrigin: ceremony.expectedOrigin, rpId: ceremony.rpId });
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('PASSKEY_VERIFICATION_FAILED');
39
- const device = { id: deps.identifiers.deviceId(), credentialId: verified.credentialId, publicKey: verified.publicKey, counter: verified.counter, transports: verified.transports, deviceType: verified.deviceType, backedUp: verified.backedUp, nickname, createdAt: now.toISOString() };
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({ ceremony: passkeyCeremonyId(token), now: now.toISOString(), device, session: { id: session, deviceId: device.id, expiresAt } });
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.code(outcome === 'bootstrapAlreadyClaimed' ? 409 : 400).type('application/problem+json').send(problem(outcome === 'bootstrapAlreadyClaimed' ? 'BOOTSTRAP_ALREADY_CLAIMED' : 'REGISTRATION_NOT_AVAILABLE', outcome === 'bootstrapAlreadyClaimed' ? 409 : 400, 'Registration could not be completed.'));
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
- return reply.code(400).type('application/problem+json').send(problem('REGISTRATION_VERIFICATION_FAILED', 400, 'Registration could not be completed.'));
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, deps.auth);
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) => (!weekly || window.durationSeconds > weekly.durationSeconds ? window : weekly), null);
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 ['commandApproval', 'fileChangeApproval', 'permissionsApproval', 'userInput', 'quiz'].includes(value);
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 ? {} : { orgPlanFilename: metadata.orgPlanFilename }),
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
- : { effectiveSkillSelection: createEffectiveSkillSelection(snapshot.effectiveSkillSelection) }),
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, } from '../application/start-settings.js';
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.code(404).type('application/problem+json').send(problem('SKILL_PROFILE_NOT_FOUND', 404, 'The skill profile was not found.'));
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.code(400).type('application/problem+json').send(problem('INVALID_SKILL_PROFILE', 400, 'The skill profile could not be deleted.'));
21
- return reply.code(500).type('application/problem+json').send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'The skill profile could not be deleted.', true));
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.object({ workspaceId: z.string().min(1), profile: z.string().min(1), refresh: z.enum(['true']).optional() }).strict();
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.code(400).type('application/problem+json').send(problem('INVALID_SKILL_REQUEST', 400, 'workspaceId and profile are required.'));
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 && (error.message === 'WORKSPACE_NOT_FOUND' || error.message === 'PROFILE_NOT_FOUND'))
56
- return reply.code(404).type('application/problem+json').send(problem(error.message, 404, 'The requested catalog entry was not found.'));
57
- const code = error instanceof SkillProfileError && error.code === 'INVALID_SKILL_PROFILE' ? 'INVALID_SKILL_PROFILE' : 'SKILL_DISCOVERY_FAILED';
58
- return reply.code(502).type('application/problem+json').send(problem(code, 502, 'Skill discovery could not be completed.', true));
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
  }