Skip to content

[codex] Structure client cloud cryptography errors#3395

Closed
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors
Closed

[codex] Structure client cloud cryptography errors#3395
juliusmarminge wants to merge 2 commits into
mainfrom
codex/client-cloud-crypto-errors

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary\n\n- replace message-bearing DPoP errors with Schema errors carrying structured operation context\n- preserve exact lower-level causes and add typed key-format/mismatch causes for validation failures\n- remove valueless error constructor wrappers from web/mobile DPoP and mobile relay token storage\n- annotate best-effort token-store logging with the failing operation\n\n## Validation\n\n-  WARN  Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})

RUN /Users/julius/.codex/worktrees/client-cloud-crypto-errors

Test Files 3 passed (3)
Tests 10 passed (10)
Start at 08:11:56
Duration 376ms (transform 78ms, setup 0ms, import 632ms, tests 86ms, environment 0ms)\n-  WARN  Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
�[1m�[94mpass:�[39m�[0m All 1848 files are correctly formatted �[2m(775ms, 16 threads)�[0m
! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1250:8]
1249 | () => ({
1250 | ,-> p({ node: _node, children, ...props }) {
1251 | | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}

;
1252 | -> }, 1253 | li({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1253:9]
1252 | },
1253 | ,-> li({ node, children, ...props }) {
1254 | | const listItemStart = node?.position?.start.offset;
1255 | | const markerOffset =
1256 | | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null;
1257 | | return (
1258 | | <li {...props} data-task-marker-offset={markerOffset ?? undefined}>
1259 | | {renderSkillInlineMarkdownChildren(children, skills)}
1260 | |
1261 | | );
1262 | -> }, 1263 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1263:12]
1262 | },
1263 | ,-> input({ node: _node, type, checked, disabled: _disabled, ...props }) {
1264 | | if (type !== "checkbox" || !onTaskListChange) {
1265 | | return (
1266 | | <input
1267 | | {...props}
1268 | | type={type}
1269 | | checked={checked}
1270 | | disabled={_disabled}
1271 | | readOnly={type === "checkbox"}
1272 | | />
1273 | | );
1274 | | }
1275 | | return (
1276 | | <input
1277 | | {...props}
1278 | | type="checkbox"
1279 | | name="markdown-task"
1280 | | aria-label="Toggle task"
1281 | | checked={checked}
1282 | | onChange={(event) => {
1283 | | const markerOffset = Number(
1284 | | event.currentTarget.closest("li")?.dataset.taskMarkerOffset,
1285 | | );
1286 | | if (!Number.isSafeInteger(markerOffset)) return;
1287 | | onTaskListChange({ markerOffset, checked: event.currentTarget.checked });
1288 | | }}
1289 | | />
1290 | | );
1291 | -> }, 1292 | a({ node, href, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1292:8]
1291 | },
1292 | ,-> a({ node, href, children, ...props }) {
1293 | | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
1294 | | const fileLinkMeta = normalizedHref ? markdownFileLinkMetaByHref.get(normalizedHref) : null;
1295 | | if (!fileLinkMeta) {
1296 | | const faviconHost = resolveExternalLinkHost(href);
1297 | | const isSameDocumentLink = href?.startsWith("#") ?? false;
1298 | | const onClick = props.onClick;
1299 | | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
1300 | | const link = (
1301 | | <a
1302 | | {...props}
1303 | | href={href}
1304 | | target={isSameDocumentLink ? undefined : "_blank"}
1305 | | rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
1306 | | onClick={(event) => {
1307 | | onClick?.(event);
1308 | | if (isSameDocumentLink && href) {
1309 | | handleMarkdownFragmentClick(event, href);
1310 | | }
1311 | | }}
1312 | | onContextMenu={(event) => {
1313 | | if (!canOpenInPreview || !href) return;
1314 | | event.preventDefault();
1315 | | event.stopPropagation();
1316 | | const api = readLocalApi();
1317 | | if (!api) return;
1318 | | void api.contextMenu
1319 | | .show(
1320 | | [
1321 | | { id: "open-in-browser", label: "Open in integrated browser" },
1322 | | { id: "open-external", label: "Open in system browser" },
1323 | | ] as const,
1324 | | { x: event.clientX, y: event.clientY },
1325 | | )
1326 | | .then((clicked) => {
1327 | | if (clicked === "open-in-browser") {
1328 | | void openExternalLinkInPreview(href);
1329 | | return;
1330 | | }
1331 | | if (clicked === "open-external") return api.shell.openExternal(href);
1332 | | })
1333 | | .catch(() => undefined);
1334 | | }}
1335 | | >
1336 | | {faviconHost ? (
1337 | |
1338 | | {children}
1339 | |
1340 | | ) : (
1341 | | children
1342 | | )}
1343 | |
1344 | | );
1345 | | if (!faviconHost || !href) {
1346 | | return link;
1347 | | }
1348 | | return (
1349 | |
1350 | |
1351 | | <TooltipPopup
1352 | | side="top"
1353 | | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere"
1354 | | >
1355 | | {href}
1356 | |
1357 | |
1358 | | );
1359 | | }
1360 | |
1361 | | const parentSuffix = fileLinkParentSuffixByPath.get(fileLinkMeta.filePath);
1362 | | const labelParts = [fileLinkMeta.basename];
1363 | | if (typeof parentSuffix === "string" && parentSuffix.length > 0) {
1364 | | labelParts.push(parentSuffix);
1365 | | }
1366 | | if (fileLinkMeta.line) {
1367 | | labelParts.push(
1368 | | L${fileLinkMeta.line}${fileLinkMeta.column ? :C${fileLinkMeta.column} : ""},
1369 | | );
1370 | | }
1371 | |
1372 | | return (
1373 | | <MarkdownFileLink
1374 | | href={fileLinkMeta.targetPath}
1375 | | targetPath={fileLinkMeta.targetPath}
1376 | | iconPath={fileLinkMeta.filePath}
1377 | | displayPath={fileLinkMeta.displayPath}
1378 | | workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
1379 | | line={fileLinkMeta.line}
1380 | | label={labelParts.join(" · ")}
1381 | | copyMarkdown={[${fileLinkMeta.basename}](${normalizedHref})}
1382 | | theme={resolvedTheme}
1383 | | threadRef={threadRef}
1384 | | onOpen={openInPreferredEditor}
1385 | | onOpenInBrowser={
1386 | | threadRef &&
1387 | | isPreviewSupportedInRuntime() &&
1388 | | isBrowserPreviewFile(fileLinkMeta.filePath)
1389 | | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
1390 | | : undefined
1391 | | }
1392 | | className={props.className}
1393 | | />
1394 | | );
1395 | -> }, 1396 | table({ node: _node, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1396:12]
1395 | },
1396 | ,-> table({ node: _node, ...props }) {
1397 | | return <MarkdownTable {...props} />;
1398 | -> }, 1399 | details({ node: _node, children, open: detailsOpen }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1399:14]
1398 | },
1399 | ,-> details({ node: _node, children, open: detailsOpen }) {
1400 | | return {children};
1401 | -> }, 1402 | pre({ node, children, ...props }) { ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/ChatMarkdown.tsx:1402:10]
1401 | },
1402 | ,-> pre({ node, children, ...props }) {
1403 | | const codeBlock = extractCodeBlock(children);
1404 | | if (!codeBlock) {
1405 | | return <pre {...props}>{children};
1406 | | }
1407 | |
1408 | | const language = extractFenceLanguage(codeBlock.className);
1409 | | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node));
1410 | | return (
1411 | | <MarkdownCodeBlock
1412 | | code={codeBlock.code}
1413 | | language={language}
1414 | | fenceTitle={fenceTitle}
1415 | | theme={resolvedTheme}
1416 | | >
1417 | | <CodeHighlightErrorBoundary fallback={<pre {...props}>{children}}>
1418 | | <Suspense fallback={<pre {...props}>{children}}>
1419 | | <SuspenseShikiCodeBlock
1420 | | className={codeBlock.className}
1421 | | code={codeBlock.code}
1422 | | themeName={diffThemeName}
1423 | | isStreaming={isStreaming}
1424 | | />
1425 | |
1426 | |
1427 | |
1428 | | );
1429 | -> }, 1430 | }), ----
help: Move this component definition out of the parent component ChatMarkdown and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/review/ReviewSheet.tsx:249:24]
248 | },
249 | ,-> headerTitle: () => (
250 | | <View style={{ alignItems: "center" }}>
251 | | <NativeText
252 | | numberOfLines={1}
253 | | style={{
254 | | fontFamily: "DMSans_700Bold",
255 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
256 | | fontWeight: "900",
257 | | color: headerForeground,
258 | | letterSpacing: -0.4,
259 | | }}
260 | | >
261 | | Files Changed
262 | |
263 | | <View
264 | | style={{
265 | | flexDirection: "row",
266 | | alignItems: "center",
267 | | justifyContent: "center",
268 | | gap: 6,
269 | | flexWrap: "wrap",
270 | | }}
271 | | >
272 | | {headerDiffSummary.additions && headerDiffSummary.deletions ? (
273 | | <>
274 | | <NativeText
275 | | style={{
276 | | fontFamily: "DMSans_700Bold",
277 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
278 | | fontWeight: "700",
279 | | color: "#16a34a",
280 | | }}
281 | | >
282 | | {headerDiffSummary.additions}
283 | |
284 | | <NativeText
285 | | style={{
286 | | fontFamily: "DMSans_700Bold",
287 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
288 | | fontWeight: "700",
289 | | color: "#e11d48",
290 | | }}
291 | | >
292 | | {headerDiffSummary.deletions}
293 | |
294 | | {pendingReviewCommentCount > 0 ? (
295 | | <NativeText
296 | | style={{
297 | | fontFamily: "DMSans_700Bold",
298 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
299 | | fontWeight: "700",
300 | | color: "#b45309",
301 | | }}
302 | | >
303 | | {pendingReviewCommentCount} pending
304 | |
305 | | ) : null}
306 | | </>
307 | | ) : (
308 | | <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
309 | | <NativeText
310 | | numberOfLines={1}
311 | | style={{
312 | | fontFamily: "DMSans_700Bold",
313 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
314 | | fontWeight: "700",
315 | | color: headerMuted,
316 | | }}
317 | | >
318 | | {selectedSection?.title ?? "Review changes"}
319 | |
320 | | {pendingReviewCommentCount > 0 ? (
321 | | <NativeText
322 | | style={{
323 | | fontFamily: "DMSans_700Bold",
324 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
325 | | fontWeight: "700",
326 | | color: "#b45309",
327 | | }}
328 | | >
329 | | {pendingReviewCommentCount} pending
330 | |
331 | | ) : null}
332 | |
333 | | )}
334 | |
335 | |
336 | -> ), 337 | }} ----
help: Move this component definition out of the parent component ReviewSheet and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/threads/ThreadRouteScreen.tsx:383:24]
382 | headerBackTitle: "",
383 | ,-> headerTitle: () => (
384 | | <Pressable
385 | | style={{ alignItems: "center", maxWidth: 200 }}
386 | | onLongPress={() => {
387 | | // TODO: trigger rename modal
388 | | }}
389 | | >
390 | | <RNText
391 | | numberOfLines={1}
392 | | style={{
393 | | fontFamily: "DMSans_700Bold",
394 | | fontSize: MOBILE_TYPOGRAPHY.headline.fontSize,
395 | | fontWeight: "900",
396 | | color: foregroundColor,
397 | | letterSpacing: -0.4,
398 | | }}
399 | | >
400 | | {selectedThread.title}
401 | |
402 | | <RNText
403 | | numberOfLines={1}
404 | | style={{
405 | | fontFamily: "DMSans_700Bold",
406 | | fontSize: MOBILE_TYPOGRAPHY.label.fontSize,
407 | | fontWeight: "700",
408 | | color: secondaryFg,
409 | | letterSpacing: 0.3,
410 | | }}
411 | | >
412 | | {headerSubtitle}
413 | |
414 | |
415 | -> ), 416 | }} ----
help: Move this component definition out of the parent component ThreadRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:612:15]
611 | valuePrefix: "project",
612 | ,-> icon: (project) => (
613 | | <ProjectFavicon
614 | | environmentId={project.environmentId}
615 | | cwd={project.workspaceRoot}
616 | | className={ITEM_ICON_CLASS}
617 | | />
618 | -> ), 619 | runProject: openProjectFromSearch, ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/web/src/components/CommandPalette.tsx:630:15]
629 | shortcutCommand: "chat.new",
630 | ,-> icon: (project) => (
631 | | <ProjectFavicon
632 | | environmentId={project.environmentId}
633 | | cwd={project.workspaceRoot}
634 | | className={ITEM_ICON_CLASS}
635 | | />
636 | -> ), 637 | runProject: async (project) => { ----
help: Move this component definition out of the parent component OpenCommandPaletteDialog and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx:911:24]
910 | title: "",
911 | ,-> headerTitle: () => (
912 | | <View
913 | | style={{
914 | | alignItems: "center",
915 | | gap: 1,
916 | | maxWidth: 240,
917 | | }}
918 | | >
919 | | <RNText
920 | | numberOfLines={1}
921 | | style={{
922 | | color: terminalTheme.foreground,
923 | | fontFamily: "DMSans_700Bold",
924 | | fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize,
925 | | lineHeight: 16,
926 | | }}
927 | | >
928 | | {headerTitle.topLine}
929 | |
930 | | <RNText
931 | | ellipsizeMode="middle"
932 | | numberOfLines={1}
933 | | style={{
934 | | color: terminalTheme.mutedForeground,
935 | | fontFamily: "Menlo",
936 | | fontSize: MOBILE_TYPOGRAPHY.caption.fontSize,
937 | | lineHeight: 14,
938 | | }}
939 | | >
940 | | {headerTitle.bottomLine}
941 | |
942 | |
943 | -> ), 944 | }} ----
help: Move this component definition out of the parent component ThreadTerminalRouteScreen and pass data as props. If you want to allow component creation in props, set allowAsProps option to true.

