@capxul/sdk 4.20.0-beta.35 → 4.20.0-beta.37

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.
@@ -1,10 +1,10 @@
1
- import { c as OrgPortTag, l as deserializeCapxulError, n as makeOrgMethods, r as makeBankingMethods, s as ConvexOrganizationAdapter, t as ConvexOrganizationSetupAdapter } from "./organizations-BmX52Uev.mjs";
2
- import { $r as CAPXUL_ERROR_CODES, $t as version, A as registerProductTelemetryInvocation, An as details, B as makePaymentsMethods, Bi as isCapxulError, Br as askIdentity, Bt as failureEvidenceProps, C as AccountReadPortTag, Ci as AuthClientError, Cn as CLI_INVOCATION_KINDS, D as convexCallErrorFromCapxul, Dn as isCapxulOperation, Dr as ChainIdSchema, Dt as normalizeSafeSaltEmail, E as ConvexCallPortTag, Ei as globalLocalStorage, En as CAPXUL_OPERATIONS, Fi as toEmail, Gt as observationContextProps, H as makeDestinationsMethods, Hi as isFailureMode, Hr as runIdentityFacade, Ht as postHogFailureObservation, Jn as ORGANIZATION_SETUP_REASONS, K as makeTargetsMethods, Ki as AuthCachePortTag, Kr as runCapxulEffect, Kt as withCapturedAttemptContext, L as makeAccountMethods, Lt as asCapxulError, M as makeApprovalsMethods, Mi as toEpochMs, Nr as isEvmAddress, On as normalizeCapxulOperation, Pi as toAuthUserId, Pr as toAddress, Pt as callOperation, Qr as formatTraceparent, Ri as Errors, Rt as toPublicError, Sn as CLI_COMMAND_NAMES, Sr as EpochMsSchema, T as TelemetryPortTag, Ti as detectAuthCacheAdapter, Tn as stampTelemetryEnvelope, Ui as isRecord, Ur as runPortEffect, Ut as observeFailedResult, Vi as FAILURE_MODES, Vr as publicIdentityFailure, W as makeCheckoutMethods, Wi as readField, Wn as setups, Wr as sessionFromActor, Wt as observeSdkClient, Yn as ORGANIZATION_SETUP_STATUSES, Zn as decodeChainCause, Zr as toCapxulResult, _i as redactUrlSecrets, ai as SystemClockLayer, an as isSignerNotReadySignal, bn as redactTelemetryProps, c as makePaymentDocumentsMethods, cn as signerFailure, cr as AppIdSchema, d as SmartAccountPortTag, di as observationRequestHeaders, dn as makeClientRequestMethods, fi as attachInvocationObservation, fn as toCountryCode, g as ConvexAccountLayer, gi as redactSecrets, gn as toHandle, h as makeAccountsMethods, hi as sanitizeObservationContext, in as isEcdsaSignatureHex, jn as onboardings, jt as BASE_SEPOLIA_CHAIN_ID, k as bindProductTelemetryInvocation, kn as OPERATIONS, l as makeSmartAccountMethods, li as BetterAuthNodeLayer, lr as images, m as PonderIndexerAdapter, mi as readInvocationObservation, mn as toTesterKind, mr as isBytes32, n as bootIdentityFlow, ni as carryInvocationParentSpan, nn as causeChain, on as openfortSignerNotReadyCode, p as makeHoldingsMethods, pi as copyInvocationObservation, pn as toKycTier, q as makeVerificationMethods, r as makeMeMethods, s as makeActivityMethods, si as ClockPortTag, sn as recoverRawDigestSigner, sr as diagnostics, t as executeIdentityProductObservation, ti as ActorFailure, tn as UNOBSERVABLE_SIGNER_STATUS, u as ConvexSmartAccountLayer, ui as BetterAuthBrowserLayer, un as makeCurrentUserMethods, vi as isCredentialField, vn as validateHandle, wi as AuthClientPortTag, xi as toPublishableKey, xn as CLI_CAPABILITIES, xr as DurationMsSchema, yi as toAllowedOrigin, zi as CapxulError, zr as makeAuthMethods, zt as failureDetail } from "./access-DpPt-SvA.mjs";
1
+ import { c as OrgPortTag, n as makeOrgMethods, r as makeBankingMethods, s as ConvexOrganizationAdapter, t as ConvexOrganizationSetupAdapter } from "./organizations-DPpyjjx2.mjs";
2
+ import { $i as PROVIDERS, $n as copyInvocationObservation, A as bindProductTelemetryInvocation, Ai as redactUrlSecrets, An as runPortEffect, Bn as ActorFailure, Bt as toPublicError, C as AccountReadPortTag, Ci as toAddress, D as ConvexCallPortTag, Dn as noSessionError, E as ConvexCallError, En as askIdentity, G as makeCheckoutMethods, Gi as toEpochMs, Gt as observeFailedResult, Ht as failureEvidenceProps, Ii as AuthClientError, Ir as decodeChainCause, It as callOperation, J as makeVerificationMethods, Ji as toEmail, Jr as diagnostics, Jt as withCapturedAttemptContext, Kn as ClockPortTag, Kt as observeSdkClient, Li as AuthClientPortTag, Lr as CAPXUL_ERROR_CODES, Mi as toAllowedOrigin, N as makeApprovalsMethods, Nn as runCapxulEffect, Nr as ORGANIZATION_SETUP_REASONS, Nt as BASE_SEPOLIA_CHAIN_ID, O as convexCallErrorFromCapxul, Oi as sanitizeObservationContext, On as publicIdentityFailure, Pi as toPublishableKey, Pr as ORGANIZATION_SETUP_STATUSES, Qi as providerStatusFailure, Qn as attachInvocationObservation, R as makeAccountMethods, Ri as detectAuthCacheAdapter, Rn as toCapxulResult, Si as isEvmAddress, Sn as toHandle, T as TelemetryPortTag, Tn as makeAuthMethods, U as makeDestinationsMethods, V as makePaymentsMethods, Vn as carryInvocationParentSpan, Vt as failureDetail$1, Wn as SystemClockLayer, Wt as postHogFailureObservation, Xn as BetterAuthBrowserLayer, Xr as images, Y as withheld, Yn as BetterAuthNodeLayer, Yr as AppIdSchema, Zi as Errors, Zn as observationRequestHeaders, _n as makeClientRequestMethods, aa as isFailureMode, ar as CLI_COMMAND_NAMES, bn as toTesterKind, c as makePaymentDocumentsMethods, cn as httpStatusOf, d as SmartAccountPortTag, di as EpochMsSchema, dn as isSignerNotReadySignal, dr as CAPXUL_OPERATIONS, ea as PROVIDER_PROBLEMS, fn as openfortSignerNotReadyCode, fr as isCapxulOperation, g as ConvexAccountLayer, gi as ChainIdSchema, gn as makeCurrentUserMethods, gr as onboardings, h as makeAccountsMethods, hn as signerPathFailure, hr as details, ia as FAILURE_MODES, in as maskedMessage, ir as CLI_CAPABILITIES, j as registerProductTelemetryInvocation, ji as isCredentialField, jn as sessionFromActor, ki as redactSecrets, kn as runIdentityFacade, kr as setups, kt as normalizeSafeSaltEmail, l as makeSmartAccountMethods, la as AuthCachePortTag, m as PonderIndexerAdapter, mn as signerFailure, mr as OPERATIONS, n as bootIdentityFlow, na as CapxulError, nn as version, oa as isRecord, on as UNOBSERVABLE_SIGNER_STATUS, or as CLI_INVOCATION_KINDS, p as makeHoldingsMethods, pn as recoverRawDigestSigner, pr as normalizeCapxulOperation, q as makeTargetsMethods, qi as toAuthUserId, qt as observationContextProps, r as makeMeMethods, ra as isCapxulError, rn as deserializeCapxulError, rr as redactTelemetryProps, s as makeActivityMethods, sa as readField, sn as causeChain, sr as FAILURE_CLASSES, t as executeIdentityProductObservation, ta as httpStatusMode, ti as isBytes32, tr as readInvocationObservation, u as ConvexSmartAccountLayer, ui as DurationMsSchema, un as isEcdsaSignatureHex, ur as stampTelemetryEnvelope, vn as toCountryCode, wn as validateHandle, yn as toKycTier, zi as globalLocalStorage, zn as formatTraceparent, zt as asCapxulError } from "./access-aMjL10vw.mjs";
3
3
  import { Cause, Clock, Context, Data, Deferred, Duration, Effect, Exit, FiberSet, Layer, Logger, Metric, Option, Predicate, References, Result, Schedule, Schema, SchemaIssue, SchemaParser, Scope, Stream, Tracer } from "effect";
4
4
  import { getFunctionName } from "convex/server";
5
5
  import { keccak256, stringToHex } from "viem";
6
6
  import { privateKeyToAccount } from "viem/accounts";
7
- import { AccountTypeEnum, ChainTypeEnum, EmbeddedState, Openfort, RecoveryMethod, ThirdPartyOAuthProvider } from "@openfort/openfort-js";
7
+ import { AccountTypeEnum, AuthenticationError, AuthorizationError, ChainTypeEnum, EmbeddedState, IframeHandshakeTimeoutError, IframeRpcTimeoutError, OPENFORT_AUTH_ERROR_CODES, Openfort, OpenfortError, RecoveryMethod, SessionError, ThirdPartyOAuthProvider } from "@openfort/openfort-js";
8
8
  import { ConvexClient, ConvexHttpClient } from "convex/browser";
9
9
  import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
10
10
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/observability";
@@ -160,29 +160,117 @@ function embeddedSigner(input) {
160
160
  }
161
161
  //#endregion
162
162
  //#region src/signing/openfort-failures.ts
163
- function httpStatusOf(link) {
164
- for (const status of [
165
- readField(link.response, "status"),
166
- link.statusCode,
167
- link.status,
168
- readField(link.details, "httpStatus")
169
- ]) if (typeof status === "number") return status;
170
- }
163
+ /**
164
+ * The codes of Openfort's two auth-refusal classes, `AuthenticationError` and
165
+ * `AuthorizationError` (`extractApiError` in `@openfort/openfort-js` 2.6.1).
166
+ * `AuthorizationError` always sets `error` to the lower-case word.
167
+ */
168
+ const OPENFORT_AUTH_REFUSAL_CODES = /* @__PURE__ */ new Set([
169
+ OPENFORT_AUTH_ERROR_CODES.INVALID_CREDENTIALS,
170
+ OPENFORT_AUTH_ERROR_CODES.INVALID_EMAIL,
171
+ OPENFORT_AUTH_ERROR_CODES.INVALID_PASSWORD,
172
+ OPENFORT_AUTH_ERROR_CODES.INVALID_TOKEN,
173
+ OPENFORT_AUTH_ERROR_CODES.PROVIDER_DISABLED,
174
+ OPENFORT_AUTH_ERROR_CODES.EMAIL_NOT_VERIFIED,
175
+ OPENFORT_AUTH_ERROR_CODES.USER_NOT_AUTHORIZED,
176
+ "user_not_authorized"
177
+ ]);
178
+ /**
179
+ * The `SessionError` codes that say Openfort refused the credential. Only an
180
+ * expired session does. The other codes of that class name an Openfort fault
181
+ * (`FAILED_TO_CREATE_SESSION`, `FAILED_TO_GET_SESSION`), local Openfort state
182
+ * (`NOT_LOGGED_IN`, `ALREADY_LOGGED_IN`) or a refresh that the third-party
183
+ * flow does not do (`REFRESH_TOKEN_ERROR`).
184
+ */
185
+ const OPENFORT_SESSION_REFUSAL_CODES = /* @__PURE__ */ new Set([OPENFORT_AUTH_ERROR_CODES.SESSION_EXPIRED]);
186
+ /**
187
+ * True when a link that has no HTTP status says Openfort refused the
188
+ * credential: Openfort's auth-refusal class or code, or a `SessionError` for an
189
+ * expired session. Openfort keeps the status on every error that it decodes
190
+ * from an answer, so an answered refusal is named by its status first; this
191
+ * reads the errors that the Openfort client makes itself, such as the
192
+ * `AuthenticationError` it throws when the token callback gives it no access
193
+ * token.
194
+ */
171
195
  function isUnauthorizedCause(cause) {
172
- for (const link of causeChain(cause)) if (httpStatusOf(link) === 401) return true;
196
+ for (const link of causeChain(cause)) {
197
+ if (link instanceof AuthenticationError || link instanceof AuthorizationError) return true;
198
+ if (link instanceof SessionError && OPENFORT_SESSION_REFUSAL_CODES.has(link.error)) return true;
199
+ if (OPENFORT_AUTH_REFUSAL_CODES.has(link.error) || OPENFORT_AUTH_REFUSAL_CODES.has(link.code)) return true;
200
+ }
173
201
  return false;
174
202
  }
