@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.
- package/README.md +8 -0
- package/dist/{access-DpPt-SvA.mjs → access-aMjL10vw.mjs} +7714 -6535
- package/dist/{index-DOR7Fkn7.d.mts → index-DzfcTT6t.d.mts} +453 -159
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +41 -37
- package/dist/node/index.d.mts +1 -1
- package/dist/node/index.mjs +1 -1
- package/dist/{organizations-BmX52Uev.mjs → organizations-DPpyjjx2.mjs} +123 -132
- package/dist/{permission-approvals-Bw9VaKay.mjs → permission-approvals-B6Oqszeb.mjs} +4 -4
- package/dist/{production-DdjhwgRC.mjs → production-PGbIhEG0.mjs} +731 -264
- package/dist/testing/index.d.mts +1 -1
- package/dist/testing/index.mjs +21 -8
- package/package.json +5 -5
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { c as OrgPortTag,
|
|
2
|
-
import { $
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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))
|
|
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
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
201
|
-
*
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
+
};
|
|
452
|
+
let read;
|
|
453
|
+
try {
|
|
454
|
+
read = await requestAccessToken();
|
|
268
455
|
} catch (cause) {
|
|
269
|
-
|
|
270
|
-
|
|
456
|
+
const error = openfortProviderError("token", cause);
|
|
457
|
+
trace({
|
|
271
458
|
tokenPresent: false,
|
|
272
|
-
|
|
459
|
+
...failureDetail(error)
|
|
273
460
|
});
|
|
274
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
361
|
-
*
|
|
362
|
-
*
|
|
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"
|
|
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.
|
|
444
|
-
if (accessToken === null) throw
|
|
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
|
-
|
|
673
|
+
...openfortFailureDetail(error)
|
|
456
674
|
});
|
|
457
|
-
throw
|
|
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
|
-
|
|
691
|
+
...openfortFailureDetail(error)
|
|
473
692
|
});
|
|
474
|
-
throw
|
|
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
|
-
|
|
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
|
-
|
|
720
|
+
...openfortFailureDetail(error)
|
|
501
721
|
});
|
|
502
|
-
throw
|
|
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
|
|
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
|
|
753
|
+
const signerNotReadyCode = openfortSignerNotReadyCode(cause);
|
|
532
754
|
const normalized = signerFailure("openfort-embedded", "signUserOpHash", cause);
|
|
533
|
-
if (normalized.code === "SIGNER_REJECTED" ||
|
|
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.
|
|
559
|
-
|
|
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:
|
|
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
|
-
|
|
937
|
+
...openfortFailureDetail(error)
|
|
694
938
|
});
|
|
695
|
-
throw
|
|
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.
|
|
974
|
-
* fail decode
|
|
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.
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 === "
|
|
1170
|
-
return bootstrapErrorFromCapxul("malformedBody", Errors.
|
|
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",
|
|
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
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
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-
|
|
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
|
-
|
|
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: (
|
|
2512
|
-
|
|
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: (
|
|
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: (
|
|
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,
|
|
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
|
|
2721
|
-
*
|
|
2722
|
-
* `
|
|
2723
|
-
* `
|
|
2724
|
-
*
|
|
2725
|
-
*
|
|
2726
|
-
*
|
|
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 =
|
|
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
|
-
/**
|
|
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 >=
|
|
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", [
|
|
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) &&
|
|
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.
|
|
3869
|
-
|
|
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
|
-
|
|
4220
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
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(
|
|
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)
|
|
4511
|
-
|
|
4512
|
-
|
|
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
|
-
|
|
4952
|
-
|
|
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
|
-
|
|
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
|
}
|