! react(no-unstable-nested-components): Do not define components during render.
,-[apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx:492:24]
491 | headerShadowVisible: false,
492 | headerTitle: () => ,
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
493 | headerSearchBarOptions: {
---- help: Move this component definition out of the parent component ThreadFilesTreeScreenand pass data as props. If you want to allow component creation in props, setallowAsProps` option to true.

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:293:15]
292 | const stored = yield* Effect.fromResult(
293 | Schema.decodeUnknownResult(StoredShellSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
294 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:313:13]
312 | const legacyStored = yield* Effect.fromResult(
313 | Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
314 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:328:13]
327 | const encoded = yield* Effect.fromResult(
328 | Schema.encodeUnknownResult(StoredShellSnapshot)(stored),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
329 | ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:355:13]
354 | const stored = yield* Effect.fromResult(
355 | Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
356 | ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/storage.ts:365:13]
364 | const encoded = yield* Effect.fromResult(
365 | Schema.encodeUnknownResult(StoredThreadSnapshot)({
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
366 | schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION,
`----

! t3code(no-inline-schema-compile): Hoist Schema.decodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:31:5]
30 | return yield* Effect.fromResult(
31 | Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
32 | ).pipe(Effect.mapError((cause) => catalogError("decode", cause)));
`----

! t3code(no-inline-schema-compile): Hoist Schema.encodeUnknownResult(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.
,-[apps/mobile/src/connection/catalog-store.ts:39:5]
38 | const encoded = yield* Effect.fromResult(
39 | Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog),
: ^^^^^^^^^^^^^^^^^^^^^^^^^^
40 | ).pipe(Effect.mapError((cause) => catalogError("encode", cause)));
`----