203
+ /**
204
+ * The steps that send Openfort API requests: the wallet `configure`, which
205
+ * starts with the token check (`POST /iam/v2/user/third_party`), and the
206
+ * wallet read, in the readiness cycle (`get`) and behind `getAddress`.
207
+ */
208
+ const OPENFORT_REQUEST_STEPS = /* @__PURE__ */ new Set([
209
+ "configure",
210
+ "get",
211
+ "getAddress"
212
+ ]);
213
+ /**
214
+ * The mode that Openfort's typed error fields name. `@openfort/openfort-js`
215
+ * 2.6.1 sets `statusCode` on every error that it decodes from an answer, and
216
+ * `requestId` on every error from an API request. The status names the mode by
217
+ * `httpStatusMode`, and `signerPathFailure` keeps the status beside it. A
218
+ * `requestId` with no status means that no answer arrived. That is a network
219
+ * failure or Openfort's 30 s request timeout, and only the message text tells
220
+ * them apart, so both are `upstream-down`. Openfort's iframe timeout classes
221
+ * are `timeout`. The result is `undefined` when no typed field names a mode.
222
+ */
223
+ function openfortRequestMode(cause) {
224
+ for (const link of causeChain(cause)) {
225
+ const httpStatus = httpStatusOf(link);
226
+ if (httpStatus !== void 0) return httpStatusMode(httpStatus);
227
+ if (link instanceof IframeRpcTimeoutError || link instanceof IframeHandshakeTimeoutError) return "timeout";
228
+ if (link instanceof OpenfortError && typeof link.requestId === "string") return "upstream-down";
229
+ }
230
+ }
175
231
  function openfortProviderError(operation, cause) {
176
232
  if (cause instanceof CapxulError) return cause;
177
- const failure_mode = operation === "configure" && isUnauthorizedCause(cause) ? "unauthorized" : "unknown";
178
- return Errors.providerError("openfort", operation, cause, { failure_mode });
233
+ const requestMode = OPENFORT_REQUEST_STEPS.has(operation) ? openfortRequestMode(cause) : void 0;
234
+ if (requestMode !== void 0 && requestMode !== "unknown") return failOpenfort(operation, requestMode, cause);
235
+ return failOpenfort(operation, operation === "configure" && isUnauthorizedCause(cause) ? "unauthorized" : "unknown", cause);
179
236
  }