Found 0 errors and 20 warnings in 1748 files (978ms, 16 threads)\n-  WARN  Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
~/packages/contracts$ tsgo --noEmit ⊘ cache disabled
~/apps/marketing$ astro check ⊘ cache disabled

~/oxlint-plugin-t3code$ tsgo --noEmit ⊘ cache disabled

~/packages/effect-codex-app-server$ tsgo --noEmit ⊘ cache disabled
08:12:00 [WARN] [vite] warning: optimizeDeps.esbuildOptions option was specified by "astro:dev-toolbar" plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.
08:12:00 [WARN] [vite] You or a plugin you are using have set optimizeDeps.esbuildOptions but this option is now deprecated. Vite now uses Rolldown to optimize the dependencies. Please use optimizeDeps.rolldownOptions instead.
08:12:00 [types] Generated 39ms
08:12:00 [check] Getting diagnostics for Astro files in /Users/julius/.codex/worktrees/client-cloud-crypto-errors/apps/marketing...
Result (10 files):

  • 0 errors
  • 0 warnings
  • 0 hints

~/packages/effect-acp$ tsgo --noEmit ⊘ cache disabled

~/packages/shared$ tsgo --noEmit ⊘ cache disabled

~/packages/client-runtime$ tsgo --noEmit ⊘ cache disabled

~/scripts$ tsgo --noEmit ⊘ cache disabled

~/packages/ssh$ tsgo --noEmit ⊘ cache disabled

~/packages/tailscale$ tsgo --noEmit ⊘ cache disabled

~/apps/web$ tsgo --noEmit ⊘ cache disabled

~/apps/mobile$ tsc --noEmit ⊘ cache disabled

~/infra/relay$ tsgo --noEmit ⊘ cache disabled

~/apps/desktop$ tsgo --noEmit ⊘ cache disabled

~/apps/server$ tsgo --noEmit ⊘ cache disabled


vp run: 0/15 cache hit (0%). (Run vp run --last-details for full details)\n-  WARN  Unsupported engine: wanted: {"node":"^24.13.1"} (current: {"node":"v25.8.2","pnpm":"10.24.0"})
$ node scripts/mobile-native-static-check.ts ⊘ cache disabled
Found 6 Swift and 2 Kotlin native source files.
$ swiftlint lint --config .swiftlint.yml --strict
$ ktlint modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
$ detekt --config detekt.yml --input modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt,modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt --build-upon-default-config
Skipping generated native project folders: android/, ios/.


Note

Medium Risk
Touches auth-related DPoP and secure token cache paths; error shape and message text change for any caller matching on strings, though signing/storage behavior is unchanged.