180
- function openfortFailureMode(error) {
181
- return error.mode ?? "unknown";
237
+ /**
238
+ * What the trace of a failed Openfort step says about its error: the failure
239
+ * mode, or the code and the field when the error has no mode.
240
+ * `PROVIDER_RESPONSE_INVALID` has no mode, and its code and field already say
241
+ * what happened, so the trace does not report it as `unknown`.
242
+ */
243
+ function openfortFailureDetail(error) {
244
+ if (error.mode !== void 0) return { failure_mode: error.mode };
245
+ const field = error.details?.field;
246
+ return {
247
+ error_code: error.code,
248
+ ...typeof field === "string" ? { field } : {}
249
+ };
182
250
  }
183
- /** Build a named PROVIDER_ERROR. The assembled Core SDK boundary reports it. */
184
- function failOpenfort(operation, failure_mode, cause) {
185
- return Errors.providerError("openfort", operation, cause, { failure_mode });
251
+ /**
252
+ * Build a named PROVIDER_ERROR. The assembled Core SDK boundary reports it.
253
+ * An HTTP status anywhere in the cause chain rides beside the mode.
254
+ */
255
+ function failOpenfort(operation, mode, cause) {
256
+ return signerPathFailure({
257
+ provider: "openfort",
258
+ operation,
259
+ mode,
260
+ cause
261
+ });
262
+ }
263
+ /**
264
+ * Build a named PROVIDER_RESPONSE_INVALID: the answer arrived, and Capxul
265
+ * cannot use it. `field` and `reason` are Capxul's own words. Nothing from
266
+ * the answer enters the error, because the answer can hold a credential: a
267
+ * cause that quotes the answer arrives `withheld`.
268
+ */
269
+ function openfortAnswerInvalid(input) {
270
+ return Errors.providerResponseInvalid({
271
+ provider: "openfort",
272
+ ...input
273
+ });
186
274
  }
187
275
  /**
188
276
  * True when the browser cannot perform Web Crypto — sandboxed iframes, headless
@@ -197,14 +285,128 @@ function isInsecureBrowserContext() {
197
285
  //#region src/signing/openfort-credentials.ts
198
286
  const sinceCycleStart = (clock) => clock.cycleStartedAt === null ? void 0 : Date.now() - clock.cycleStartedAt;
199
287
  /**
200
- * The auth server's token reply: `token` from the OIDC endpoint, `session.token`
201
- * from better-auth. A reply of any other shape fails the token read.
288
+ * The auth server's replies. The OIDC endpoint answers `{ token }`.
289
+ * better-auth's `get-session` answers `{ session: { token } }`, and `null` or
290
+ * `{ session: null }` when nobody is signed in. The encryption-session
291
+ * endpoint answers `{ sessionId }`. A reply of any other shape, `{}` included,
292
+ * is an answer Capxul cannot use.
202
293
  */
203
- const AccessTokenBody = Schema.Struct({
204
- token: Schema.optional(Schema.NullOr(Schema.String)),
205
- session: Schema.optional(Schema.NullOr(Schema.Struct({ token: Schema.optional(Schema.NullOr(Schema.String)) })))
294
+ const OidcTokenBody = Schema.NullOr(Schema.Struct({ token: Schema.NullOr(Schema.String) }));
295
+ const BetterAuthSessionBody = Schema.NullOr(Schema.Struct({ session: Schema.NullOr(Schema.Struct({ token: Schema.String })) }));
296
+ const EncryptionSessionBody = Schema.Struct({ sessionId: Schema.String });
297
+ const MISSING$1 = "missing";
298
+ const WRONG_TYPE$1 = "not the expected type";
299
+ /**
300
+ * The Schema issues of a reply, each with a fixed reason. The Schema's own
301
+ * message can quote the reply, and the reply can hold a credential.
302
+ */
303
+ const replyIssues = SchemaIssue.makeFormatterStandardSchemaV1({
304
+ leafHook: (issue) => issue._tag === "MissingKey" ? MISSING$1 : WRONG_TYPE$1,
305
+ checkHook: () => WRONG_TYPE$1
206
306
  });
207
307
  /**
308
+ * A reply that fails its Schema, named by the path of the first field that
309
+ * fails, or `body` when the whole reply has the wrong shape.
310
+ */
311
+ function unusableReply(operation, issue, httpStatus) {
312
+ const first = replyIssues(issue).issues[0];
313
+ const path = first?.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)) ?? [];
314
+ const missing = first?.message === MISSING$1;
315
+ return openfortAnswerInvalid({
316
+ operation,
317
+ field: path.length === 0 ? "body" : path.join("."),
318
+ problem: missing ? "missing" : "invalid",
319
+ reason: missing ? MISSING$1 : WRONG_TYPE$1,
320
+ httpStatus
321
+ });
322
+ }
323
+ /**
324
+ * An answer with no token: the host or the auth server has no signed-in
325
+ * session to give Openfort. That refuses the credential, so it is
326
+ * `unauthorized`, the mode Openfort's own refusal of a missing token takes
327
+ * during `configure`.
328
+ */
329
+ function accessTokenRead(token) {
330
+ return token === void 0 || token === null || token.length === 0 ? {
331
+ token: null,
332
+ failure: failOpenfort("token", "unauthorized")
333
+ } : { token };
334
+ }
335
+ /**
336
+ * How long one auth-server request may wait for its answer. It is below the
337
+ * SDK's 30 s identity invoke deadline (`DEFAULT_INVOKE_TIMEOUT_MS` in
338
+ * `platform/production.ts`), so a request that never answers fails here as
339
+ * its own `timeout`, with its trace, before that deadline ends the attempt.
340
+ */
341
+ const AUTH_SERVER_REQUEST_TIMEOUT_MS = 15e3;
342
+ /**
343
+ * No answer arrived. The request's deadline is `timeout`; a network failure,
344
+ * or the page tearing the request down on a reload, is `upstream-down`. The
345
+ * deadline's own state decides, because a body read it stops can reject with
346
+ * `AbortError` rather than the deadline's `TimeoutError`.
347
+ */
348
+ function noAnswer(operation, deadline, cause) {
349
+ return failOpenfort(operation, deadline.aborted || readField(cause, "name") === "TimeoutError" ? "timeout" : "upstream-down", cause);
350
+ }
351
+ /**
352
+ * Send one auth-server request with its deadline as the signal. The same
353
+ * deadline stops a body that does not arrive (`readJson`). `fetch` rejects
354
+ * only when no answer arrived. No caller passes its own signal: a caller abort
355
+ * needs its own mode before it can join the deadline.
356
+ */
357
+ async function send(operation, url, init, deadline) {
358
+ try {
359
+ return await fetch(url, {
360
+ ...init,
361
+ signal: deadline
362
+ });
363
+ } catch (cause) {
364
+ throw noAnswer(operation, deadline, cause);
365
+ }
366
+ }
367
+ /** A failure named by its HTTP status. The body is never read: it can carry provider detail. */
368
+ function statusFailure(operation, status) {
369
+ return providerStatusFailure({
370
+ provider: "openfort",
371
+ operation,
372
+ httpStatus: status
373
+ });
374
+ }
375
+ /**
376
+ * Read a 2xx body. A body that is not JSON arrived, and Capxul cannot use it:
377
+ * `PROVIDER_RESPONSE_INVALID` on `body`. The parser's own error quotes the
378
+ * body, so only its name is kept. A body that fails in transit, or that
379
+ * the request's deadline stops, is no answer, the same as a `fetch` that
380
+ * rejects.
381
+ */
382
+ async function readJson(operation, response, deadline) {
383
+ try {
384
+ return await response.json();
385
+ } catch (cause) {
386
+ if (cause instanceof SyntaxError) throw openfortAnswerInvalid({
387
+ operation,
388
+ field: "body",
389
+ problem: "invalid",
390
+ reason: "not JSON",
391
+ httpStatus: response.status,
392
+ cause: withheld(cause, "the parser's error quotes the body")
393
+ });
394
+ throw noAnswer(operation, deadline, cause);
395
+ }
396
+ }
397
+ /**
398
+ * The trace detail of a failed credential step: its mode (its code and field
399
+ * when it has no mode), and the HTTP status when one answered.
400
+ */
401
+ function failureDetail(error) {
402
+ const httpStatus = error.details?.httpStatus;
403
+ return {
404
+ ok: false,
405
+ ...typeof httpStatus === "number" ? { httpStatus } : {},
406
+ ...openfortFailureDetail(error)
407
+ };
408
+ }
409
+ /**
208
410
  * The two credentials Openfort needs from the signed-in session: the access
209
411
  * token it verifies, and an encryption session for automatic recovery. A host
210
412
  * that owns login passes `auth`; otherwise both come from the auth server.
@@ -217,70 +419,80 @@ function makeOpenfortCredentials(input) {
217
419
  function encryptionSessionUrl() {
218
420
  return `${authBaseUrl}/encryption-session`;
219
421
  }
220
- async function fetchBetterAuthAccessToken() {
422
+ /** The token from the host, or from the auth server; a refusal when it has none. */
423
+ async function requestAccessToken() {
424
+ if (auth !== void 0) return accessTokenRead((await auth.getAccessToken())?.trim());
425
+ const deadline = AbortSignal.timeout(AUTH_SERVER_REQUEST_TIMEOUT_MS);
426
+ const response = await send("token", accessTokenUrl(), {
427
+ credentials: "include",
428
+ cache: "no-store"
429
+ }, deadline);
430
+ if (!response.ok) return {
431
+ token: null,
432
+ failure: statusFailure("token", response.status)
433
+ };
434
+ const body = await readJson("token", response, deadline);
435
+ const token = authProvider === "oidc" ? Result.map(Schema.decodeUnknownResult(OidcTokenBody)(body), (reply) => reply?.token) : Result.map(Schema.decodeUnknownResult(BetterAuthSessionBody)(body), (reply) => reply?.session?.token);
436
+ if (Result.isFailure(token)) throw unusableReply("token", token.failure.issue, response.status);
437
+ return accessTokenRead(token.success?.trim());
438
+ }
439
+ /** Read the access token Openfort verifies. Each read leaves one `openfort.token` trace. */
440
+ async function readAccessToken() {
221
441
  clock.tokenCalls += 1;
222
442
  const tokenStartedAt = Date.now();
223
- try {
224
- if (auth !== void 0) {
225
- const token = (await auth.getAccessToken())?.trim() ?? "";
226
- diagnostic?.trace("openfort.token", {
227
- ok: token.length > 0,
228
- tokenPresent: token.length > 0,
229
- provider: authProvider,
230
- call: clock.tokenCalls,
231
- duration_ms: Date.now() - tokenStartedAt,
232
- elapsed_ms: sinceCycleStart(clock)
233
- });
234
- return token.length === 0 ? null : token;
235
- }
236
- const response = await fetch(accessTokenUrl(), {
237
- credentials: "include",
238
- cache: "no-store"
239
- });
240
- if (!response.ok) {
241
- diagnostic?.trace("openfort.token", {
242
- ok: false,
243
- tokenPresent: false,
244
- httpStatus: response.status,
245
- failure_mode: "unknown"
246
- });
247
- return null;
248
- }
249
- const body = Schema.decodeUnknownSync(AccessTokenBody)(await response.json());
250
- const token = (authProvider === "oidc" ? body.token : body.session?.token)?.trim();
251
- if (token === void 0 || token.length === 0) {
252
- diagnostic?.trace("openfort.token", {
253
- ok: false,
254
- tokenPresent: false,
255
- failure_mode: "unknown"
256
- });
257
- return null;
258
- }
443
+ const trace = (detail) => {
259
444
  diagnostic?.trace("openfort.token", {
260
- ok: true,
261
- tokenPresent: true,
445
+ ...detail,
262
446
  provider: authProvider,
263
447
  call: clock.tokenCalls,
264
448
  duration_ms: Date.now() - tokenStartedAt,
265
449
  elapsed_ms: sinceCycleStart(clock)
266
450
  });
267
- return token;
451
+ };
452
+ let read;
453
+ try {
454
+ read = await requestAccessToken();
268
455
  } catch (cause) {
269
- diagnostic?.trace("openfort.token", {
270
- ok: false,
456
+ const error = openfortProviderError("token", cause);
457
+ trace({
271
458
  tokenPresent: false,
272
- failure_mode: "unknown"
459
+ ...failureDetail(error)
273
460
  });
274
- throw openfortProviderError("token", cause);
461
+ throw error;
275
462
  }
463
+ trace(read.token === null ? {
464
+ tokenPresent: false,
465
+ ...failureDetail(read.failure)
466
+ } : {
467
+ ok: true,
468
+ tokenPresent: true
469
+ });
470
+ return read;
471
+ }
472
+ /**
473
+ * Openfort's token callback. A signed-out session is `null`, as Openfort
474
+ * expects. Any other refusal is thrown: Openfort passes a callback's throw on
475
+ * unchanged, so `configure` keeps the refusal's own mode instead of the
476
+ * `INVALID_TOKEN` refusal that Openfort makes from `null`.
477
+ */
478
+ async function fetchBetterAuthAccessToken() {
479
+ const read = await readAccessToken();
480
+ if (read.token !== null || read.failure.mode === "unauthorized") return read.token;
481
+ throw read.failure;
276
482
  }
277
483
  async function createEncryptionSession(accessToken) {
278
484
  if (auth !== void 0) {
279
485
  const sessionId = (await auth.createEncryptionSession(accessToken)).trim();
280
- if (sessionId.length === 0) throw Errors.providerError("openfort", "encryptionSession", /* @__PURE__ */ new Error("Openfort encryption session response missing sessionId"), { failure_mode: "unknown" });
486
+ if (sessionId.length === 0) throw openfortAnswerInvalid({
487
+ operation: "encryptionSession",
488
+ field: "sessionId",
489
+ problem: "missing",
490
+ reason: "empty"
491
+ });
281
492
  return { sessionId };
282
493
  }
283
- const response = await fetch(encryptionSessionUrl(), {
494
+ const deadline = AbortSignal.timeout(AUTH_SERVER_REQUEST_TIMEOUT_MS);
495
+ const response = await send("encryptionSession", encryptionSessionUrl(), {
284
496
  method: "POST",
285
497
  credentials: "include",
286
498
  headers: {
@@ -288,15 +500,17 @@ function makeOpenfortCredentials(input) {
288
500
  "Content-Type": "application/json"
289
501
  },
290
502
  body: JSON.stringify({})
291
- });
292
- if (!response.ok) throw Errors.providerError("openfort", "encryptionSession", /* @__PURE__ */ new Error(`Openfort encryption session failed (${response.status})`), {
293
- httpStatus: response.status,
294
- failure_mode: "unknown"
295
- });
296
- const sessionId = readField(await response.json(), "sessionId");
297
- if (typeof sessionId !== "string" || sessionId.length === 0) throw Errors.providerError("openfort", "encryptionSession", /* @__PURE__ */ new Error("Openfort encryption session response missing sessionId"), {
298
- httpStatus: response.status,
299
- failure_mode: "unknown"
503
+ }, deadline);
504
+ if (!response.ok) throw statusFailure("encryptionSession", response.status);
505
+ const reply = Schema.decodeUnknownResult(EncryptionSessionBody)(await readJson("encryptionSession", response, deadline));
506
+ if (Result.isFailure(reply)) throw unusableReply("encryptionSession", reply.failure.issue, response.status);
507
+ const sessionId = reply.success.sessionId.trim();
508
+ if (sessionId.length === 0) throw openfortAnswerInvalid({
509
+ operation: "encryptionSession",
510
+ field: "sessionId",
511
+ problem: "missing",
512
+ reason: "empty",
513
+ httpStatus: response.status
300
514
  });
301
515
  return {
302
516
  sessionId,
@@ -304,6 +518,7 @@ function makeOpenfortCredentials(input) {
304
518
  };
305
519
  }
306
520
  return {
521
+ readAccessToken,
307
522
  fetchBetterAuthAccessToken,
308
523
  createEncryptionSession
309
524
  };
@@ -357,9 +572,11 @@ const STALE_USER_ERROR_CODES = /* @__PURE__ */ new Set(["USER_NOT_FOUND"]);
357
572
  * `isTransportError` walks it in the Convex transport adapter.
358
573
  *
359
574
  * Known ceiling (#1435): a 401 that carries no recognized code is NOT healed.
360
- * `extractApiError` keeps the status only on `AuthenticationError`, so such a
361
- * payload is reachable. Add the code a live payload shows; do not add a message
362
- * match, because that trades one bug class for an ADR-0023 R4 violation.
575
+ * `extractApiError` keeps the 401 on every error it decodes, but the status
576
+ * alone does not name the stale user, so such a payload stays outside the
577
+ * heal and is named `unauthorized`. Add the code a live payload shows; do not
578
+ * add a message match, because that trades one bug class for an ADR-0023 R4
579
+ * violation.
363
580
  */
364
581
  function isStaleUserSignal(cause) {
365
582
  for (const link of causeChain(cause)) if (typeof link.error === "string" && STALE_USER_ERROR_CODES.has(link.error) || typeof link.code === "string" && STALE_USER_ERROR_CODES.has(link.code)) return true;
@@ -396,7 +613,7 @@ function makeWalletReadyCycle(input) {
396
613
  ok: false,
397
614
  failure_mode: "no-secure-context"
398
615
  });
399
- throw failOpenfort("configure", "no-secure-context", /* @__PURE__ */ new Error("Web Crypto unavailable: browser is not a secure context"));
616
+ throw failOpenfort("configure", "no-secure-context");
400
617
  }
401
618
  async function configureEmbeddedWallet(encryptionSession) {
402
619
  await openfort.embeddedWallet.configure({
@@ -440,21 +657,22 @@ function makeWalletReadyCycle(input) {
440
657
  if (isInsecureBrowserContext()) failNoSecureContext();
441
658
  await openfort.waitForInitialization();
442
659
  step("initialize");
443
- const accessToken = await credentials.fetchBetterAuthAccessToken();
444
- if (accessToken === null) throw openfortProviderError("token", /* @__PURE__ */ new Error("Better Auth access token unavailable for Openfort"));
660
+ const accessToken = await credentials.readAccessToken();
661
+ if (accessToken.token === null) throw accessToken.failure;
445
662
  step("token");
446
663
  let encryption;
447
664
  try {
448
- encryption = await credentials.createEncryptionSession(accessToken);
665
+ encryption = await credentials.createEncryptionSession(accessToken.token);
449
666
  } catch (cause) {
667
+ const error = openfortProviderError("encryptionSession", cause);
450
668
  let httpStatus;
451
669
  for (const link of causeChain(cause)) httpStatus ??= httpStatusOf(link);
452
670
  diagnostic?.trace("openfort.encryptionSession", {
453
671
  ok: false,
454
672
  ...httpStatus === void 0 ? {} : { httpStatus },
455
- failure_mode: "unknown"
673
+ ...openfortFailureDetail(error)
456
674
  });
457
- throw openfortProviderError("encryptionSession", cause);
675
+ throw error;
458
676
  }
459
677
  diagnostic?.trace("openfort.encryptionSession", {
460
678
  ok: true,
@@ -467,11 +685,12 @@ function makeWalletReadyCycle(input) {
467
685
  diagnostic?.trace("openfort.embeddedState", { state: embeddedState });
468
686
  step("embeddedState", { state: embeddedState });
469
687
  } catch (cause) {
688
+ const error = openfortProviderError("embeddedState", cause);
470
689
  diagnostic?.trace("openfort.embeddedState", {
471
690
  ok: false,
472
- failure_mode: "unknown"
691
+ ...openfortFailureDetail(error)
473
692
  });
474
- throw openfortProviderError("embeddedState", cause);
693
+ throw error;
475
694
  }
476
695
  if (embeddedState !== EmbeddedState.READY) {
477
696
  clearStaleOpenfortBrowserStorage(publishableKey);
@@ -484,7 +703,7 @@ function makeWalletReadyCycle(input) {
484
703
  const error = openfortProviderError("configure", cause);
485
704
  diagnostic?.trace("openfort.configure", {
486
705
  ok: false,
487
- failure_mode: openfortFailureMode(error)
706
+ ...openfortFailureDetail(error)
488
707
  });
489
708
  throw error;
490
709
  }
@@ -495,11 +714,12 @@ function makeWalletReadyCycle(input) {
495
714
  step("get");
496
715
  } catch (cause) {
497
716
  if (!isStaleUserSignal(cause)) {
717
+ const error = openfortProviderError("get", cause);
498
718
  diagnostic?.trace("openfort.get", {
499
719
  ok: false,
500
- failure_mode: "unknown"
720
+ ...openfortFailureDetail(error)
501
721
  });
502
- throw openfortProviderError("get", cause);
722
+ throw error;
503
723
  }
504
724
  diagnostic?.trace("openfort.get", {
505
725
  ok: false,
@@ -521,16 +741,18 @@ function makeWalletReadyCycle(input) {
521
741
  /**
522
742
  * Sign once; when Openfort reports the signer not ready, reset readiness, run
523
743
  * one fresh readiness cycle and sign again. A rejection by the person is never
524
- * retried, and a second not-ready answer is a named `signer-not-ready` failure.
744
+ * retried. A failure the readiness cycle already named (a mode, or an answer
745
+ * Capxul cannot use) is thrown as itself; any other second failure is a named
746
+ * `signer-not-ready` failure.
525
747
  */
526
748
  function recoveringSign(input) {
527
749
  return async (hash) => {
528
750
  try {
529
751
  return await input.sign(hash);
530
752
  } catch (cause) {
531
- const providerErrorCode = openfortSignerNotReadyCode(cause);
753
+ const signerNotReadyCode = openfortSignerNotReadyCode(cause);
532
754
  const normalized = signerFailure("openfort-embedded", "signUserOpHash", cause);
533
- if (normalized.code === "SIGNER_REJECTED" || providerErrorCode === void 0) throw normalized;
755
+ if (normalized.code === "SIGNER_REJECTED" || signerNotReadyCode === void 0) throw normalized;
534
756
  input.diagnostic?.trace("openfort.signerRecovery", {
535
757
  outcome: "started",
536
758
  attempt: 1,
@@ -549,23 +771,44 @@ function recoveringSign(input) {
549
771
  } catch (retryCause) {
550
772
  const retryFailure = signerFailure("openfort-embedded", "signUserOpHash", retryCause);
551
773
  if (retryFailure.code === "SIGNER_REJECTED") throw retryFailure;
774
+ const named = namedReadinessFailure(retryCause);
775
+ if (named !== void 0) {
776
+ input.diagnostic?.trace("openfort.signerRecovery", {
777
+ outcome: "failed",
778
+ attempt: 1,
779
+ ...openfortFailureDetail(named)
780
+ });
781
+ throw named;
782
+ }
552
783
  if (isSignerNotReadySignal(retryCause)) input.markUnavailable();
553
784
  input.diagnostic?.trace("openfort.signerRecovery", {
554
785
  outcome: "failed",
555
786
  attempt: 1,
556
787
  failure_mode: isSignerNotReadySignal(retryCause) ? "signer-not-ready" : "unknown"
557
788
  });
558
- throw Errors.providerError("openfort", "signerRecovery", retryCause, {
559
- failure_mode: "signer-not-ready",
789
+ throw Errors.providerUnavailable({
790
+ provider: "openfort",
791
+ operation: "signerRecovery",
792
+ mode: "signer-not-ready",
793
+ cause: retryCause,
560
794
  details: {
561
795
  provider_error_source: "openfort",
562
- provider_error_code: providerErrorCode
796
+ provider_error_code: signerNotReadyCode
563
797
  }
564
798
  });
565
799
  }
566
800
  }
567
801
  };
568
802
  }
803
+ /**
804
+ * A failure the readiness cycle already named: an answer Capxul cannot use, or
805
+ * a provider failure with a mode other than `signer-not-ready`.
806
+ */
807
+ function namedReadinessFailure(cause) {
808
+ if (!(cause instanceof CapxulError)) return void 0;
809
+ if (cause.code === "PROVIDER_RESPONSE_INVALID") return cause;
810
+ return cause.code === "PROVIDER_ERROR" && cause.mode !== void 0 && cause.mode !== "signer-not-ready" ? cause : void 0;
811
+ }
569
812
  //#endregion
570
813
  //#region src/signing/openfort-browser.ts
571
814
  function isolatedDiagnostic(diagnostic) {
@@ -688,11 +931,12 @@ function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
688
931
  });
689
932
  return address;
690
933
  } catch (cause) {
934
+ const error = openfortProviderError("getAddress", cause);
691
935
  diagnostic?.trace("openfort.address", {
692
936
  ok: false,
693
- failure_mode: "unknown"
937
+ ...openfortFailureDetail(error)
694
938
  });
695
- throw openfortProviderError("getAddress", cause);
939
+ throw error;
696
940
  }
697
941
  },
698
942
  resetSession: resetReadiness
@@ -762,8 +1006,8 @@ function readSessionEmail(storage) {
762
1006
  let session;
763
1007
  try {
764
1008
  session = JSON.parse(raw);
765
- } catch {
766
- throw new Error("devPrivateKeySigner: cached session is not valid JSON");
1009
+ } catch (cause) {
1010
+ throw new Error("devPrivateKeySigner: cached session is not valid JSON", { cause });
767
1011
  }
768
1012
  if (session === null) throw new Error("devPrivateKeySigner: cached session is not valid JSON");
769
1013
  const email = readField(session, "email");
@@ -970,8 +1214,10 @@ const EngineeringTelemetryBootstrapPolicy = Schema.Struct({
970
1214
  *
971
1215
  * - `protocol`: discriminator that lets future protocols coexist on the
972
1216
  * same endpoint without a wire-shape conflict.
973
- * - `version`: numeric version inside the protocol. Unknown versions MUST
974
- * fail decode with an INVALID_INPUT-class `CapxulError` at the SDK seam.
1217
+ * - `version`: numeric version inside the protocol. An unknown or missing
1218
+ * version MUST fail decode at the SDK seam as `PROVIDER_RESPONSE_INVALID`
1219
+ * (ADR-0039 D3) on field `version`, with problem `invalid` or `missing`:
1220
+ * the backend answered, and Capxul cannot use the answer.
975
1221
  * - `state`: the resolved bootstrap payload. Field shape matches the
976
1222
  * `BootstrapResolution` port; the SDK consumer can pass `state`
977
1223
  * directly (after `normalizeRuntimeUrl` on the two URL fields) into
@@ -1001,11 +1247,13 @@ const BootstrapEnvelope = Schema.Struct({
1001
1247
  //#endregion
1002
1248
  //#region src/applications/bootstrap-envelope.ts
1003
1249
  /**
1004
- * Decode a bootstrap body. A body the envelope refuses throws INVALID_INPUT
1005
- * (#119 AC1): unknown `version` literals, missing fields and brand-rule
1006
- * failures all land there. The engineering telemetry policy decodes on its
1007
- * own, so a policy the SDK does not admit turns SDK telemetry off instead of
1008
- * failing the bootstrap.
1250
+ * Decode a bootstrap body. The backend answered, so a body the envelope
1251
+ * refuses is the backend's answer that Capxul cannot use, not the caller's
1252
+ * input: it throws PROVIDER_RESPONSE_INVALID (ADR-0039 D3), named by the first
1253
+ * field that fails. Unknown `version` literals, missing fields, brand-rule
1254
+ * failures and runtime URLs that are not http or https all land there. The
1255
+ * engineering telemetry policy decodes on its own, so a policy the SDK does
1256
+ * not admit turns SDK telemetry off instead of failing the bootstrap.
1009
1257
  */
1010
1258
  function decodeBootstrapBody(body) {
1011
1259
  const state = readField(body, "state");
@@ -1014,7 +1262,7 @@ function decodeBootstrapBody(body) {
1014
1262
  ...body,
1015
1263
  state: withoutPolicy(state)
1016
1264
  } : body);
1017
- if (Result.isFailure(decoded)) throw Errors.invalidInput("bootstrapEnvelope", SchemaIssue.makeFormatterDefault()(decoded.failure));
1265
+ if (Result.isFailure(decoded)) throw refusedEnvelope(decoded.failure);
1018
1266
  const { state: decodedState } = decoded.success;
1019
1267
  const decodedEngineeringTelemetry = rawEngineeringTelemetry === void 0 ? void 0 : SchemaParser.decodeUnknownResult(EngineeringTelemetryBootstrapPolicy)(rawEngineeringTelemetry, { onExcessProperty: "error" });
1020
1268
  return {
@@ -1033,6 +1281,40 @@ function decodeBootstrapBody(body) {
1033
1281
  ...engineeringTelemetryOf(decodedEngineeringTelemetry)
1034
1282
  };
1035
1283
  }
1284
+ const MISSING = "missing";
1285
+ const WRONG_TYPE = "not the expected type";
1286
+ /**
1287
+ * The envelope's Schema issues, each with a fixed reason. The Schema's own
1288
+ * message can quote the body, and the body holds the session token.
1289
+ */
1290
+ const envelopeIssues = SchemaIssue.makeFormatterStandardSchemaV1({
1291
+ leafHook: (issue) => issue._tag === "MissingKey" ? MISSING : WRONG_TYPE,
1292
+ checkHook: () => WRONG_TYPE
1293
+ });
1294
+ /**
1295
+ * A body the envelope refuses, named by the path of the first field that
1296
+ * fails, or `body` when the whole body has the wrong shape.
1297
+ */
1298
+ function refusedEnvelope(issue) {
1299
+ const first = envelopeIssues(issue).issues[0];
1300
+ const path = first?.path?.map((segment) => String(typeof segment === "object" ? segment.key : segment)) ?? [];
1301
+ const missing = first?.message === MISSING;
1302
+ return unusableAnswer(path.length === 0 ? "body" : path.join("."), missing ? "missing" : "invalid", missing ? MISSING : WRONG_TYPE);
1303
+ }
1304
+ /**
1305
+ * The bootstrap answered, and Capxul cannot use `field` of its answer. `cause`
1306
+ * is what refused the field, when it holds none of the body.
1307
+ */
1308
+ function unusableAnswer(field, problem, reason, cause) {
1309
+ return Errors.providerResponseInvalid({
1310
+ provider: "convex",
1311
+ operation: "bootstrap",
1312
+ field,
1313
+ problem,
1314
+ reason,
1315
+ ...cause === void 0 ? {} : { cause }
1316
+ });
1317
+ }
1036
1318
  /** The bootstrap state without the policy, which decodes separately. */
1037
1319
  function withoutPolicy(state) {
1038
1320
  const { engineeringTelemetry: _policy, ...rest } = state;
@@ -1052,10 +1334,10 @@ function normalizeRuntimeUrl(field, raw) {
1052
1334
  let parsed;
1053
1335
  try {
1054
1336
  parsed = new URL(raw);
1055
- } catch {
1056
- throw Errors.invalidInput(field, "must be an http or https URL");
1337
+ } catch (cause) {
1338
+ throw unusableAnswer(`state.${field}`, "invalid", "must be an http or https URL", cause);
1057
1339
  }
1058
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw Errors.invalidInput(field, "must be an http or https URL");
1340
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw unusableAnswer(`state.${field}`, "invalid", "must be an http or https URL");
1059
1341
  return parsed.toString().replace(/\/$/, "");
1060
1342
  }
1061
1343
  //#endregion
@@ -1166,8 +1448,14 @@ var HttpBootstrapAdapter = class {
1166
1448
  return resolution;
1167
1449
  },
1168
1450
  catch: (cause) => {
1169
- if (cause instanceof CapxulError && cause.code === "INVALID_INPUT") return bootstrapErrorFromCapxul("invalidInput", cause);
1170
- return bootstrapErrorFromCapxul("malformedBody", Errors.providerError("convex", "bootstrap", cause instanceof Error ? cause : new Error(String(cause))));
1451
+ if (cause instanceof CapxulError && cause.code === "PROVIDER_RESPONSE_INVALID") return bootstrapErrorFromCapxul("malformedBody", cause);
1452
+ return bootstrapErrorFromCapxul("malformedBody", Errors.providerResponseInvalid({
1453
+ provider: "convex",
1454
+ operation: "bootstrap",
1455
+ field: "body",
1456
+ problem: "invalid",
1457
+ cause
1458
+ }));
1171
1459
  }
1172
1460
  });
1173
1461
  return Effect.promise(() => safeText(res)).pipe(Effect.flatMap((body) => {
@@ -1182,7 +1470,9 @@ var HttpBootstrapAdapter = class {
1182
1470
  ...edgeRequestId === void 0 ? {} : { edgeRequestId }
1183
1471
  };
1184
1472
  if (res.status === 401) return Effect.fail(bootstrapErrorFromCapxul("notAuthenticated", keyRefused(input, evidence)));
1185
- return Effect.fail(bootstrapErrorFromCapxul("provider", Errors.providerError("convex", "bootstrap", /* @__PURE__ */ new Error(`HTTP ${res.status}${edgeError === void 0 ? "" : ` ${edgeError}`}`), {
1473
+ return Effect.fail(bootstrapErrorFromCapxul("provider", providerStatusFailure({
1474
+ provider: "convex",
1475
+ operation: "bootstrap",
1186
1476
  httpStatus: res.status,
1187
1477
  details: evidence
1188
1478
  })));
@@ -1372,43 +1662,23 @@ function makeIdentityMethods(deps) {
1372
1662
  return toCapxulResult(loadIdentityProgram, identityDepsContext(deps), deps.runPromise);
1373
1663
  },
1374
1664
  async completeProfile(profile, options) {
1375
- if (options?.signal?.aborted) return {
1376
- ok: false,
1377
- error: Errors.cancelled({ operation: onboardings.complete.name })
1378
- };
1379
- if (typeof profile !== "object" || profile === null || typeof profile.displayName !== "string" || typeof profile.country !== "string" || typeof profile.handle !== "string") return {
1380
- ok: false,
1381
- error: Errors.invalidInput("profile", "invalid profile")
1382
- };
1665
+ const refuse = (error) => runCapxulEffect(Effect.fail(error), options, onboardings.complete.name, deps.runPromise);
1666
+ if (options?.signal?.aborted) return refuse(Errors.cancelled({ operation: onboardings.complete.name }));
1667
+ if (typeof profile !== "object" || profile === null || typeof profile.displayName !== "string" || typeof profile.country !== "string" || typeof profile.handle !== "string") return refuse(Errors.invalidInput("profile", "invalid profile"));
1383
1668
  const displayName = profile.displayName.trim();
1384
1669
  const handle = profile.handle.trim();
1385
- if (displayName.length === 0) return {
1386
- ok: false,
1387
- error: Errors.invalidInput("displayName", "must not be empty")
1388
- };
1389
- if (handle.length === 0) return {
1390
- ok: false,
1391
- error: Errors.invalidInput("handle", "must not be empty")
1392
- };
1670
+ if (displayName.length === 0) return refuse(Errors.invalidInput("displayName", "must not be empty"));
1671
+ if (handle.length === 0) return refuse(Errors.invalidInput("handle", "must not be empty"));
1393
1672
  let country;
1394
1673
  try {
1395
1674
  country = toCountryCode(profile.country);
1396
- } catch {
1397
- return {
1398
- ok: false,
1399
- error: Errors.invalidInput("country", "unsupported country")
1400
- };
1675
+ } catch (cause) {
1676
+ return refuse(Errors.invalidInput("country", "unsupported country", cause));
1401
1677
  }
1402
1678
  const carried = checkProfileCarriage(profile);
1403
- if (!carried.ok) return {
1404
- ok: false,
1405
- error: Errors.invalidInput(carried.field, carried.message)
1406
- };
1679
+ if (!carried.ok) return refuse(Errors.invalidInput(carried.field, carried.message));
1407
1680
  const session = deps.actor?.authSession();
1408
- if (session === void 0 || session === null) return {
1409
- ok: false,
1410
- error: Errors.notAuthenticated()
1411
- };
1681
+ if (session === void 0 || session === null) return refuse(noSessionError(deps.actor));
1412
1682
  return runPortEffect(deps.identityPort.completeOnboarding({
1413
1683
  authUserId: session.authUserId,
1414
1684
  email: session.email,
@@ -1597,10 +1867,10 @@ function normalizeOrganization(input) {
1597
1867
  let country;
1598
1868
  try {
1599
1869
  country = toCountryCode(input.country);
1600
- } catch {
1870
+ } catch (cause) {
1601
1871
  return {
1602
1872
  ok: false,
1603
- error: Errors.invalidInput("country", "must be an ISO country code")
1873
+ error: Errors.invalidInput("country", "must be an ISO country code", cause)
1604
1874
  };
1605
1875
  }
1606
1876
  const bio = input.bio?.trim() ?? "";
@@ -2242,7 +2512,43 @@ const OBSERVED_CONVEX_MUTATIONS = new Set(OPERATIONS.filter((row) => row.kind ==
2242
2512
  * id. The WebSocket client wraps it as `[CONVEX Q(path)] … Called by client`.
2243
2513
  */
2244
2514
  const REDACTED_ERROR = /^(?:\[CONVEX [QMA?]\([^)]*\)\] )?\[Request ID: ([^\]\s]+)\] Server Error(?:\s+Called by client)?$/u;
2245
- function mapToCapxulError(operation, err, transport) {
2515
+ /** Whole milliseconds since `startedAt`, a `performance.now()` reading at the call's start. */
2516
+ function elapsedSince(startedAt) {
2517
+ return Math.round(performance.now() - startedAt);
2518
+ }
2519
+ /**
2520
+ * Start one call's clock and return the mapper for its failure, so a redacted
2521
+ * refusal carries how long the call ran (CAP-387). `transportAtFailure` reads
2522
+ * the action's reconnect evidence when the call fails.
2523
+ */
2524
+ function timedFailure(operation, transportAtFailure) {
2525
+ const startedAt = performance.now();
2526
+ return (err) => mapToConvexCallError(operation, err, elapsedSince(startedAt), transportAtFailure?.());
2527
+ }
2528
+ /**
2529
+ * A refusal for want of a session (`NOT_AUTHENTICATED`) from a call that ran
2530
+ * signed out because the auth provider failed to give the session's token:
2531
+ * the call fails with that token failure, its code, mode and class, and not as
2532
+ * a person who signed out (CAP-404). The refusal stays the cause.
2533
+ * `tokenFailure` is read only for such a refusal.
2534
+ */
2535
+ function withTokenFailure(mapFailure, tokenFailure) {
2536
+ return (err) => {
2537
+ const refusal = mapFailure(err);
2538
+ const reason = refusal.publicCode === "NOT_AUTHENTICATED" ? tokenFailure() : void 0;
2539
+ if (reason === void 0) return refusal;
2540
+ return new ConvexCallError({
2541
+ operation: refusal.operation,
2542
+ publicCode: reason.code,
2543
+ publicError: reason,
2544
+ cause: refusal,
2545
+ tokenFailure: true,
2546
+ ...reason.details === void 0 ? {} : { details: reason.details }
2547
+ });
2548
+ };
2549
+ }
2550
+ /** `elapsedMs` is how long the call ran, from {@link elapsedSince}. */
2551
+ function mapToCapxulError(operation, err, elapsedMs, transport) {
2246
2552
  const decoded = decodeConvexError(err);
2247
2553
  if (decoded !== null) return decoded;
2248
2554
  if (err instanceof CapxulError) return err;
@@ -2256,7 +2562,7 @@ function mapToCapxulError(operation, err, transport) {
2256
2562
  });
2257
2563
  }
2258
2564
  if (err instanceof Error && isTransportError(err)) return Errors.networkError(operation, err, { provider: "convex" });
2259
- return backendFailure(operation, err);
2565
+ return backendFailure(operation, err, elapsedMs);
2260
2566
  }
2261
2567
  /**
2262
2568
  * A failure the backend raised outside its own error boundary: Convex
@@ -2273,7 +2579,7 @@ function mapToCapxulError(operation, err, transport) {
2273
2579
  * the reply says where to look instead. The text matches stay for a backend
2274
2580
  * that does not redact (development, local).
2275
2581
  */
2276
- function backendFailure(operation, err) {
2582
+ function backendFailure(operation, err, elapsedMs) {
2277
2583
  const text = err instanceof Error ? err.message : String(err);
2278
2584
  const redacted = REDACTED_ERROR.exec(text.trim())?.[1];
2279
2585
  if (redacted !== void 0) return new CapxulError("UNKNOWN", `The backend refused ${operation} and gave no detail. Find Convex request ${redacted} in the backend logs. If the backend can be newer than this client, update the CLI or SDK.`, {
@@ -2283,7 +2589,8 @@ function backendFailure(operation, err) {
2283
2589
  operation,
2284
2590
  provider: "convex",
2285
2591
  requestId: redacted,
2286
- reason: "backend-error-redacted"
2592
+ reason: "backend-error-redacted",
2593
+ ...elapsedMs === void 0 ? {} : { elapsed_ms: elapsedMs }
2287
2594
  }
2288
2595
  });
2289
2596
  const requestId = /\[Request ID: ([^\]\s]+)\]/u.exec(text)?.[1];
@@ -2318,8 +2625,8 @@ function backendFailure(operation, err) {
2318
2625
  details: where
2319
2626
  });
2320
2627
  }
2321
- function mapToConvexCallError(operation, err, transport) {
2322
- return convexCallErrorFromCapxul(operation, mapToCapxulError(operation, err, transport), transport);
2628
+ function mapToConvexCallError(operation, err, elapsedMs, transport) {
2629
+ return convexCallErrorFromCapxul(operation, mapToCapxulError(operation, err, elapsedMs, transport), transport);
2323
2630
  }
2324
2631
  function logActionTransportFailure(operation, error) {
2325
2632
  if (error.transport === void 0) return Effect.void;
@@ -2399,9 +2706,13 @@ function observedConvexArgs(input) {
2399
2706
  /**
2400
2707
  * Subscribe one query, and the connection state when the caller asks for it.
2401
2708
  * The returned stop always finishes, and a callback never runs after it.
2709
+ * `tokenFailure` says why the socket runs signed out, when the auth provider
2710
+ * failed to give a token; a refusal for want of a session reports it instead
2711
+ * (CAP-404).
2402
2712
  */
2403
2713
  function subscribeConvexQuery(client, fn, args, callback, options) {
2404
2714
  const path = getFunctionName(fn);
2715
+ let startedAt = performance.now();
2405
2716
  let active = true;
2406
2717
  let unsubscribeQuery;
2407
2718
  let unsubscribeConnection;
@@ -2422,6 +2733,7 @@ function subscribeConvexQuery(client, fn, args, callback, options) {
2422
2733
  const observe = (state) => {
2423
2734
  if (!active || previous === state.isWebSocketConnected) return;
2424
2735
  previous = state.isWebSocketConnected;
2736
+ if (previous) startedAt = performance.now();
2425
2737
  try {
2426
2738
  options.onConnectionChange?.(previous);
2427
2739
  } catch {}
@@ -2430,15 +2742,21 @@ function subscribeConvexQuery(client, fn, args, callback, options) {
2430
2742
  observe(client.connectionState());
2431
2743
  }
2432
2744
  unsubscribeQuery = client.onUpdate(fn, args, (value) => {
2745
+ startedAt = void 0;
2433
2746
  if (active) callback({
2434
2747
  status: "ok",
2435
2748
  value
2436
2749
  });
2437
2750
  }, (err) => {
2438
- if (active) callback({
2439
- status: "error",
2440
- error: mapToCapxulError(path, err)
2441
- });
2751
+ const elapsedMs = startedAt === void 0 ? void 0 : elapsedSince(startedAt);
2752
+ startedAt = void 0;
2753
+ if (active) {
2754
+ const error = mapToCapxulError(path, err, elapsedMs);
2755
+ callback({
2756
+ status: "error",
2757
+ error: (error.code === "NOT_AUTHENTICATED" ? options?.tokenFailure?.() : void 0) ?? error
2758
+ });
2759
+ }
2442
2760
  });
2443
2761
  if (!active) unsubscribeQuery();
2444
2762
  return stop;
@@ -2448,13 +2766,7 @@ function subscribeConvexQuery(client, fn, args, callback, options) {
2448
2766
  }
2449
2767
  }
2450
2768
  //#endregion
2451
- //#region src/platform/transport/convex-adapter.ts
2452
- /**
2453
- * A socket that has not emitted `close` within this window is not going to.
2454
- * ponytail: one fixed bound; `closeTimeoutMs` is the knob if a slow runtime
2455
- * ever needs a different one.
2456
- */
2457
- const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
2769
+ //#region src/platform/transport/convex-socket-auth.ts
2458
2770
  /**
2459
2771
  * The fetcher the Convex client's auth handshake gets. Convex first asks for
2460
2772
  * "a possibly cached token" (`forceRefreshToken: false`) and, as soon as the
@@ -2472,6 +2784,94 @@ const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
2472
2784
  function freshTokenHandshake(fetchToken) {
2473
2785
  return async (args) => args.forceRefreshToken ? fetchToken(args) : null;
2474
2786
  }
2787
+ /**
2788
+ * The socket's session: each auth handshake the adapter starts, and the
2789
+ * failure in the latest token answer. Convex runs a call signed out when the
2790
+ * auth provider gives no token, so that failure, not the backend's refusal for
2791
+ * want of a session, says why the call failed (CAP-404).
2792
+ */
2793
+ var ConvexSocketAuth = class {
2794
+ #client;
2795
+ #tokenProvider;
2796
+ /** Counts the handshakes. An answer to an older one changes nothing. */
2797
+ #handshake = 0;
2798
+ /** The failure in the latest token answer, and the handshake that asked for it. */
2799
+ #failure;
2800
+ constructor(client, tokenProvider) {
2801
+ this.#client = client;
2802
+ this.#tokenProvider = tokenProvider;
2803
+ }
2804
+ /**
2805
+ * The session changed: the last session's failure explains no refusal now,
2806
+ * and Convex asks for the new session's token.
2807
+ */
2808
+ handshake() {
2809
+ this.#failure = void 0;
2810
+ this.#ask();
2811
+ }
2812
+ /** Start a handshake: Convex asks for the session's token again. */
2813
+ #ask() {
2814
+ const handshake = ++this.#handshake;
2815
+ this.#client.setAuth(freshTokenHandshake(async (args) => {
2816
+ const answer = await this.#tokenProvider(args);
2817
+ if (handshake === this.#handshake) this.#failure = answer instanceof CapxulError ? {
2818
+ failure: answer,
2819
+ handshake
2820
+ } : void 0;
2821
+ return answer instanceof CapxulError ? null : answer;
2822
+ }));
2823
+ }
2824
+ /**
2825
+ * Why a call on the socket ran signed out: the failure in the latest token
2826
+ * answer, or none when the provider gave a token or refused the session.
2827
+ * Convex asks for a token again only when the session changes, so the first
2828
+ * refusal after each failed answer starts a new handshake, and a later call
2829
+ * runs as the session once the provider answers.
2830
+ */
2831
+ signedOutBecause() {
2832
+ const latest = this.#failure;
2833
+ if (latest === void 0) return void 0;
2834
+ if (latest.handshake === this.#handshake) this.#ask();
2835
+ return latest.failure;
2836
+ }
2837
+ };
2838
+ //#endregion
2839
+ //#region src/platform/transport/convex-fresh-query.ts
2840
+ /**
2841
+ * One query read past the client's cache, over HTTP with its own token. When
2842
+ * the auth provider gave no token, the read runs signed out, and a refusal for
2843
+ * want of a session reports that token failure instead (CAP-404).
2844
+ */
2845
+ function freshQuery(deps, fn, args, path) {
2846
+ let tokenFailure;
2847
+ return Effect.tryPromise({
2848
+ try: async (signal) => {
2849
+ const observed = args();
2850
+ const token = await deps.tokenProvider?.({ forceRefreshToken: false });
2851
+ if (signal.aborted) throw Errors.cancelled({ operation: path });
2852
+ const client = new ConvexHttpClient(deps.convexUrl, {
2853
+ logger: deps.logger,
2854
+ fetch: (input, init) => globalThis.fetch(input, {
2855
+ ...init,
2856
+ signal
2857
+ })
2858
+ });
2859
+ if (token instanceof CapxulError) tokenFailure = token;
2860
+ else if (typeof token === "string") client.setAuth(token);
2861
+ const query = fn;
2862
+ return client.query(query, observed);
2863
+ },
2864
+ catch: withTokenFailure(timedFailure(path), () => tokenFailure)
2865
+ });
2866
+ }
2867
+ //#endregion
2868
+ //#region src/platform/transport/convex-adapter.ts
2869
+ /**
2870
+ * A socket that has not emitted `close` within this window is not going to.
2871
+ * ponytail: one fixed bound; `closeTimeoutMs` is the knob if a slow runtime
2872
+ * ever needs a different one.
2873
+ */
2874
+ const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
2475
2875
  /** Run a Convex call under the current trace parent, when there is one. */
2476
2876
  const withTraceparent = (run) => Effect.flatMap(Effect.serviceOption(Tracer.ParentSpan), (parent) => run(parent._tag === "Some" ? formatTraceparent(parent.value) : void 0));
2477
2877
  var ConvexCallAdapter = class {
@@ -2479,6 +2879,7 @@ var ConvexCallAdapter = class {
2479
2879
  #convexUrl;
2480
2880
  #logger;
2481
2881
  #tokenProvider;
2882
+ #socketAuth;
2482
2883
  #applicationId;
2483
2884
  #observation;
2484
2885
  #connectionMonitor;
@@ -2494,12 +2895,13 @@ var ConvexCallAdapter = class {
2494
2895
  logger: this.#logger
2495
2896
  });
2496
2897
  this.#tokenProvider = deps.tokenProvider;
2898
+ this.#socketAuth = deps.tokenProvider === void 0 ? void 0 : new ConvexSocketAuth(this.#client, deps.tokenProvider);
2497
2899
  this.#applicationId = deps.applicationId;
2498
2900
  this.#observation = deps.observation;
2499
2901
  this.refreshAuth();
2500
2902
  }
2501
2903
  refreshAuth() {
2502
- if (this.#tokenProvider) this.#client.setAuth(freshTokenHandshake(this.#tokenProvider));
2904
+ this.#socketAuth?.handshake();
2503
2905
  }
2504
2906
  observeConnection(observer) {
2505
2907
  return this.#connectionMonitor.observe(this.#client, observer);
@@ -2507,35 +2909,24 @@ var ConvexCallAdapter = class {
2507
2909
  query(fn, args, options) {
2508
2910
  const path = getFunctionName(fn);
2509
2911
  return withTraceparent((traceparent) => {
2912
+ const observed = () => this.#observedArgs(OBSERVED_CONVEX_QUERIES, path, args, traceparent);
2913
+ if (options?.fresh === true) return freshQuery({
2914
+ convexUrl: this.#convexUrl,
2915
+ logger: this.#logger,
2916
+ tokenProvider: this.#tokenProvider
2917
+ }, fn, observed, path);
2510
2918
  return Effect.tryPromise({
2511
- try: (signal) => {
2512
- const observed = this.#observedArgs(OBSERVED_CONVEX_QUERIES, path, args, traceparent);
2513
- return options?.fresh === true ? this.#freshQuery(fn, observed, signal, path) : this.#client.query(fn, observed);
2514
- },
2515
- catch: (cause) => mapToConvexCallError(path, cause)
2919
+ try: () => this.#client.query(fn, observed()),
2920
+ catch: withTokenFailure(timedFailure(path), () => this.#socketAuth?.signedOutBecause())
2516
2921
  });
2517
2922
  });
2518
2923
  }
2519
- async #freshQuery(fn, args, signal, path) {
2520
- const token = await this.#tokenProvider?.({ forceRefreshToken: false });
2521
- if (signal.aborted) throw Errors.cancelled({ operation: path });
2522
- const client = new ConvexHttpClient(this.#convexUrl, {
2523
- logger: this.#logger,
2524
- fetch: (input, init) => globalThis.fetch(input, {
2525
- ...init,
2526
- signal
2527
- })
2528
- });
2529
- if (token !== void 0 && token !== null) client.setAuth(token);
2530
- const query = fn;
2531
- return client.query(query, args);
2532
- }
2533
2924
  mutation(fn, args) {
2534
2925
  const path = getFunctionName(fn);
2535
2926
  return withTraceparent((traceparent) => {
2536
2927
  return Effect.tryPromise({
2537
2928
  try: () => this.#client.mutation(fn, this.#observedArgs(OBSERVED_CONVEX_MUTATIONS, path, args, traceparent)),
2538
- catch: (cause) => mapToConvexCallError(path, cause)
2929
+ catch: withTokenFailure(timedFailure(path), () => this.#socketAuth?.signedOutBecause())
2539
2930
  });
2540
2931
  });
2541
2932
  }
@@ -2545,10 +2936,7 @@ var ConvexCallAdapter = class {
2545
2936
  const connectionAtStart = connectionState(this.#client);
2546
2937
  return Effect.tryPromise({
2547
2938
  try: () => this.#client.action(fn, this.#observedArgs(OBSERVED_CONVEX_ACTIONS, path, args, traceparent)),
2548
- catch: (cause) => {
2549
- const transport = this.#connectionMonitor.reconnectEvidence(connectionAtStart, connectionState(this.#client));
2550
- return mapToConvexCallError(path, cause, transport);
2551
- }
2939
+ catch: withTokenFailure(timedFailure(path, () => this.#connectionMonitor.reconnectEvidence(connectionAtStart, connectionState(this.#client))), () => this.#socketAuth?.signedOutBecause())
2552
2940
  }).pipe(Effect.tapError((error) => logActionTransportFailure(path, error)));
2553
2941
  });
2554
2942
  }
@@ -2571,7 +2959,10 @@ var ConvexCallAdapter = class {
2571
2959
  }
2572
2960
  subscribe(fn, args, callback, options) {
2573
2961
  return Effect.try({
2574
- try: () => subscribeConvexQuery(this.#client, fn, args, callback, options),
2962
+ try: () => subscribeConvexQuery(this.#client, fn, args, callback, {
2963
+ ...options,
2964
+ tokenFailure: () => this.#socketAuth?.signedOutBecause()
2965
+ }),
2575
2966
  catch: (cause) => mapToConvexCallError(getFunctionName(fn), cause)
2576
2967
  }).pipe(Effect.tap(() => Effect.sync(() => {
2577
2968
  callback({ status: "loading" });
@@ -2717,13 +3108,14 @@ function cloneGroupInput(input) {
2717
3108
  //#endregion
2718
3109
  //#region src/observability/telemetry/from-posthog.ts
2719
3110
  /**
2720
- * Drop props that must never cross to a host-owned external sink. Today that is
2721
- * the `$exception` `details` blob: `captureException` serializes
2722
- * `CapxulError.details` (e.g. `{ asset, available, required }`, `{ name }`,
2723
- * `{ accountId }` — errors.ts) into it, and the shared redactor has no
2724
- * `$exception` rule, so it is stripped here at the boundary (infra#1037). The
2725
- * safe fields (error code, operation, failure_mode, the fixed leak-safe message,
2726
- * stack frames) are preserved.
3111
+ * Drop a `details` prop before it crosses to the host-owned sink.
3112
+ * `CapxulError.details` can hold amounts, names and account IDs (e.g.
3113
+ * `{ asset, available, required }`), and the shared redactor has no
3114
+ * `$exception` rule (infra#1037). `captureException` does not send that prop:
3115
+ * it sends the allow-listed detail as top-level keys and as `error_details`.
3116
+ * The strip drops only a top-level `details` key. It does not read
3117
+ * `error_details`, where `bootstrap_failed` puts the masked `details` object
3118
+ * (`failureEvidenceProps`).
2727
3119
  */
2728
3120
  function stripHostUnsafeProps(props) {
2729
3121
  if (props === void 0) return void 0;
@@ -2731,10 +3123,6 @@ function stripHostUnsafeProps(props) {
2731
3123
  return safe;
2732
3124
  }
2733
3125
  /**
2734
- * The subset of a posthog-js client the seam calls. `identify` / `group` /
2735
- * `reset` are optional — a host that only wants event capture can omit them.
2736
- */
2737
- /**
2738
3126
  * Adapt the host's already-initialized posthog-like client into a
2739
3127
  * `TelemetryPort` for the `telemetry` prop / input. This port SUPPLANTS the
2740
3128
  * SDK's no-op default telemetry sink (`production.ts` binds it via
@@ -2806,6 +3194,7 @@ function postHogObservability(client, options = {}) {
2806
3194
  const policy = {
2807
3195
  client,
2808
3196
  capxulEnv: options.capxulEnv ?? "unknown",
3197
+ ...options.failureCapture === void 0 ? {} : { failureCapture: options.failureCapture },
2809
3198
  snapshot,
2810
3199
  deliver: (capture) => {
2811
3200
  try {
@@ -2858,6 +3247,20 @@ function hostEngineeringPolicy(observability) {
2858
3247
  };
2859
3248
  }
2860
3249
  //#endregion
3250
+ //#region ../observability/src/delivery/budget.ts
3251
+ /** Fixed admission limits for one CLI invocation and its SDK client. */
3252
+ const CLI_ENGINEERING_LIMITS = Object.freeze({
3253
+ records: 80,
3254
+ ordinaryRecords: 64,
3255
+ recordBytes: 16384,
3256
+ recordFields: 48,
3257
+ requests: 5,
3258
+ ordinaryRequests: 2,
3259
+ requestBytes: 1572864,
3260
+ metricSeries: 16,
3261
+ requestTimeoutMs: 1e3
3262
+ });
3263
+ //#endregion
2861
3264
  //#region ../observability/src/delivery/admission.ts
2862
3265
  function newDeliveryState(budget, ownsMetrics) {
2863
3266
  return {
@@ -2901,7 +3304,7 @@ function closeDelivery(state) {
2901
3304
  if (state.ownsMetrics) state.budget.metricRegistry.clear();
2902
3305
  }
2903
3306
  const text = (value) => typeof value === "string" ? value.slice(0, 128) : "capxul.engineering";
2904
- const attributes = (input, limit = 32) => {
3307
+ const attributes = (input, limit = CLI_ENGINEERING_LIMITS.recordFields) => {
2905
3308
  const out = {};
2906
3309
  let count = 0;
2907
3310
  let bytes = 0;
@@ -2938,8 +3341,20 @@ const failureText = (failure, key, limit, source) => {
2938
3341
  return;
2939
3342
  }
2940
3343
  };
3344
+ /**
3345
+ * A `CapxulError` reads as its failure row reads (`maskedMessage`), so a
3346
+ * factory message keeps the word after its field name (CAP-427). The row
3347
+ * reads the same error, so the exporter reads nothing more than the row does.
3348
+ */
3349
+ const capxulMessage = (failure, source) => {
3350
+ try {
3351
+ return isCapxulError(failure) ? maskedMessage(failure, source).slice(0, 2048) : void 0;
3352
+ } catch {
3353
+ return;
3354
+ }
3355
+ };
2941
3356
  const exportFailure = (failure, source) => {
2942
- const message = failureText(failure, "message", 2048, source) ?? (typeof failure === "string" ? redactSecrets(failure, source).slice(0, 2048) : "Engineering operation failed");
3357
+ const message = capxulMessage(failure, source) ?? failureText(failure, "message", 2048, source) ?? (typeof failure === "string" ? redactSecrets(failure, source).slice(0, 2048) : "Engineering operation failed");
2943
3358
  const code = failureText(failure, "code", 128);
2944
3359
  const error = new Error((code === void 0 ? message : `${message} [code=${code}]`).slice(0, 2048));
2945
3360
  error.name = failureText(failure, "_tag", 128) ?? failureText(failure, "name", 128) ?? "Error";
@@ -3090,7 +3505,10 @@ function admittedTracer(state, delegate) {
3090
3505
  ...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
3091
3506
  });
3092
3507
  }
3093
- /** The span as a producer sees it: at most 32 fields and 2000 attribute bytes, 2 events, 2 links. */
3508
+ /**
3509
+ * The span as a producer sees it: at most `recordFields` fields and 2000
3510
+ * attribute bytes, 2 events, 2 links.
3511
+ */
3094
3512
  function boundedSpan(state, span) {
3095
3513
  const { budget } = state;
3096
3514
  const wrapped = inheritSpan(span);
@@ -3098,7 +3516,7 @@ function boundedSpan(state, span) {
3098
3516
  let ended = false;
3099
3517
  Object.defineProperties(wrapped, {
3100
3518
  attribute: { value: (key, value) => {
3101
- if (fields >= 32 && !span.attributes.has(key)) {
3519
+ if (fields >= CLI_ENGINEERING_LIMITS.recordFields && !span.attributes.has(key)) {
3102
3520
  budget.drop();
3103
3521
  return;
3104
3522
  }
@@ -3147,19 +3565,6 @@ function boundedSpan(state, span) {
3147
3565
  return wrapped;
3148
3566
  }
3149
3567
  //#endregion
3150
- //#region ../observability/src/delivery/budget.ts
3151
- /** Fixed admission limits for one CLI invocation and its SDK client. */
3152
- const CLI_ENGINEERING_LIMITS = Object.freeze({
3153
- records: 80,
3154
- ordinaryRecords: 64,
3155
- recordBytes: 16384,
3156
- requests: 5,
3157
- ordinaryRequests: 2,
3158
- requestBytes: 1572864,
3159
- metricSeries: 16,
3160
- requestTimeoutMs: 1e3
3161
- });
3162
- //#endregion
3163
3568
  //#region ../observability/src/delivery/requests.ts
3164
3569
  function admittedSerialization(state, delegate) {
3165
3570
  return {
@@ -3307,8 +3712,8 @@ const validateEngineeringTelemetryConfig = (config) => {
3307
3712
  let url;
3308
3713
  try {
3309
3714
  url = new URL(config.host);
3310
- } catch {
3311
- throw new TypeError("engineering telemetry host must be an absolute HTTPS URL");
3715
+ } catch (cause) {
3716
+ throw new TypeError("engineering telemetry host must be an absolute HTTPS URL", { cause });
3312
3717
  }
3313
3718
  if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0 || !(url.hostname === "posthog.com" || url.hostname.endsWith(".posthog.com"))) throw new TypeError("engineering telemetry host must be a credential-free HTTPS origin");
3314
3719
  if (!ENGINEERING_CAPXUL_ENV_SET.has(config.capxulEnv)) throw new TypeError("engineering telemetry capxulEnv is not canonical");
@@ -3368,7 +3773,8 @@ const MINIMAL_CORRELATIONS = /* @__PURE__ */ new Set([
3368
3773
  "correlation_id",
3369
3774
  "journey_id",
3370
3775
  "invocation_id",
3371
- "posthogDistinctId"
3776
+ "posthogDistinctId",
3777
+ "failure_correlation_id"
3372
3778
  ]);
3373
3779
  const MINIMAL_METRIC_LABELS = /* @__PURE__ */ new Set([
3374
3780
  "command",
@@ -3381,6 +3787,12 @@ const MINIMAL_METRIC_LABELS = /* @__PURE__ */ new Set([
3381
3787
  const MINIMAL_METRIC_NAMES = /* @__PURE__ */ new Set(["capxul.cli.command.count", "capxul.cli.command.duration"]);
3382
3788
  const traceId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{32}$/u.test(value);
3383
3789
  const spanId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{16}$/u.test(value);
3790
+ /**
3791
+ * A correlation id the minimal export carries: an identifier with no `/`, not
3792
+ * `0x` hex, and unchanged by the secret mask. The CLI quotes a backend
3793
+ * correlation id as its `ref` only when it passes, so the ref finds a row.
3794
+ */
3795
+ const minimalCorrelationId = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value) && !/^0x[0-9a-f]+$/iu.test(value) && redactSecrets(value) === value;
3384
3796
  const minimalName = (value) => typeof value === "string" && (MINIMAL_LOG_NAMES.has(value) || isCapxulOperation(value) || CLI_COMMAND_NAMES.some((command) => value === `cli.command.${command}`)) ? value : "capxul.engineering";
3385
3797
  const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
3386
3798
  ["outcome", [
@@ -3392,6 +3804,8 @@ const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
3392
3804
  ]],
3393
3805
  ["mode", FAILURE_MODES],
3394
3806
  ["failure_mode", FAILURE_MODES],
3807
+ ["failure_class", FAILURE_CLASSES],
3808
+ ["problem", PROVIDER_PROBLEMS],
3395
3809
  ["capxul_env", ENGINEERING_CAPXUL_ENVS],
3396
3810
  ["producer", ["browser", "server"]],
3397
3811
  ["command", CLI_COMMAND_NAMES],
@@ -3420,7 +3834,7 @@ const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
3420
3834
  "healStaleCache",
3421
3835
  "walletReady"
3422
3836
  ]],
3423
- ["provider", ["better-auth", "oidc"]],
3837
+ ["provider", [...PROVIDERS, "oidc"]],
3424
3838
  ["cli_identity_kind", ["anonymous", "identified"]],
3425
3839
  ["output_mode", ["human", "json"]],
3426
3840
  ["setup_status", ORGANIZATION_SETUP_STATUSES],
@@ -3439,25 +3853,36 @@ const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
3439
3853
  const MINIMAL_CODE_KEYS = /* @__PURE__ */ new Set([
3440
3854
  "error.code",
3441
3855
  "error_code",
3856
+ "error_kind",
3442
3857
  "failure_code",
3443
- "refusal_code",
3444
- "capxul_error_code"
3858
+ "refusal_code"
3445
3859
  ]);
3446
3860
  const MINIMAL_VERSION_KEYS = /* @__PURE__ */ new Set([
3447
3861
  "sdk_version",
3448
3862
  "cli_version",
3449
3863
  "release"
3450
3864
  ]);
3865
+ /** The failure row's identifiers that the `none` text policy lets through (ADR-0039 D6). */
3866
+ const MINIMAL_FAILURE_IDENTIFIERS = /* @__PURE__ */ new Set([
3867
+ "provider_operation",
3868
+ "provider_code",
3869
+ "field"
3870
+ ]);
3871
+ /** What raised a failure: its layer (`convex`, `sdk`), and a CLI defect's error name. */
3872
+ const MINIMAL_FAILURE_ORIGINS = /* @__PURE__ */ new Set(["failure_layer", "failure_error_name"]);
3451
3873
  function minimalStringValue(key, text) {
3452
3874
  if (key === "operation") return isCapxulOperation(text);
3453
3875
  if (key === "organization_id") return /^org_[0-9A-Za-z]+$/u.test(text);
3454
3876
  if (MINIMAL_CODE_KEYS.has(key)) return MINIMAL_ERROR_CODES.has(text);
3455
3877
  if (MINIMAL_VERSION_KEYS.has(key)) return /^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(text);
3878
+ if (MINIMAL_FAILURE_IDENTIFIERS.has(key)) return /^[A-Za-z0-9_][A-Za-z0-9_.:/[\]-]{0,127}$/u.test(text) && redactSecrets(text) === text;
3879
+ if (MINIMAL_FAILURE_ORIGINS.has(key)) return /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/u.test(text) && redactSecrets(text) === text;
3880
+ if (key === "request_id") return /^[0-9a-f]{16}$/u.test(text);
3456
3881
  if (key === "trace_id") return traceId(text);
3457
3882
  if (key === "span_id" || key === "parent_span_id") return spanId(text);
3458
3883
  const values = MINIMAL_ENUM_VALUES.get(key);
3459
3884
  if (values !== void 0) return values.includes(text);
3460
- return MINIMAL_CORRELATIONS.has(key) && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(text) && !/^0x[0-9a-f]+$/iu.test(text) && redactSecrets(text) === text;
3885
+ return MINIMAL_CORRELATIONS.has(key) && minimalCorrelationId(text);
3461
3886
  }
3462
3887
  /** A named privacy projection, separate from the default credential-only redaction policy. */
3463
3888
  function minimalAttributes(attributes, metric = false) {
@@ -3816,8 +4241,8 @@ function normalizeHttpUrl(field, raw) {
3816
4241
  let parsed;
3817
4242
  try {
3818
4243
  parsed = new URL(raw);
3819
- } catch {
3820
- throw Errors.invalidInput(field, "must be an http or https URL");
4244
+ } catch (cause) {
4245
+ throw Errors.invalidInput(field, "must be an http or https URL", cause);
3821
4246
  }
3822
4247
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw Errors.invalidInput(field, "must be an http or https URL");
3823
4248
  return parsed.toString().replace(/\/$/, "");
@@ -3865,8 +4290,9 @@ function productionConvexCallLayer(input, refreshConvexAuthRef) {
3865
4290
  forceRefresh: forceRefreshToken,
3866
4291
  ...input.signal === void 0 ? {} : { signal: input.signal }
3867
4292
  })));
3868
- if (Result.isFailure(tokenResult)) return null;
3869
- return String(tokenResult.success.token);
4293
+ if (Result.isSuccess(tokenResult)) return String(tokenResult.success.token);
4294
+ const failure = toPublicError(tokenResult.failure);
4295
+ return failure.code === "NOT_AUTHENTICATED" ? null : failure;
3870
4296
  }
3871
4297
  }).pipe(Layer.flatMap((context) => {
3872
4298
  const convexCall = Context.get(context, ConvexCallPortTag);
@@ -3987,10 +4413,10 @@ function validateCreateCapxulClientInput(input) {
3987
4413
  const runtime = input.runtime ?? detectRuntime();
3988
4414
  if (input.devToolsUrl !== void 0) try {
3989
4415
  validateDevToolsUrl(input.devToolsUrl);
3990
- } catch {
4416
+ } catch (cause) {
3991
4417
  return {
3992
4418
  ok: false,
3993
- error: Errors.invalidInput("devToolsUrl", "must be a local ws or wss URL without credentials")
4419
+ error: Errors.invalidInput("devToolsUrl", "must be a local ws or wss URL without credentials", cause)
3994
4420
  };
3995
4421
  }
3996
4422
  if (input.signer !== void 0 && input.openfortSignerFactory !== void 0) return {
@@ -4094,7 +4520,7 @@ async function createProductionAdapters(input) {
4094
4520
  props: {
4095
4521
  ...bootstrapTelemetryEnvelope(input, resolvedInput.value),
4096
4522
  reason: bootstrapResult.error.code,
4097
- ...failureDetail(bootstrapResult.error),
4523
+ ...failureDetail$1(bootstrapResult.error),
4098
4524
  ...failureEvidenceProps(bootstrapResult.error)
4099
4525
  }
4100
4526
  });
@@ -4216,11 +4642,15 @@ function makeFinancialOpsMethods(deps) {
4216
4642
  }
4217
4643
  //#endregion
4218
4644
  //#region src/media/images.ts
4219
- /** The upload POST failed: the storage provider, not the row, refused. */
4220
- function uploadFailed(cause) {
4645
+ const UPLOAD = {
4646
+ provider: "convex-storage",
4647
+ operation: "uploadImage"
4648
+ };
4649
+ /** The upload POST failed: the storage provider, not the row, gave no usable answer. */
4650
+ function uploadFailed(error) {
4221
4651
  return {
4222
4652
  ok: false,
4223
- error: Errors.providerError("convex-storage", "uploadImage", cause)
4653
+ error
4224
4654
  };
4225
4655
  }
4226
4656
  /** The storage id the upload response names, if it names one. */
@@ -4239,23 +4669,51 @@ function makeMediaMethods(deps) {
4239
4669
  async uploadImage(blob, options) {
4240
4670
  const url = await call(images.generateUploadUrl, {}, options);
4241
4671
  if (!url.ok) return url;
4672
+ let response;
4242
4673
  try {
4243
- const response = await fetchImpl(url.value.uploadUrl, {
4674
+ response = await fetchImpl(url.value.uploadUrl, {
4244
4675
  method: "POST",
4245
4676
  headers: { "Content-Type": blob.type },
4246
4677
  body: blob,
4247
4678
  ...options?.signal === void 0 ? {} : { signal: options.signal }
4248
4679
  });
4249
- if (!response.ok) return uploadFailed(`upload failed with status ${response.status}`);
4250
- const storageId = storageIdOf(await response.json());
4251
- if (storageId === void 0) return uploadFailed("upload response carried no storageId");
4252
- return {
4253
- ok: true,
4254
- value: { storageId }
4680
+ } catch (cause) {
4681
+ if (options?.signal?.aborted === true) return {
4682
+ ok: false,
4683
+ error: Errors.cancelled({ operation: images.generateUploadUrl.name }, cause)
4255
4684
  };
4685
+ return uploadFailed(Errors.providerUnavailable({
4686
+ ...UPLOAD,
4687
+ mode: "upstream-down",
4688
+ cause
4689
+ }));
4690
+ }
4691
+ if (!response.ok) return uploadFailed(providerStatusFailure({
4692
+ ...UPLOAD,
4693
+ httpStatus: response.status
4694
+ }));
4695
+ let body;
4696
+ try {
4697
+ body = await response.json();
4256
4698
  } catch (cause) {
4257
- return uploadFailed(cause);
4699
+ return uploadFailed(Errors.providerResponseInvalid({
4700
+ ...UPLOAD,
4701
+ field: "body",
4702
+ problem: "invalid",
4703
+ cause
4704
+ }));
4258
4705
  }
4706
+ const storageId = storageIdOf(body);
4707
+ if (storageId === void 0) return uploadFailed(Errors.providerResponseInvalid({
4708
+ ...UPLOAD,
4709
+ field: "storageId",
4710
+ problem: "missing",
4711
+ reason: "upload response carried no storageId"
4712
+ }));
4713
+ return {
4714
+ ok: true,
4715
+ value: { storageId }
4716
+ };
4259
4717
  },
4260
4718
  setProfileImage: (input, options) => call(images.setProfileImage, { storageId: input.storageId }, options),
4261
4719
  setOrgLogo: (input, options) => call(images.setOrgLogo, {
@@ -4507,10 +4965,14 @@ function makeIdentityRuntime(actor, identityPort, runPromise) {
4507
4965
  reason: "INVALID_INPUT"
4508
4966
  };
4509
4967
  const session = actor.authSession();
4510
- if (session === null) return {
4511
- ok: false,
4512
- reason: "NOT_AUTHENTICATED"
4513
- };
4968
+ if (session === null) {
4969
+ const error = noSessionError(actor);
4970
+ return {
4971
+ ok: false,
4972
+ reason: error.code,
4973
+ error
4974
+ };
4975
+ }
4514
4976
  const result = await runPortEffect(identityPort.completeOnboarding({
4515
4977
  authUserId: session.authUserId,
4516
4978
  email: session.email,
@@ -4842,7 +5304,7 @@ function assembleCapxulClient(input) {
4842
5304
  }),
4843
5305
  close: () => closeClientResources(stopActor, effectRunner.runPromise)
4844
5306
  };
4845
- return observeSdkClient(client, observation.failureObservation, observation.hostObservationSnapshot);
5307
+ return observeSdkClient(client, observation.failureObservation, observation.hostObservationSnapshot, () => actor.authSession()?.authUserId);
4846
5308
  }
4847
5309
  //#endregion
4848
5310
  //#region src/observability/diagnostics/effect-log.ts
@@ -4912,6 +5374,23 @@ function resolveOpenfortProductionSigner(input) {
4912
5374
  }
4913
5375
  //#endregion
4914
5376
  //#region src/platform/production-client.ts
5377
+ /**
5378
+ * The signer's session reset, as the callback its two later callers run: the
5379
+ * sign-out wrapper (`resetSignerSessionOnSignOut`) and `close`. A failed reset
5380
+ * throws the typed signer failure, and each caller turns the throw into its
5381
+ * own failure. It lives outside the factory because the factory returns a
5382
+ * `CapxulResult` and must not throw. A signer with no session gives none.
5383
+ */
5384
+ function signerSessionReset(signer) {
5385
+ if (signer === void 0 || !("resetSession" in signer)) return void 0;
5386
+ return () => {
5387
+ try {
5388
+ signer.resetSession();
5389
+ } catch (cause) {
5390
+ throw signerFailure(signer.source, "resetSession", cause);
5391
+ }
5392
+ };
5393
+ }
4915
5394
  /** Shared production composition. The normal factory passes no controls. */
4916
5395
  async function createCapxulClientWithSignerControls(input, controls) {
4917
5396
  const invocationObservability = bindHostObservabilityInvocation(input.observability);
@@ -4948,16 +5427,8 @@ async function createCapxulClientWithSignerControls(input, controls) {
4948
5427
  await adapters.value.close().catch(() => void 0);
4949
5428
  return observeFailedResult(factoryChainFence, observation, "createCapxulClient");
4950
5429
  }
4951
- if (signer !== void 0 && "resetSession" in signer) {
4952
- const { resetSession, source } = signer;
4953
- resetSignerSession.current = () => {
4954
- try {
4955
- resetSession();
4956
- } catch (cause) {
4957
- throw signerFailure(source, "resetSession", cause);
4958
- }
4959
- };
4960
- }
5430
+ const resetSession = signerSessionReset(signer);
5431
+ if (resetSession !== void 0) resetSignerSession.current = resetSession;
4961
5432
  const effectRunner = {
4962
5433
  runSync: Effect.runSyncWith(adapters.value.context),
4963
5434
  runPromise: Effect.runPromiseWith(adapters.value.context)
@@ -4985,11 +5456,7 @@ async function createCapxulClientWithSignerControls(input, controls) {
4985
5456
  }), adapters.value.close, effectRunner.runPromise);
4986
5457
  const close = idempotentClose(async () => {
4987
5458
  try {
4988
- if (signer !== void 0 && "resetSession" in signer) try {
4989
- signer.resetSession();
4990
- } catch (cause) {
4991
- throw signerFailure(signer.source, "resetSession", cause);
4992
- }
5459
+ resetSession?.();
4993
5460
  } finally {
4994
5461
  await ownedClient._internal.close?.();
4995
5462
  }