Overview
Replaces free-form message strings on mobile/web DPoP and mobile managed relay token failures with Effect Schema tagged errors that carry a discriminated operation literal and an optional cause (Defect), plus derived message getters.

On mobile DPoP, validation failures now use dedicated types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) wrapped as validate-key / derive-public-key where appropriate; invalid proof URLs fail as normalize-proof-url without a cause. Web DPoP follows the same pattern (including isBrowserDpopError for decode chaining).

Managed relay token store errors are structured the same way; load/save/clear logging annotates the full error object and operation instead of a generic operation string. Tests assert operation (and causes where relevant) rather than fixed message text.

Reviewed by Cursor Bugbot for commit df4f4d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Structure cloud cryptography errors with operation-tagged Schema.TaggedErrorClass types

  • Replaces Data.TaggedError with Schema.TaggedErrorClass for CloudDpopError, BrowserDpopError, and ManagedRelayTokenStoreError, adding an operation literal discriminant and optional typed cause to each.
  • Each error class now exposes a message getter that formats the operation name, and new specific error types (DpopPublicKeyFormatError, DpopStoredPublicKeyMismatchError) are introduced for granular failure cases.
  • Exports isBrowserDpopError and isManagedRelayTokenStoreError type guards via Schema.is.
  • Log annotations in managedRelayTokenStore now include the structured error object and its operation field instead of a freeform string.
  • Behavioral Change: callers that previously matched on error message strings must now inspect the structured operation field.

Macroscope summarized df4f4d7.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
@macroscopeapp

macroscopeapp Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

This PR refactors error classes to use structured typed operations instead of free-form string messages. The changes are purely structural with no runtime behavior modifications - errors are thrown at the same points with equivalent semantics.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge

Copy link
Copy Markdown
Member Author

Superseded by the smaller, more structural #3277, #3307, and #3309. Those PRs retain storage/request identifiers and distinct DPoP failure types instead of combining all client cryptography changes into one branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant