@capxul/sdk 4.20.0-beta.3 → 4.20.0-beta.30
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 +252 -33
- package/dist/access-cQALBWW3.mjs +23227 -0
- package/dist/index-DidkO1aY.d.mts +34071 -0
- package/dist/index.d.mts +2 -653
- package/dist/index.mjs +84 -235
- package/dist/node/index.d.mts +9 -9
- package/dist/node/index.mjs +29 -38
- package/dist/organizations-4-jlv-yi.mjs +12546 -0
- package/dist/permission-approvals-OBgXonE4.mjs +138 -0
- package/dist/production-BjxVSadV.mjs +4882 -0
- package/dist/testing/index.d.mts +32 -15
- package/dist/testing/index.mjs +296 -146
- package/package.json +49 -16
- package/dist/OAuthBearerAuthClient-C-ip-z8M.d.mts +0 -628
- package/dist/OAuthBearerAuthClient-DWkofxdZ.mjs +0 -2488
- package/dist/production-CJwxgI8u.d.mts +0 -3141
- package/dist/production-M4dWz5g4.mjs +0 -17131
|
@@ -0,0 +1,4882 @@
|
|
|
1
|
+
import { c as deserializeCapxulError, n as makeOrgMethods, o as ConvexOrganizationAdapter, s as OrgPortTag, t as ConvexOrganizationSetupAdapter } from "./organizations-4-jlv-yi.mjs";
|
|
2
|
+
import { $r as ActorFailure, $t as version, A as registerProductTelemetryInvocation, Ai as toEpochMs, An as details, Bi as isFailureMode, Br as runIdentityFacade, Bt as failureEvidenceProps, C as AccountReadPortTag, Ci as detectAuthCacheAdapter, Cn as CLI_INVOCATION_KINDS, D as convexCallErrorFromCapxul, Dn as isCapxulOperation, E as ConvexCallPortTag, En as CAPXUL_OPERATIONS, Gt as observationContextProps, H as makePaymentsMethods, Hi as readField, Hr as sessionFromActor, Ht as postHogFailureObservation, Ii as Errors, It as callOperation, J as makeTargetsMethods, Jn as ORGANIZATION_SETUP_REASONS, K as makeCheckoutMethods, Kt as withCapturedAttemptContext, L as makeAccountMethods, Li as CapxulError, Lr as makeAuthMethods, Lt as asCapxulError, M as makeApprovalsMethods, Mi as toAuthUserId, Mr as toAddress, Ni as toEmail, Nt as BASE_SEPOLIA_CHAIN_ID, On as normalizeCapxulOperation, Ri as isCapxulError, Rr as askIdentity, Rt as toPublicError, Si as AuthClientPortTag, Sn as CLI_COMMAND_NAMES, T as TelemetryPortTag, Tn as stampTelemetryEnvelope, Tr as ChainIdSchema, Ut as observeFailedResult, V as makeOfframpMethods, Vi as isRecord, Vr as runPortEffect, W as makeDestinationsMethods, Wi as AuthCachePortTag, Wn as setups, Wr as runCapxulEffect, Wt as observeSdkClient, Xr as formatTraceparent, Yn as ORGANIZATION_SETUP_STATUSES, Yr as toCapxulResult, Zn as decodeChainCause, Zr as CAPXUL_ERROR_CODES, _i as toAllowedOrigin, ai as ClockPortTag, an as isSignerNotReadySignal, bn as redactTelemetryProps, br as EpochMsSchema, c as makePaymentDocumentsMethods, ci as BetterAuthBrowserLayer, cn as signerFailure, cr as AppIdSchema, d as SmartAccountPortTag, di as copyInvocationObservation, dn as makeClientRequestMethods, ei as carryInvocationParentSpan, fi as readInvocationObservation, fn as toCountryCode, fr as isBytes32, g as ConvexAccountLayer, gi as isCredentialField, gn as toHandle, h as makeAccountsMethods, hi as redactUrlSecrets, in as isEcdsaSignatureHex, jn as onboardings, jr as isEvmAddress, k as bindProductTelemetryInvocation, kn as OPERATIONS, kt as normalizeSafeSaltEmail, l as makeSmartAccountMethods, li as observationRequestHeaders, lr as images, m as PonderIndexerAdapter, mi as redactSecrets, mn as toTesterKind, n as bootIdentityFlow, nn as causeChain, on as openfortSignerNotReadyCode, p as makeHoldingsMethods, pi as sanitizeObservationContext, pn as toKycTier, r as makeMeMethods, ri as SystemClockLayer, s as makeActivityMethods, si as BetterAuthNodeLayer, sn as recoverRawDigestSigner, sr as diagnostics, t as executeIdentityProductObservation, tn as UNOBSERVABLE_SIGNER_STATUS, u as ConvexSmartAccountLayer, ui as attachInvocationObservation, un as makeCurrentUserMethods, vn as validateHandle, wi as globalLocalStorage, xi as AuthClientError, xn as CLI_CAPABILITIES, yi as toPublishableKey, yr as DurationMsSchema, z as logDroppedEvent, zi as FAILURE_MODES, zr as publicIdentityFailure, zt as failureDetail } from "./access-cQALBWW3.mjs";
|
|
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
|
+
import { getFunctionName } from "convex/server";
|
|
5
|
+
import { keccak256, stringToHex } from "viem";
|
|
6
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
7
|
+
import { AccountTypeEnum, ChainTypeEnum, EmbeddedState, Openfort, RecoveryMethod, ThirdPartyOAuthProvider } from "@openfort/openfort-js";
|
|
8
|
+
import { ConvexClient, ConvexHttpClient } from "convex/browser";
|
|
9
|
+
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
|
|
10
|
+
import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/observability";
|
|
11
|
+
//#region ../observability/src/browser/network.ts
|
|
12
|
+
/** In unstructured bodies, preserve text around explicitly named credentials. */
|
|
13
|
+
const NETWORK_FIELD = /((?:"?)([A-Za-z_][A-Za-z0-9_.-]*)(?:"?)\s*[:=]\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s,;&}]+)/gu;
|
|
14
|
+
function maskNetworkText(text, context) {
|
|
15
|
+
return redactSecrets(text).replace(NETWORK_FIELD, (whole, prefix, name, value) => {
|
|
16
|
+
if (!isCredentialField(name, context)) return whole;
|
|
17
|
+
const quote = value.startsWith("\"") ? "\"" : value.startsWith("'") ? "'" : "";
|
|
18
|
+
return `${prefix}${quote}[REDACTED]${quote}`;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function maskNetworkHeaders(headers) {
|
|
22
|
+
if (headers === void 0) return void 0;
|
|
23
|
+
const masked = {};
|
|
24
|
+
for (const [name, value] of Object.entries(headers)) masked[name] = isCredentialField(name, "header") ? "[REDACTED]" : /^(?:location|content-location|referer)$/iu.test(name) ? redactUrlSecrets(value) : maskNetworkText(value);
|
|
25
|
+
return masked;
|
|
26
|
+
}
|
|
27
|
+
function maskJsonValue(value, depth = 0, context) {
|
|
28
|
+
if (typeof value === "function" || typeof value === "symbol") return "[UNREADABLE]";
|
|
29
|
+
if (typeof value === "string") return maskNetworkText(value, context);
|
|
30
|
+
if (typeof value !== "object" || value === null) return value;
|
|
31
|
+
if (depth >= 16) return "[REDACTED]";
|
|
32
|
+
if (Array.isArray(value)) return value.map((item) => maskJsonValue(item, depth + 1, context));
|
|
33
|
+
const masked = Object.create(null);
|
|
34
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
35
|
+
if (!descriptor.enumerable) continue;
|
|
36
|
+
masked[key] = isCredentialField(key, context) ? "[REDACTED]" : !("value" in descriptor) ? "[UNREADABLE]" : typeof descriptor.value === "string" && isPostHogUrlProperty(key) ? redactUrlSecrets(descriptor.value) : maskJsonValue(descriptor.value, depth + 1, context);
|
|
37
|
+
}
|
|
38
|
+
return masked;
|
|
39
|
+
}
|
|
40
|
+
function maskNetworkBody(body, context) {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof body !== "string") return maskJsonValue(body, 0, context);
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(body);
|
|
46
|
+
} catch {
|
|
47
|
+
if (/^[^=&\s]+=[\s\S]*$/u.test(body)) return new URLSearchParams(Array.from(new URLSearchParams(body), ([name, value]) => [name, isCredentialField(name, context) ? "[REDACTED]" : maskNetworkText(value)])).toString();
|
|
48
|
+
return maskNetworkText(body, context);
|
|
49
|
+
}
|
|
50
|
+
return JSON.stringify(maskJsonValue(parsed, 0, context));
|
|
51
|
+
} catch {
|
|
52
|
+
return "[UNREADABLE]";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Keep request evidence while masking credential values at the browser boundary. */
|
|
56
|
+
function maskCapturedNetworkRequest(request) {
|
|
57
|
+
const requestUrl = request.name ?? request.url ?? "";
|
|
58
|
+
let context;
|
|
59
|
+
try {
|
|
60
|
+
const path = new URL(requestUrl, "https://capxul.invalid").pathname;
|
|
61
|
+
if (/\/(?:auth|oauth|sign-in|signin|login|verify-otp|verify-email)(?:\/|$)/iu.test(path)) context = "authentication";
|
|
62
|
+
} catch {}
|
|
63
|
+
return {
|
|
64
|
+
...request,
|
|
65
|
+
...request.name === void 0 ? {} : { name: redactUrlSecrets(request.name) },
|
|
66
|
+
...request.url === void 0 ? {} : { url: redactUrlSecrets(request.url) },
|
|
67
|
+
requestHeaders: maskNetworkHeaders(request.requestHeaders),
|
|
68
|
+
responseHeaders: maskNetworkHeaders(request.responseHeaders),
|
|
69
|
+
requestBody: maskNetworkBody(request.requestBody, context),
|
|
70
|
+
responseBody: maskNetworkBody(request.responseBody)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function isPostHogUrlProperty(key) {
|
|
74
|
+
const normalized = key.toLowerCase();
|
|
75
|
+
return normalized.includes("url") || normalized.includes("referrer") || normalized.includes("path") || normalized.includes("filename");
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/signing/embedded-wallet.ts
|
|
79
|
+
function openfortEmbeddedWalletPort(input) {
|
|
80
|
+
return {
|
|
81
|
+
async getAddress() {
|
|
82
|
+
if (input.ensureReady !== void 0) await input.ensureReady();
|
|
83
|
+
return (await input.embeddedWallet.get()).address;
|
|
84
|
+
},
|
|
85
|
+
async signRawDigest(hash) {
|
|
86
|
+
if (input.ensureReady !== void 0) await input.ensureReady();
|
|
87
|
+
const signature = await input.embeddedWallet.signMessage(hash, {
|
|
88
|
+
hashMessage: false,
|
|
89
|
+
arrayifyMessage: false
|
|
90
|
+
});
|
|
91
|
+
if (!isEcdsaSignatureHex(signature)) throw new Error("embeddedSigner: embedded wallet returned invalid signature format");
|
|
92
|
+
return signature;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/signing/openfort-embedded.ts
|
|
98
|
+
/** Browser helper: wrap an initialized Openfort `embeddedWallet` API. */
|
|
99
|
+
function openfortEmbeddedSignerFromWallet(input) {
|
|
100
|
+
return embeddedSigner({ wallet: openfortEmbeddedWalletPort({
|
|
101
|
+
embeddedWallet: input.embeddedWallet,
|
|
102
|
+
...input.ensureWalletReady === void 0 ? {} : { ensureReady: input.ensureWalletReady }
|
|
103
|
+
}) });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The embedded-wallet (passkey / Openfort) member of the named `CapxulSigner`
|
|
107
|
+
* constructor trio `localPrivateKeySigner` / `injectedWalletSigner` /
|
|
108
|
+
* `embeddedSigner` (backend-orchestrated-deploy.md). Takes the
|
|
109
|
+
* provider-agnostic `OpenfortEmbeddedWalletPort` (getAddress + signRawDigest);
|
|
110
|
+
* the Openfort-API convenience wrapper is `openfortEmbeddedSignerFromWallet`.
|
|
111
|
+
*/
|
|
112
|
+
function embeddedSigner(input) {
|
|
113
|
+
let cachedAddress = null;
|
|
114
|
+
let addressInFlight = null;
|
|
115
|
+
let cacheEpoch = 0;
|
|
116
|
+
const resetAddressCache = () => {
|
|
117
|
+
cacheEpoch += 1;
|
|
118
|
+
cachedAddress = null;
|
|
119
|
+
addressInFlight = null;
|
|
120
|
+
};
|
|
121
|
+
const resolveAddress = async () => {
|
|
122
|
+
if (cachedAddress !== null) return cachedAddress;
|
|
123
|
+
if (addressInFlight !== null) return addressInFlight;
|
|
124
|
+
const epoch = cacheEpoch;
|
|
125
|
+
addressInFlight = (async () => {
|
|
126
|
+
try {
|
|
127
|
+
const raw = await input.wallet.getAddress();
|
|
128
|
+
if (!isEvmAddress(raw)) throw new Error("embeddedSigner: embedded wallet returned invalid address format");
|
|
129
|
+
const address = toAddress(raw);
|
|
130
|
+
if (epoch === cacheEpoch) cachedAddress = address;
|
|
131
|
+
return address;
|
|
132
|
+
} finally {
|
|
133
|
+
if (epoch === cacheEpoch) addressInFlight = null;
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
return addressInFlight;
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
source: "openfort-embedded",
|
|
140
|
+
getAddress: resolveAddress,
|
|
141
|
+
resetAddressCache,
|
|
142
|
+
async signUserOpHash(hash) {
|
|
143
|
+
if (!isBytes32(hash)) throw new Error("embeddedSigner: SafeOp digest must be a 0x-prefixed 32-byte hex");
|
|
144
|
+
const address = await resolveAddress();
|
|
145
|
+
let signature;
|
|
146
|
+
try {
|
|
147
|
+
signature = await input.wallet.signRawDigest(hash);
|
|
148
|
+
} catch (cause) {
|
|
149
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
150
|
+
throw new Error(`embeddedSigner: raw digest signing failed; ensure the embedded wallet is configured (${detail})`, { cause });
|
|
151
|
+
}
|
|
152
|
+
if (!isEcdsaSignatureHex(signature)) throw new Error("embeddedSigner: embedded wallet returned invalid signature format");
|
|
153
|
+
if ((await recoverRawDigestSigner("embeddedSigner", {
|
|
154
|
+
hash,
|
|
155
|
+
signature
|
|
156
|
+
})).toLowerCase() !== address.toLowerCase()) throw new Error("embeddedSigner: signature did not recover the embedded wallet address for the raw SafeOp digest");
|
|
157
|
+
return signature;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
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
|
+
}
|
|
171
|
+
function isUnauthorizedCause(cause) {
|
|
172
|
+
for (const link of causeChain(cause)) if (httpStatusOf(link) === 401) return true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function openfortProviderError(operation, cause) {
|
|
176
|
+
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 });
|
|
179
|
+
}
|
|
180
|
+
function openfortFailureMode(error) {
|
|
181
|
+
return error.mode ?? "unknown";
|
|
182
|
+
}
|
|
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 });
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* True when the browser cannot perform Web Crypto — sandboxed iframes, headless
|
|
189
|
+
* agent browsers, or non-HTTPS origins. OpenFort's embedded-wallet `configure`
|
|
190
|
+
* silently produces no address in this state, so we detect it up front and name
|
|
191
|
+
* it `no-secure-context` instead of letting it decay into `unknown`.
|
|
192
|
+
*/
|
|
193
|
+
function isInsecureBrowserContext() {
|
|
194
|
+
return readField(globalThis, "isSecureContext") === false || globalThis.crypto?.subtle === void 0;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/signing/openfort-credentials.ts
|
|
198
|
+
const sinceCycleStart = (clock) => clock.cycleStartedAt === null ? void 0 : Date.now() - clock.cycleStartedAt;
|
|
199
|
+
/**
|
|
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.
|
|
202
|
+
*/
|
|
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)) })))
|
|
206
|
+
});
|
|
207
|
+
/**
|
|
208
|
+
* The two credentials Openfort needs from the signed-in session: the access
|
|
209
|
+
* token it verifies, and an encryption session for automatic recovery. A host
|
|
210
|
+
* that owns login passes `auth`; otherwise both come from the auth server.
|
|
211
|
+
*/
|
|
212
|
+
function makeOpenfortCredentials(input) {
|
|
213
|
+
const { authProvider, authBaseUrl, auth, diagnostic, clock } = input;
|
|
214
|
+
function accessTokenUrl() {
|
|
215
|
+
return authProvider === "oidc" ? `${authBaseUrl}/convex/token` : `${authBaseUrl}/get-session`;
|
|
216
|
+
}
|
|
217
|
+
function encryptionSessionUrl() {
|
|
218
|
+
return `${authBaseUrl}/encryption-session`;
|
|
219
|
+
}
|
|
220
|
+
async function fetchBetterAuthAccessToken() {
|
|
221
|
+
clock.tokenCalls += 1;
|
|
222
|
+
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
|
+
}
|
|
259
|
+
diagnostic?.trace("openfort.token", {
|
|
260
|
+
ok: true,
|
|
261
|
+
tokenPresent: true,
|
|
262
|
+
provider: authProvider,
|
|
263
|
+
call: clock.tokenCalls,
|
|
264
|
+
duration_ms: Date.now() - tokenStartedAt,
|
|
265
|
+
elapsed_ms: sinceCycleStart(clock)
|
|
266
|
+
});
|
|
267
|
+
return token;
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
diagnostic?.trace("openfort.token", {
|
|
270
|
+
ok: false,
|
|
271
|
+
tokenPresent: false,
|
|
272
|
+
failure_mode: "unknown"
|
|
273
|
+
});
|
|
274
|
+
throw openfortProviderError("token", cause);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function createEncryptionSession(accessToken) {
|
|
278
|
+
if (auth !== void 0) {
|
|
279
|
+
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" });
|
|
281
|
+
return { sessionId };
|
|
282
|
+
}
|
|
283
|
+
const response = await fetch(encryptionSessionUrl(), {
|
|
284
|
+
method: "POST",
|
|
285
|
+
credentials: "include",
|
|
286
|
+
headers: {
|
|
287
|
+
Authorization: `Bearer ${accessToken}`,
|
|
288
|
+
"Content-Type": "application/json"
|
|
289
|
+
},
|
|
290
|
+
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"
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
sessionId,
|
|
303
|
+
httpStatus: response.status
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
fetchBetterAuthAccessToken,
|
|
308
|
+
createEncryptionSession
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/signing/openfort-storage.ts
|
|
313
|
+
/** Openfort SDK storage keys (`@openfort/openfort-js` StorageKeys). */
|
|
314
|
+
const OPENFORT_BROWSER_STORAGE_KEYS = [
|
|
315
|
+
"openfort.authentication",
|
|
316
|
+
"openfort.account",
|
|
317
|
+
"openfort.session",
|
|
318
|
+
"openfort.configuration"
|
|
319
|
+
];
|
|
320
|
+
/**
|
|
321
|
+
* Matches `@openfort/openfort-js` ScopedStorage.createScope — chars 8–15 of the
|
|
322
|
+
* publishable key, prefixed onto each StorageKeys entry in localStorage.
|
|
323
|
+
*/
|
|
324
|
+
function openfortBrowserStorageScope(publishableKey) {
|
|
325
|
+
const trimmed = publishableKey.trim();
|
|
326
|
+
if (trimmed.length < 16) return;
|
|
327
|
+
return trimmed.substring(8, 16);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Drop cached Openfort auth/account state so third-party login re-runs for the
|
|
331
|
+
* current Better Auth session. The SDK skips `authenticateThirdParty` when a
|
|
332
|
+
* stale `userId` is already in storage, which yields 401 on `v2/accounts`.
|
|
333
|
+
*/
|
|
334
|
+
function clearStaleOpenfortBrowserStorage(publishableKey) {
|
|
335
|
+
if (typeof localStorage === "undefined") return;
|
|
336
|
+
const scope = openfortBrowserStorageScope(publishableKey);
|
|
337
|
+
if (scope === void 0) return;
|
|
338
|
+
for (const key of OPENFORT_BROWSER_STORAGE_KEYS) localStorage.removeItem(`${scope}.${key}`);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The Openfort error code that names the stale-user class (#1435).
|
|
342
|
+
* `getThirdPartyAuthToken` skips `authenticateThirdParty` while a `userId` sits
|
|
343
|
+
* in scoped storage. A purged `userId` therefore pins every later call to a 401
|
|
344
|
+
* that `extractApiError` reports as `USER_NOT_FOUND`.
|
|
345
|
+
*
|
|
346
|
+
* The set holds one member on purpose. Session-expiry codes (`SESSION_EXPIRED`,
|
|
347
|
+
* `NOT_LOGGED_IN`, `INVALID_TOKEN`, `REFRESH_TOKEN_ERROR`) are a different
|
|
348
|
+
* cause, and a bare 401 is a different cause again: `app-env-allowlist` is a
|
|
349
|
+
* 401 by definition (`packages/errors/src/errors.ts:80-81`). Healing those and
|
|
350
|
+
* tagging them `stale-openfort-cache` would delete the triage signal the tag
|
|
351
|
+
* exists to carry.
|
|
352
|
+
*/
|
|
353
|
+
const STALE_USER_ERROR_CODES = /* @__PURE__ */ new Set(["USER_NOT_FOUND"]);
|
|
354
|
+
/**
|
|
355
|
+
* The one predicate that opens the heal. It reads the Openfort error code and
|
|
356
|
+
* never the message text (ADR-0023 R4). It walks the cause chain the way
|
|
357
|
+
* `isTransportError` walks it in the Convex transport adapter.
|
|
358
|
+
*
|
|
359
|
+
* 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.
|
|
363
|
+
*/
|
|
364
|
+
function isStaleUserSignal(cause) {
|
|
365
|
+
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;
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/signing/openfort-readiness.ts
|
|
370
|
+
/**
|
|
371
|
+
* One wallet readiness cycle: initialize, read the access token, open an
|
|
372
|
+
* encryption session, configure the embedded wallet when it is not ready, and
|
|
373
|
+
* read it back, healing a stale cached Openfort user once.
|
|
374
|
+
*/
|
|
375
|
+
function makeWalletReadyCycle(input) {
|
|
376
|
+
const { openfort, publishableKey, diagnostic, credentials, clock } = input;
|
|
377
|
+
const step = (name, extra = {}) => {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
diagnostic?.trace("openfort.step", {
|
|
380
|
+
step: name,
|
|
381
|
+
duration_ms: now - clock.stepStartedAt,
|
|
382
|
+
elapsed_ms: sinceCycleStart(clock),
|
|
383
|
+
...extra
|
|
384
|
+
});
|
|
385
|
+
clock.stepStartedAt = now;
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Closes the black hole: when the browser has no Web Crypto, OpenFort's
|
|
389
|
+
* `configure` would resolve to no address and the failure would be reported
|
|
390
|
+
* as `unknown`. Detect it before any network or wallet work. Tag it
|
|
391
|
+
* `no-secure-context` on a PROVIDER_ERROR scoped to `configure`. Add a
|
|
392
|
+
* DiagnosticPort breadcrumb. The assembled Core SDK boundary reports it.
|
|
393
|
+
*/
|
|
394
|
+
function failNoSecureContext() {
|
|
395
|
+
diagnostic?.trace("openfort.configure", {
|
|
396
|
+
ok: false,
|
|
397
|
+
failure_mode: "no-secure-context"
|
|
398
|
+
});
|
|
399
|
+
throw failOpenfort("configure", "no-secure-context", /* @__PURE__ */ new Error("Web Crypto unavailable: browser is not a secure context"));
|
|
400
|
+
}
|
|
401
|
+
async function configureEmbeddedWallet(encryptionSession) {
|
|
402
|
+
await openfort.embeddedWallet.configure({
|
|
403
|
+
accountType: AccountTypeEnum.EOA,
|
|
404
|
+
chainType: ChainTypeEnum.EVM,
|
|
405
|
+
recoveryParams: {
|
|
406
|
+
recoveryMethod: RecoveryMethod.AUTOMATIC,
|
|
407
|
+
encryptionSession
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* ONE heal cycle on the stale-user rejection (#1435). Clear the scoped
|
|
413
|
+
* storage that pins the dead `userId`, run the existing configure path — it
|
|
414
|
+
* re-runs third-party auth against the live Better Auth session now that no
|
|
415
|
+
* `userId` is cached — and retry the read once. Exactly one cycle: a second
|
|
416
|
+
* rejection is terminal and carries `failure_mode: "stale-openfort-cache"`.
|
|
417
|
+
* The caller stays in `recovering` throughout; only the outcome moves it.
|
|
418
|
+
*/
|
|
419
|
+
async function healStaleOpenfortCache(encryptionSession) {
|
|
420
|
+
try {
|
|
421
|
+
clearStaleOpenfortBrowserStorage(publishableKey);
|
|
422
|
+
diagnostic?.trace("openfort.storageCleared", { beforeConfigure: false });
|
|
423
|
+
await configureEmbeddedWallet(encryptionSession);
|
|
424
|
+
await openfort.embeddedWallet.get();
|
|
425
|
+
} catch (cause) {
|
|
426
|
+
diagnostic?.trace("openfort.staleCacheHeal", {
|
|
427
|
+
ok: false,
|
|
428
|
+
failure_mode: "stale-openfort-cache"
|
|
429
|
+
});
|
|
430
|
+
throw failOpenfort("get", "stale-openfort-cache", cause);
|
|
431
|
+
}
|
|
432
|
+
diagnostic?.trace("openfort.staleCacheHeal", { ok: true });
|
|
433
|
+
}
|
|
434
|
+
return function startWalletReady() {
|
|
435
|
+
return (async () => {
|
|
436
|
+
const startedAt = Date.now();
|
|
437
|
+
clock.cycleStartedAt = startedAt;
|
|
438
|
+
clock.stepStartedAt = startedAt;
|
|
439
|
+
clock.tokenCalls = 0;
|
|
440
|
+
if (isInsecureBrowserContext()) failNoSecureContext();
|
|
441
|
+
await openfort.waitForInitialization();
|
|
442
|
+
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"));
|
|
445
|
+
step("token");
|
|
446
|
+
let encryption;
|
|
447
|
+
try {
|
|
448
|
+
encryption = await credentials.createEncryptionSession(accessToken);
|
|
449
|
+
} catch (cause) {
|
|
450
|
+
let httpStatus;
|
|
451
|
+
for (const link of causeChain(cause)) httpStatus ??= httpStatusOf(link);
|
|
452
|
+
diagnostic?.trace("openfort.encryptionSession", {
|
|
453
|
+
ok: false,
|
|
454
|
+
...httpStatus === void 0 ? {} : { httpStatus },
|
|
455
|
+
failure_mode: "unknown"
|
|
456
|
+
});
|
|
457
|
+
throw openfortProviderError("encryptionSession", cause);
|
|
458
|
+
}
|
|
459
|
+
diagnostic?.trace("openfort.encryptionSession", {
|
|
460
|
+
ok: true,
|
|
461
|
+
...encryption.httpStatus === void 0 ? {} : { httpStatus: encryption.httpStatus }
|
|
462
|
+
});
|
|
463
|
+
step("encryptionSession", encryption.httpStatus === void 0 ? {} : { httpStatus: encryption.httpStatus });
|
|
464
|
+
let embeddedState;
|
|
465
|
+
try {
|
|
466
|
+
embeddedState = await openfort.embeddedWallet.getEmbeddedState();
|
|
467
|
+
diagnostic?.trace("openfort.embeddedState", { state: embeddedState });
|
|
468
|
+
step("embeddedState", { state: embeddedState });
|
|
469
|
+
} catch (cause) {
|
|
470
|
+
diagnostic?.trace("openfort.embeddedState", {
|
|
471
|
+
ok: false,
|
|
472
|
+
failure_mode: "unknown"
|
|
473
|
+
});
|
|
474
|
+
throw openfortProviderError("embeddedState", cause);
|
|
475
|
+
}
|
|
476
|
+
if (embeddedState !== EmbeddedState.READY) {
|
|
477
|
+
clearStaleOpenfortBrowserStorage(publishableKey);
|
|
478
|
+
diagnostic?.trace("openfort.storageCleared", { beforeConfigure: true });
|
|
479
|
+
try {
|
|
480
|
+
await configureEmbeddedWallet(encryption.sessionId);
|
|
481
|
+
diagnostic?.trace("openfort.configure", { ok: true });
|
|
482
|
+
step("configure");
|
|
483
|
+
} catch (cause) {
|
|
484
|
+
const error = openfortProviderError("configure", cause);
|
|
485
|
+
diagnostic?.trace("openfort.configure", {
|
|
486
|
+
ok: false,
|
|
487
|
+
failure_mode: openfortFailureMode(error)
|
|
488
|
+
});
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
await openfort.embeddedWallet.get();
|
|
494
|
+
diagnostic?.trace("openfort.get", { ok: true });
|
|
495
|
+
step("get");
|
|
496
|
+
} catch (cause) {
|
|
497
|
+
if (!isStaleUserSignal(cause)) {
|
|
498
|
+
diagnostic?.trace("openfort.get", {
|
|
499
|
+
ok: false,
|
|
500
|
+
failure_mode: "unknown"
|
|
501
|
+
});
|
|
502
|
+
throw openfortProviderError("get", cause);
|
|
503
|
+
}
|
|
504
|
+
diagnostic?.trace("openfort.get", {
|
|
505
|
+
ok: false,
|
|
506
|
+
failure_mode: "stale-openfort-cache"
|
|
507
|
+
});
|
|
508
|
+
await healStaleOpenfortCache(encryption.sessionId);
|
|
509
|
+
step("healStaleCache");
|
|
510
|
+
}
|
|
511
|
+
step("walletReady", {
|
|
512
|
+
ok: true,
|
|
513
|
+
total_ms: Date.now() - startedAt,
|
|
514
|
+
token_calls: clock.tokenCalls
|
|
515
|
+
});
|
|
516
|
+
})();
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/signing/openfort-recovery.ts
|
|
521
|
+
/**
|
|
522
|
+
* Sign once; when Openfort reports the signer not ready, reset readiness, run
|
|
523
|
+
* 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.
|
|
525
|
+
*/
|
|
526
|
+
function recoveringSign(input) {
|
|
527
|
+
return async (hash) => {
|
|
528
|
+
try {
|
|
529
|
+
return await input.sign(hash);
|
|
530
|
+
} catch (cause) {
|
|
531
|
+
const providerErrorCode = openfortSignerNotReadyCode(cause);
|
|
532
|
+
const normalized = signerFailure("openfort-embedded", "signUserOpHash", cause);
|
|
533
|
+
if (normalized.code === "SIGNER_REJECTED" || providerErrorCode === void 0) throw normalized;
|
|
534
|
+
input.diagnostic?.trace("openfort.signerRecovery", {
|
|
535
|
+
outcome: "started",
|
|
536
|
+
attempt: 1,
|
|
537
|
+
failure_mode: "signer-not-ready"
|
|
538
|
+
});
|
|
539
|
+
input.resetReadiness();
|
|
540
|
+
try {
|
|
541
|
+
await input.ensureWalletReady();
|
|
542
|
+
const signature = await input.sign(hash);
|
|
543
|
+
input.diagnostic?.trace("openfort.signerRecovery", {
|
|
544
|
+
outcome: "succeeded",
|
|
545
|
+
attempt: 1,
|
|
546
|
+
failure_mode: "signer-not-ready"
|
|
547
|
+
});
|
|
548
|
+
return signature;
|
|
549
|
+
} catch (retryCause) {
|
|
550
|
+
const retryFailure = signerFailure("openfort-embedded", "signUserOpHash", retryCause);
|
|
551
|
+
if (retryFailure.code === "SIGNER_REJECTED") throw retryFailure;
|
|
552
|
+
if (isSignerNotReadySignal(retryCause)) input.markUnavailable();
|
|
553
|
+
input.diagnostic?.trace("openfort.signerRecovery", {
|
|
554
|
+
outcome: "failed",
|
|
555
|
+
attempt: 1,
|
|
556
|
+
failure_mode: isSignerNotReadySignal(retryCause) ? "signer-not-ready" : "unknown"
|
|
557
|
+
});
|
|
558
|
+
throw Errors.providerError("openfort", "signerRecovery", retryCause, {
|
|
559
|
+
failure_mode: "signer-not-ready",
|
|
560
|
+
details: {
|
|
561
|
+
provider_error_source: "openfort",
|
|
562
|
+
provider_error_code: providerErrorCode
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/signing/openfort-browser.ts
|
|
571
|
+
function isolatedDiagnostic(diagnostic) {
|
|
572
|
+
if (diagnostic === void 0) return void 0;
|
|
573
|
+
return { trace: (scope, detail) => {
|
|
574
|
+
try {
|
|
575
|
+
Promise.resolve(diagnostic.trace(scope, detail)).catch(() => void 0);
|
|
576
|
+
} catch {}
|
|
577
|
+
} };
|
|
578
|
+
}
|
|
579
|
+
function createOpenfortBrowserSignerFromBootstrap(bootstrap, options = {}) {
|
|
580
|
+
const diagnostic = isolatedDiagnostic(options.diagnostic);
|
|
581
|
+
const authBaseUrl = normalizeBetterAuthBaseUrl(bootstrap.authBaseUrl);
|
|
582
|
+
let currentStatus = "unknown";
|
|
583
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
584
|
+
function setStatus(next) {
|
|
585
|
+
if (currentStatus === next) return;
|
|
586
|
+
currentStatus = next;
|
|
587
|
+
for (const listener of statusListeners) try {
|
|
588
|
+
listener(next);
|
|
589
|
+
} catch {
|
|
590
|
+
statusListeners.delete(listener);
|
|
591
|
+
diagnostic?.trace("openfort.signerStatusListener", {
|
|
592
|
+
ok: false,
|
|
593
|
+
failure_mode: "unknown"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const statusStore = {
|
|
598
|
+
status: () => currentStatus,
|
|
599
|
+
subscribe: (listener) => {
|
|
600
|
+
statusListeners.add(listener);
|
|
601
|
+
return () => {
|
|
602
|
+
statusListeners.delete(listener);
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
const authProvider = bootstrap.openfortAuthProvider ?? "better-auth";
|
|
607
|
+
const clock = {
|
|
608
|
+
cycleStartedAt: null,
|
|
609
|
+
stepStartedAt: 0,
|
|
610
|
+
tokenCalls: 0
|
|
611
|
+
};
|
|
612
|
+
const credentials = makeOpenfortCredentials({
|
|
613
|
+
authProvider,
|
|
614
|
+
authBaseUrl,
|
|
615
|
+
auth: options.auth,
|
|
616
|
+
diagnostic,
|
|
617
|
+
clock
|
|
618
|
+
});
|
|
619
|
+
const openfort = new Openfort({
|
|
620
|
+
baseConfiguration: { publishableKey: bootstrap.openfortPublishableKey },
|
|
621
|
+
shieldConfiguration: { shieldPublishableKey: bootstrap.shieldPublishableKey },
|
|
622
|
+
thirdPartyAuth: {
|
|
623
|
+
provider: authProvider === "oidc" ? ThirdPartyOAuthProvider.OIDC : ThirdPartyOAuthProvider.BETTER_AUTH,
|
|
624
|
+
getAccessToken: credentials.fetchBetterAuthAccessToken
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
const startWalletReady = makeWalletReadyCycle({
|
|
628
|
+
openfort,
|
|
629
|
+
publishableKey: bootstrap.openfortPublishableKey,
|
|
630
|
+
diagnostic,
|
|
631
|
+
credentials,
|
|
632
|
+
clock
|
|
633
|
+
});
|
|
634
|
+
let walletReadyPromise = null;
|
|
635
|
+
async function ensureOpenfortWalletReady() {
|
|
636
|
+
let joined = walletReadyPromise;
|
|
637
|
+
if (joined === null) {
|
|
638
|
+
joined = startWalletReady();
|
|
639
|
+
walletReadyPromise = joined;
|
|
640
|
+
setStatus("recovering");
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
await joined;
|
|
644
|
+
if (walletReadyPromise === joined) setStatus("ready");
|
|
645
|
+
} catch (cause) {
|
|
646
|
+
if (walletReadyPromise === joined) {
|
|
647
|
+
walletReadyPromise = null;
|
|
648
|
+
setStatus("unavailable");
|
|
649
|
+
}
|
|
650
|
+
throw cause;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const signer = openfortEmbeddedSignerFromWallet({
|
|
654
|
+
embeddedWallet: openfort.embeddedWallet,
|
|
655
|
+
ensureWalletReady: ensureOpenfortWalletReady
|
|
656
|
+
});
|
|
657
|
+
const controlledSign = options.signUserOpHash;
|
|
658
|
+
const signEmbeddedHash = controlledSign === void 0 ? (hash) => signer.signUserOpHash(hash) : (hash) => controlledSign(hash, (digest) => signer.signUserOpHash(digest));
|
|
659
|
+
const resetReadiness = () => {
|
|
660
|
+
walletReadyPromise = null;
|
|
661
|
+
clearStaleOpenfortBrowserStorage(bootstrap.openfortPublishableKey);
|
|
662
|
+
signer.resetAddressCache();
|
|
663
|
+
setStatus("unknown");
|
|
664
|
+
};
|
|
665
|
+
const signUserOpHash = recoveringSign({
|
|
666
|
+
sign: signEmbeddedHash,
|
|
667
|
+
resetReadiness,
|
|
668
|
+
ensureWalletReady: ensureOpenfortWalletReady,
|
|
669
|
+
markUnavailable: () => {
|
|
670
|
+
walletReadyPromise = null;
|
|
671
|
+
signer.resetAddressCache();
|
|
672
|
+
setStatus("unavailable");
|
|
673
|
+
},
|
|
674
|
+
diagnostic
|
|
675
|
+
});
|
|
676
|
+
return {
|
|
677
|
+
...signer,
|
|
678
|
+
signUserOpHash,
|
|
679
|
+
statusStore,
|
|
680
|
+
ensureWalletReady: ensureOpenfortWalletReady,
|
|
681
|
+
getAddress: async () => {
|
|
682
|
+
const addressStartedAt = Date.now();
|
|
683
|
+
try {
|
|
684
|
+
const address = await signer.getAddress();
|
|
685
|
+
diagnostic?.trace("openfort.address", {
|
|
686
|
+
ok: true,
|
|
687
|
+
duration_ms: Date.now() - addressStartedAt
|
|
688
|
+
});
|
|
689
|
+
return address;
|
|
690
|
+
} catch (cause) {
|
|
691
|
+
diagnostic?.trace("openfort.address", {
|
|
692
|
+
ok: false,
|
|
693
|
+
failure_mode: "unknown"
|
|
694
|
+
});
|
|
695
|
+
throw openfortProviderError("getAddress", cause);
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
resetSession: resetReadiness
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/** Build the existing browser signer around host-owned authenticated callbacks. */
|
|
702
|
+
function createOpenfortBrowserSigner(input) {
|
|
703
|
+
return createOpenfortBrowserSignerFromBootstrap(input.bootstrap, {
|
|
704
|
+
auth: input.auth,
|
|
705
|
+
...input.diagnostic === void 0 ? {} : { diagnostic: input.diagnostic }
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function normalizeBetterAuthBaseUrl(raw) {
|
|
709
|
+
const trimmed = raw.replace(/\/$/, "");
|
|
710
|
+
return trimmed.endsWith("/api/auth") ? trimmed : `${trimmed}/api/auth`;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Normalize a raw email for binding id + Safe salt derivation.
|
|
714
|
+
* Delegates to {@link normalizeSafeSaltEmail} so binding resolution and
|
|
715
|
+
* `deriveCapxulSafeAddress` never drift apart.
|
|
716
|
+
*/
|
|
717
|
+
function normalizeBindingEmail(email) {
|
|
718
|
+
return normalizeSafeSaltEmail(email);
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/signing/dev-signer.ts
|
|
722
|
+
const SESSION_KEY = "capxul.session";
|
|
723
|
+
/**
|
|
724
|
+
* Chains a `local-private-key` signer may sign on. Base Sepolia only, and this
|
|
725
|
+
* list does not grow without an ADR: the keys are deterministic throwaways
|
|
726
|
+
* derived from a shared seed (see `deriveDevPrivateKey`), so anyone holding
|
|
727
|
+
* the seed holds every account. A dev key on a value-bearing chain is a
|
|
728
|
+
* custody incident, not a config mistake.
|
|
729
|
+
*/
|
|
730
|
+
const DEV_KEY_ALLOWED_CHAIN_IDS = [BASE_SEPOLIA_CHAIN_ID];
|
|
731
|
+
/**
|
|
732
|
+
* Testnet fence for the dev-key signing lane (#1149, folds #1065; ADR-0018 P9
|
|
733
|
+
* human/Openfort vs agent/dev-key split). Call it wherever a signer first
|
|
734
|
+
* meets a resolved chain; it refuses before the signer can be used.
|
|
735
|
+
*
|
|
736
|
+
* Only `local-private-key` signers are fenced — Openfort-embedded and injected
|
|
737
|
+
* wallets carry their own custody and are the sanctioned human paths.
|
|
738
|
+
*/
|
|
739
|
+
function assertDevKeySignerIsTestnetOnly(signer, chainId) {
|
|
740
|
+
if (signer?.source !== "local-private-key") return {
|
|
741
|
+
ok: true,
|
|
742
|
+
value: void 0
|
|
743
|
+
};
|
|
744
|
+
if (DEV_KEY_ALLOWED_CHAIN_IDS.includes(chainId)) return {
|
|
745
|
+
ok: true,
|
|
746
|
+
value: void 0
|
|
747
|
+
};
|
|
748
|
+
return {
|
|
749
|
+
ok: false,
|
|
750
|
+
error: Errors.invalidInput("signer", `dev-key signer is testnet-only: chain ${chainId} is not allowed (expected ${DEV_KEY_ALLOWED_CHAIN_IDS.join(", ")})`)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/** Deterministic dev private key for an email under a seed. Exported for probes. */
|
|
754
|
+
function deriveDevPrivateKey(seed, email) {
|
|
755
|
+
return keccak256(stringToHex(seed + normalizeBindingEmail(email)));
|
|
756
|
+
}
|
|
757
|
+
function readSessionEmail(storage) {
|
|
758
|
+
const resolved = storage ?? globalLocalStorage();
|
|
759
|
+
if (resolved === void 0) throw new Error("devPrivateKeySigner: no browser localStorage available and no explicit email supplied");
|
|
760
|
+
const raw = resolved.getItem(SESSION_KEY);
|
|
761
|
+
if (raw === null) throw new Error("devPrivateKeySigner: no cached session yet — sign in before the account lane uses the signer");
|
|
762
|
+
let session;
|
|
763
|
+
try {
|
|
764
|
+
session = JSON.parse(raw);
|
|
765
|
+
} catch {
|
|
766
|
+
throw new Error("devPrivateKeySigner: cached session is not valid JSON");
|
|
767
|
+
}
|
|
768
|
+
if (session === null) throw new Error("devPrivateKeySigner: cached session is not valid JSON");
|
|
769
|
+
const email = readField(session, "email");
|
|
770
|
+
if (typeof email !== "string" || email.length === 0) throw new Error("devPrivateKeySigner: cached session has no email");
|
|
771
|
+
return email;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Browser-safe dev signer. Lazy: the email (and so the key) is resolved at
|
|
775
|
+
* each `getAddress()` / `signUserOpHash()` from the cached session, so the
|
|
776
|
+
* same signer instance follows whichever user is signed in.
|
|
777
|
+
*/
|
|
778
|
+
function devPrivateKeySigner(input) {
|
|
779
|
+
if (input.seed.trim().length === 0) throw new Error("devPrivateKeySigner: seed must be non-empty");
|
|
780
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
781
|
+
const resolveAccount = () => {
|
|
782
|
+
const email = normalizeBindingEmail(input.email ?? readSessionEmail(input.storage));
|
|
783
|
+
const cached = accounts.get(email);
|
|
784
|
+
if (cached !== void 0) return cached;
|
|
785
|
+
const account = privateKeyToAccount(deriveDevPrivateKey(input.seed, email));
|
|
786
|
+
accounts.set(email, account);
|
|
787
|
+
return account;
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
source: "local-private-key",
|
|
791
|
+
async getAddress() {
|
|
792
|
+
return toAddress(resolveAccount().address);
|
|
793
|
+
},
|
|
794
|
+
async signUserOpHash(hash) {
|
|
795
|
+
return resolveAccount().sign({ hash });
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region ../protocol/src/errors/convex-decoding.ts
|
|
801
|
+
const KNOWN_CODES = new Set(CAPXUL_ERROR_CODES);
|
|
802
|
+
function isCapxulCode(value) {
|
|
803
|
+
return typeof value === "string" && KNOWN_CODES.has(value);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* A serialized failure, rebuilt whole. Nothing that crossed is dropped: a code
|
|
807
|
+
* this build does not know (a newer backend) becomes an explicit `UNKNOWN` that
|
|
808
|
+
* names the code it received, and an unrecognized mode or chain shape is kept
|
|
809
|
+
* raw in `details` instead of being guessed at or discarded.
|
|
810
|
+
*/
|
|
811
|
+
function reconstruct(serialized) {
|
|
812
|
+
if (typeof serialized.code !== "string") return null;
|
|
813
|
+
const unrecognized = {
|
|
814
|
+
...isCapxulCode(serialized.code) ? {} : { receivedCode: serialized.code },
|
|
815
|
+
...serialized.mode === void 0 || isFailureMode(serialized.mode) ? {} : { unrecognizedMode: serialized.mode }
|
|
816
|
+
};
|
|
817
|
+
const chain = serialized.chain === void 0 ? null : decodeChainCause(serialized.chain);
|
|
818
|
+
if (serialized.chain !== void 0 && chain === null) unrecognized.unrecognizedChain = serialized.chain;
|
|
819
|
+
const hasDetails = isRecord(serialized.details) || Object.keys(unrecognized).length > 0;
|
|
820
|
+
const cause = causeLinks(serialized.cause);
|
|
821
|
+
const payload = {
|
|
822
|
+
code: isCapxulCode(serialized.code) ? serialized.code : "UNKNOWN",
|
|
823
|
+
message: typeof serialized.message === "string" ? serialized.message : serialized.code,
|
|
824
|
+
...hasDetails ? { details: {
|
|
825
|
+
...isRecord(serialized.details) ? serialized.details : {},
|
|
826
|
+
...unrecognized
|
|
827
|
+
} } : {},
|
|
828
|
+
...typeof serialized.correlationId === "string" ? { correlationId: serialized.correlationId } : {},
|
|
829
|
+
...typeof serialized.layer === "string" ? { layer: serialized.layer } : {},
|
|
830
|
+
...isFailureMode(serialized.mode) ? { mode: serialized.mode } : {},
|
|
831
|
+
...chain === null ? {} : { chain },
|
|
832
|
+
...cause === void 0 ? {} : { cause }
|
|
833
|
+
};
|
|
834
|
+
return deserializeCapxulError(payload);
|
|
835
|
+
}
|
|
836
|
+
function causeLinks(value) {
|
|
837
|
+
if (!Array.isArray(value)) return void 0;
|
|
838
|
+
const links = value.filter((link) => isRecord(link) && typeof link.name === "string" && typeof link.message === "string");
|
|
839
|
+
return links.length === 0 ? void 0 : links;
|
|
840
|
+
}
|
|
841
|
+
function decodeConvexError(err) {
|
|
842
|
+
if (err === null || err === void 0) return null;
|
|
843
|
+
if (err instanceof CapxulError) return err;
|
|
844
|
+
if (typeof err !== "object") return null;
|
|
845
|
+
if (!("data" in err)) return null;
|
|
846
|
+
const data = err.data;
|
|
847
|
+
if (isRecord(data)) return reconstruct(data);
|
|
848
|
+
if (typeof data === "string") try {
|
|
849
|
+
const parsed = JSON.parse(data);
|
|
850
|
+
if (isRecord(parsed)) return reconstruct(parsed);
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/applications/bootstrap-port.ts
|
|
858
|
+
var BootstrapError = class extends Data.TaggedError("BootstrapError") {};
|
|
859
|
+
function bootstrapErrorFromCapxul(kind, error) {
|
|
860
|
+
return new BootstrapError({
|
|
861
|
+
operation: "resolve",
|
|
862
|
+
kind,
|
|
863
|
+
publicCode: error.code,
|
|
864
|
+
cause: error,
|
|
865
|
+
...kind === "notAuthenticated" || error.details === void 0 ? {} : { details: error.details }
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
var BootstrapPortTag = class extends Context.Service()("@capxul/sdk/ports/BootstrapPort") {};
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region ../protocol/src/access/value-schema.ts
|
|
871
|
+
/**
|
|
872
|
+
* `SessionToken` schema. Mirrors `toSessionToken`: non-empty string.
|
|
873
|
+
* Issuance source distinguishes SDK-handshake tokens from auth-session
|
|
874
|
+
* tokens; the brand itself is opaque.
|
|
875
|
+
*/
|
|
876
|
+
const SessionTokenSchema = Schema.String.pipe(Schema.refine((s) => s.length > 0, { message: "must be a non-empty string" }));
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region ../protocol/src/access/bootstrap-schema.ts
|
|
879
|
+
const PUBLIC_POSTHOG_PROJECT_TOKEN = /^phc_[A-Za-z0-9_-]{1,191}$/u;
|
|
880
|
+
const PostHogIngestOrigin = Schema.String.pipe(Schema.refine((value) => {
|
|
881
|
+
try {
|
|
882
|
+
const url = new URL(value);
|
|
883
|
+
return 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"));
|
|
884
|
+
} catch {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
}, { message: "must be a credential-free PostHog HTTPS ingest origin" }));
|
|
888
|
+
const EngineeringTelemetryBootstrapPolicy = Schema.Struct({
|
|
889
|
+
host: PostHogIngestOrigin,
|
|
890
|
+
projectToken: Schema.String.pipe(Schema.refine((value) => PUBLIC_POSTHOG_PROJECT_TOKEN.test(value), { message: "must be a public PostHog project token" })),
|
|
891
|
+
capxulEnv: Schema.Union([
|
|
892
|
+
Schema.Literal("development"),
|
|
893
|
+
Schema.Literal("staging"),
|
|
894
|
+
Schema.Literal("production"),
|
|
895
|
+
Schema.Literal("local")
|
|
896
|
+
])
|
|
897
|
+
});
|
|
898
|
+
/**
|
|
899
|
+
* `BootstrapEnvelope` v1.
|
|
900
|
+
*
|
|
901
|
+
* - `protocol`: discriminator that lets future protocols coexist on the
|
|
902
|
+
* same endpoint without a wire-shape conflict.
|
|
903
|
+
* - `version`: numeric version inside the protocol. Unknown versions MUST
|
|
904
|
+
* fail decode with an INVALID_INPUT-class `CapxulError` at the SDK seam.
|
|
905
|
+
* - `state`: the resolved bootstrap payload. Field shape matches the
|
|
906
|
+
* `BootstrapResolution` port; the SDK consumer can pass `state`
|
|
907
|
+
* directly (after `normalizeRuntimeUrl` on the two URL fields) into
|
|
908
|
+
* the port without re-branding.
|
|
909
|
+
*/
|
|
910
|
+
const BootstrapEnvelope = Schema.Struct({
|
|
911
|
+
protocol: Schema.Literal("capxul.bootstrap"),
|
|
912
|
+
version: Schema.Literal(1),
|
|
913
|
+
state: Schema.Struct({
|
|
914
|
+
applicationId: AppIdSchema,
|
|
915
|
+
chainId: ChainIdSchema,
|
|
916
|
+
sessionToken: SessionTokenSchema,
|
|
917
|
+
issuedAt: EpochMsSchema,
|
|
918
|
+
expiresIn: DurationMsSchema,
|
|
919
|
+
authBaseUrl: Schema.String,
|
|
920
|
+
convexUrl: Schema.String,
|
|
921
|
+
siteBaseUrl: Schema.String,
|
|
922
|
+
openfortPublishableKey: Schema.String,
|
|
923
|
+
shieldPublishableKey: Schema.String,
|
|
924
|
+
/** Which Openfort third-party auth provider the signer presents; absent means better-auth. */
|
|
925
|
+
openfortAuthProvider: Schema.optional(Schema.Literals(["better-auth", "oidc"])),
|
|
926
|
+
/** Origin of the Ponder indexer the SDK reads directly; absent means no indexer reads. */
|
|
927
|
+
indexerUrl: Schema.optional(Schema.String),
|
|
928
|
+
engineeringTelemetry: Schema.optional(EngineeringTelemetryBootstrapPolicy)
|
|
929
|
+
})
|
|
930
|
+
});
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/applications/bootstrap-envelope.ts
|
|
933
|
+
/**
|
|
934
|
+
* Decode a bootstrap body. A body the envelope refuses throws INVALID_INPUT
|
|
935
|
+
* (#119 AC1): unknown `version` literals, missing fields and brand-rule
|
|
936
|
+
* failures all land there. The engineering telemetry policy decodes on its
|
|
937
|
+
* own, so a policy the SDK does not admit turns SDK telemetry off instead of
|
|
938
|
+
* failing the bootstrap.
|
|
939
|
+
*/
|
|
940
|
+
function decodeBootstrapBody(body) {
|
|
941
|
+
const state = readField(body, "state");
|
|
942
|
+
const rawEngineeringTelemetry = readField(state, "engineeringTelemetry");
|
|
943
|
+
const decoded = SchemaParser.decodeUnknownResult(BootstrapEnvelope)(isRecord(body) && isRecord(state) ? {
|
|
944
|
+
...body,
|
|
945
|
+
state: withoutPolicy(state)
|
|
946
|
+
} : body);
|
|
947
|
+
if (Result.isFailure(decoded)) throw Errors.invalidInput("bootstrapEnvelope", SchemaIssue.makeFormatterDefault()(decoded.failure));
|
|
948
|
+
const { state: decodedState } = decoded.success;
|
|
949
|
+
const decodedEngineeringTelemetry = rawEngineeringTelemetry === void 0 ? void 0 : SchemaParser.decodeUnknownResult(EngineeringTelemetryBootstrapPolicy)(rawEngineeringTelemetry, { onExcessProperty: "error" });
|
|
950
|
+
return {
|
|
951
|
+
applicationId: decodedState.applicationId,
|
|
952
|
+
chainId: decodedState.chainId,
|
|
953
|
+
sessionToken: decodedState.sessionToken,
|
|
954
|
+
issuedAt: decodedState.issuedAt,
|
|
955
|
+
expiresIn: decodedState.expiresIn,
|
|
956
|
+
authBaseUrl: normalizeRuntimeUrl("authBaseUrl", decodedState.authBaseUrl),
|
|
957
|
+
convexUrl: normalizeRuntimeUrl("convexUrl", decodedState.convexUrl),
|
|
958
|
+
siteBaseUrl: normalizeRuntimeUrl("siteBaseUrl", decodedState.siteBaseUrl),
|
|
959
|
+
openfortPublishableKey: decodedState.openfortPublishableKey,
|
|
960
|
+
shieldPublishableKey: decodedState.shieldPublishableKey,
|
|
961
|
+
...decodedState.openfortAuthProvider === void 0 ? {} : { openfortAuthProvider: decodedState.openfortAuthProvider },
|
|
962
|
+
...decodedState.indexerUrl === void 0 ? {} : { indexerUrl: decodedState.indexerUrl },
|
|
963
|
+
...engineeringTelemetryOf(decodedEngineeringTelemetry)
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/** The bootstrap state without the policy, which decodes separately. */
|
|
967
|
+
function withoutPolicy(state) {
|
|
968
|
+
const { engineeringTelemetry: _policy, ...rest } = state;
|
|
969
|
+
return rest;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* The engineering telemetry policy the backend sent. One it does not admit
|
|
973
|
+
* leaves SDK telemetry off, and says so once with the decode issue.
|
|
974
|
+
*/
|
|
975
|
+
function engineeringTelemetryOf(decoded) {
|
|
976
|
+
if (decoded === void 0) return {};
|
|
977
|
+
if (Result.isSuccess(decoded)) return { engineeringTelemetry: decoded.success };
|
|
978
|
+
console.warn("[capxul-sdk] engineering telemetry policy refused; SDK telemetry is off", { issue: SchemaIssue.makeFormatterDefault()(decoded.failure) });
|
|
979
|
+
return {};
|
|
980
|
+
}
|
|
981
|
+
function normalizeRuntimeUrl(field, raw) {
|
|
982
|
+
let parsed;
|
|
983
|
+
try {
|
|
984
|
+
parsed = new URL(raw);
|
|
985
|
+
} catch {
|
|
986
|
+
throw Errors.invalidInput(field, "must be an http or https URL");
|
|
987
|
+
}
|
|
988
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw Errors.invalidInput(field, "must be an http or https URL");
|
|
989
|
+
return parsed.toString().replace(/\/$/, "");
|
|
990
|
+
}
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/applications/bootstrap-adapter.ts
|
|
993
|
+
/** A cached envelope this close to its expiry is fetched again, so it cannot lapse mid-command. */
|
|
994
|
+
const CACHE_EXPIRY_MARGIN_MS = 3e4;
|
|
995
|
+
/**
|
|
996
|
+
* The cached envelope, when it is still inside its own lifetime. It is decoded
|
|
997
|
+
* as a fresh body is; anything that does not decode, or a store that fails,
|
|
998
|
+
* is a miss.
|
|
999
|
+
*/
|
|
1000
|
+
async function cachedEnvelope(cache, now) {
|
|
1001
|
+
try {
|
|
1002
|
+
const cached = decodeBootstrapBody(await cache.load());
|
|
1003
|
+
const expiresAt = cached.issuedAt + cached.expiresIn - CACHE_EXPIRY_MARGIN_MS;
|
|
1004
|
+
return cached.issuedAt <= now && now < expiresAt ? cached : null;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const DEFAULT_RETRY = {
|
|
1010
|
+
attempts: 2,
|
|
1011
|
+
baseDelayMs: 400
|
|
1012
|
+
};
|
|
1013
|
+
/** Request-side statuses a proxy or backend returns while momentarily unable to serve. */
|
|
1014
|
+
const TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
1015
|
+
408,
|
|
1016
|
+
425,
|
|
1017
|
+
429
|
|
1018
|
+
]);
|
|
1019
|
+
/** Every 5xx is a server-side condition worth one more try; the client sent nothing wrong. */
|
|
1020
|
+
function isTransientHttpStatus(status) {
|
|
1021
|
+
return status >= 500 || TRANSIENT_HTTP_STATUSES.has(status);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* A transient HTTP status is worth one more try. The bootstrap call crosses
|
|
1025
|
+
* the host's own proxy before it reaches Convex, and every observed failure
|
|
1026
|
+
* of that hop cleared on a retry seconds later. A network rejection (offline,
|
|
1027
|
+
* DNS, CORS) is not retried: it does not clear in a second, and the caller
|
|
1028
|
+
* surfaces it as `NETWORK_ERROR` at once. Auth and input rejections are
|
|
1029
|
+
* deterministic and never retried.
|
|
1030
|
+
*/
|
|
1031
|
+
function isTransientBootstrapFailure(error) {
|
|
1032
|
+
if (error.kind !== "provider") return false;
|
|
1033
|
+
const status = readField(error.details, "httpStatus");
|
|
1034
|
+
return typeof status === "number" && isTransientHttpStatus(status);
|
|
1035
|
+
}
|
|
1036
|
+
async function safeText(res) {
|
|
1037
|
+
try {
|
|
1038
|
+
return await res.text();
|
|
1039
|
+
} catch {
|
|
1040
|
+
return "";
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
var HttpBootstrapAdapter = class {
|
|
1044
|
+
bootstrapBaseUrl;
|
|
1045
|
+
bootstrapHost;
|
|
1046
|
+
fetchImpl;
|
|
1047
|
+
observation;
|
|
1048
|
+
retry;
|
|
1049
|
+
cache;
|
|
1050
|
+
constructor(deps) {
|
|
1051
|
+
this.bootstrapBaseUrl = deps.bootstrapBaseUrl.replace(/\/$/, "");
|
|
1052
|
+
this.bootstrapHost = hostOf(this.bootstrapBaseUrl);
|
|
1053
|
+
this.observation = deps.observation;
|
|
1054
|
+
this.retry = deps.retry ?? DEFAULT_RETRY;
|
|
1055
|
+
this.cache = deps.cache;
|
|
1056
|
+
this.fetchImpl = deps.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1057
|
+
}
|
|
1058
|
+
resolve(input) {
|
|
1059
|
+
const fetched = this.attempt(input).pipe(Effect.retry({
|
|
1060
|
+
times: this.retry.attempts,
|
|
1061
|
+
while: isTransientBootstrapFailure,
|
|
1062
|
+
schedule: Schedule.exponential(Duration.millis(this.retry.baseDelayMs))
|
|
1063
|
+
}));
|
|
1064
|
+
const cache = this.cache;
|
|
1065
|
+
if (cache === void 0) return fetched;
|
|
1066
|
+
return Effect.promise(() => cachedEnvelope(cache, Date.now())).pipe(Effect.filterOrElse((cached) => cached !== null, () => fetched));
|
|
1067
|
+
}
|
|
1068
|
+
attempt(input) {
|
|
1069
|
+
return Effect.tryPromise({
|
|
1070
|
+
try: () => {
|
|
1071
|
+
const headers = {
|
|
1072
|
+
"content-type": "application/json",
|
|
1073
|
+
...input.origin === void 0 ? {} : { origin: input.origin },
|
|
1074
|
+
...observationRequestHeaders(this.observation)
|
|
1075
|
+
};
|
|
1076
|
+
return this.fetchImpl(`${this.bootstrapBaseUrl}/v1/client/bootstrap`, {
|
|
1077
|
+
method: "POST",
|
|
1078
|
+
headers,
|
|
1079
|
+
body: JSON.stringify({ publishableKey: input.publishableKey })
|
|
1080
|
+
});
|
|
1081
|
+
},
|
|
1082
|
+
catch: (cause) => bootstrapErrorFromCapxul("network", Errors.networkError("bootstrap", cause, {
|
|
1083
|
+
provider: "bootstrap",
|
|
1084
|
+
failure_mode: "upstream-down",
|
|
1085
|
+
...this.bootstrapHost === void 0 ? {} : { target_host: this.bootstrapHost }
|
|
1086
|
+
}))
|
|
1087
|
+
}).pipe(Effect.flatMap((res) => this.mapResponse(res, input)));
|
|
1088
|
+
}
|
|
1089
|
+
mapResponse(res, input) {
|
|
1090
|
+
if (res.ok) return Effect.tryPromise({
|
|
1091
|
+
try: async () => {
|
|
1092
|
+
const body = await res.json();
|
|
1093
|
+
const resolution = decodeBootstrapBody(body);
|
|
1094
|
+
const cache = this.cache;
|
|
1095
|
+
if (cache !== void 0) await Promise.resolve().then(() => cache.save(body)).catch(() => void 0);
|
|
1096
|
+
return resolution;
|
|
1097
|
+
},
|
|
1098
|
+
catch: (cause) => {
|
|
1099
|
+
if (cause instanceof CapxulError && cause.code === "INVALID_INPUT") return bootstrapErrorFromCapxul("invalidInput", cause);
|
|
1100
|
+
return bootstrapErrorFromCapxul("malformedBody", Errors.providerError("convex", "bootstrap", cause instanceof Error ? cause : new Error(String(cause))));
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
return Effect.promise(() => safeText(res)).pipe(Effect.flatMap((body) => {
|
|
1104
|
+
const refused = backendFailure$1(body);
|
|
1105
|
+
if (refused !== null) return Effect.fail(bootstrapErrorFromCapxul(bootstrapKind(refused.code), refused));
|
|
1106
|
+
const responseBody = body.slice(0, 300);
|
|
1107
|
+
const edgeError = res.headers?.get?.("x-vercel-error") ?? void 0;
|
|
1108
|
+
const edgeRequestId = res.headers?.get?.("x-vercel-id") ?? void 0;
|
|
1109
|
+
const evidence = {
|
|
1110
|
+
...responseBody.length === 0 ? {} : { responseBody },
|
|
1111
|
+
...edgeError === void 0 ? {} : { edgeError },
|
|
1112
|
+
...edgeRequestId === void 0 ? {} : { edgeRequestId }
|
|
1113
|
+
};
|
|
1114
|
+
if (res.status === 401) return Effect.fail(bootstrapErrorFromCapxul("notAuthenticated", keyRefused(input, evidence)));
|
|
1115
|
+
return Effect.fail(bootstrapErrorFromCapxul("provider", Errors.providerError("convex", "bootstrap", /* @__PURE__ */ new Error(`HTTP ${res.status}${edgeError === void 0 ? "" : ` ${edgeError}`}`), {
|
|
1116
|
+
httpStatus: res.status,
|
|
1117
|
+
details: evidence
|
|
1118
|
+
})));
|
|
1119
|
+
}));
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
/** A JSON `SerializedCapxulError` body, decoded whole; anything else is not the backend's. */
|
|
1123
|
+
function backendFailure$1(body) {
|
|
1124
|
+
try {
|
|
1125
|
+
return decodeConvexError({ data: JSON.parse(body) });
|
|
1126
|
+
} catch {
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* A 401 the backend did not answer with its own refusal body (an older
|
|
1132
|
+
* backend answers `NOT_AUTHENTICATED: …` as text). The backend checks the
|
|
1133
|
+
* origin only when one is sent, so without one the key is what it refused.
|
|
1134
|
+
*/
|
|
1135
|
+
function keyRefused(input, evidence) {
|
|
1136
|
+
return new CapxulError("NOT_AUTHENTICATED", input.origin === void 0 ? "The bootstrap refused the publishable key." : "The bootstrap refused the publishable key or its origin.", { details: {
|
|
1137
|
+
...input.origin === void 0 ? {
|
|
1138
|
+
field: "publishableKey",
|
|
1139
|
+
reason: "unknown or revoked key"
|
|
1140
|
+
} : {},
|
|
1141
|
+
provider: "bootstrap",
|
|
1142
|
+
httpStatus: 401,
|
|
1143
|
+
...evidence
|
|
1144
|
+
} });
|
|
1145
|
+
}
|
|
1146
|
+
function bootstrapKind(code) {
|
|
1147
|
+
if (code === "NOT_AUTHENTICATED") return "notAuthenticated";
|
|
1148
|
+
if (code === "INVALID_INPUT") return "invalidInput";
|
|
1149
|
+
if (code === "RATE_LIMITED") return "rateLimited";
|
|
1150
|
+
return "provider";
|
|
1151
|
+
}
|
|
1152
|
+
function HttpBootstrapLayer(deps) {
|
|
1153
|
+
return Layer.succeed(BootstrapPortTag, new HttpBootstrapAdapter(deps));
|
|
1154
|
+
}
|
|
1155
|
+
function hostOf(url) {
|
|
1156
|
+
try {
|
|
1157
|
+
return new URL(url).hostname;
|
|
1158
|
+
} catch {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
//#endregion
|
|
1163
|
+
//#region src/profiles/port.ts
|
|
1164
|
+
/** The chain families a payout address may name (#1955). */
|
|
1165
|
+
const PAYOUT_CHAINS = [
|
|
1166
|
+
"evm",
|
|
1167
|
+
"solana",
|
|
1168
|
+
"starknet"
|
|
1169
|
+
];
|
|
1170
|
+
var IdentityError = class extends Data.TaggedError("IdentityError") {};
|
|
1171
|
+
function identityErrorFromCapxul(operation, error, cause = error) {
|
|
1172
|
+
return new IdentityError({
|
|
1173
|
+
operation,
|
|
1174
|
+
publicCode: error.code,
|
|
1175
|
+
publicError: error,
|
|
1176
|
+
cause,
|
|
1177
|
+
...error.details === void 0 ? {} : { details: error.details }
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
var IdentityPortTag = class extends Context.Service()("@capxul/sdk/ports/IdentityPort") {};
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/profiles/adapter.ts
|
|
1183
|
+
var ConvexIdentityAdapter = class {
|
|
1184
|
+
#convex;
|
|
1185
|
+
constructor(deps) {
|
|
1186
|
+
this.#convex = deps.convex;
|
|
1187
|
+
}
|
|
1188
|
+
loadByAuthUserId = (authUserId) => this.#call(details.get, { authUserId }, (view) => view === null ? null : brandProfile(view));
|
|
1189
|
+
create = (input) => this.#call(details.create, input, brandProfile);
|
|
1190
|
+
update = (input) => this.#call(details.update, input, brandProfile);
|
|
1191
|
+
completeOnboarding = (input) => this.#call(onboardings.complete, input, brandProfile);
|
|
1192
|
+
/** One row call: its failure as an IdentityError named by the row, its reply branded. */
|
|
1193
|
+
#call(row, args, brand) {
|
|
1194
|
+
return callOperation(this.#convex, row, args).pipe(Effect.mapError((error) => identityErrorFromCapxul(row.name, error)), Effect.flatMap((output) => Effect.try({
|
|
1195
|
+
try: () => brand(output),
|
|
1196
|
+
catch: (cause) => identityErrorFromUnknown(row.name, cause)
|
|
1197
|
+
})), Effect.catchDefect((cause) => Effect.fail(identityErrorFromUnknown(row.name, cause))));
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const ConvexIdentityLayer = Layer.effect(IdentityPortTag, Effect.map(ConvexCallPortTag, (convex) => new ConvexIdentityAdapter({ convex })));
|
|
1201
|
+
function brandProfile(view) {
|
|
1202
|
+
return {
|
|
1203
|
+
authUserId: toAuthUserId(view.authUserId),
|
|
1204
|
+
...view.testerKind === void 0 ? {} : { testerKind: toTesterKind(view.testerKind) },
|
|
1205
|
+
email: toEmail(view.email),
|
|
1206
|
+
displayName: view.displayName,
|
|
1207
|
+
country: view.country === null ? null : toCountryCode(view.country),
|
|
1208
|
+
onboarded: view.onboarded,
|
|
1209
|
+
withdrawalAddress: view.withdrawalAddress === null ? null : toAddress(view.withdrawalAddress),
|
|
1210
|
+
handle: view.handle === null ? null : toHandle(view.handle),
|
|
1211
|
+
imageUrl: view.imageUrl,
|
|
1212
|
+
kycTier: toKycTier(view.kycTier),
|
|
1213
|
+
createdAt: toEpochMs(view.createdAt),
|
|
1214
|
+
updatedAt: toEpochMs(view.updatedAt)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** A branding refusal keeps its code; anything else is a defect, never a provider failure. */
|
|
1218
|
+
function identityErrorFromUnknown(operation, cause) {
|
|
1219
|
+
if (cause instanceof CapxulError) return identityErrorFromCapxul(operation, cause);
|
|
1220
|
+
return identityErrorFromCapxul(operation, Errors.unknown(cause), cause);
|
|
1221
|
+
}
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/profiles/programs.ts
|
|
1224
|
+
/** The single Context tag the profile atom + `identity.loadCurrent` resolve. */
|
|
1225
|
+
var IdentityDepsTag = class extends Context.Service()("@capxul/sdk/IdentityDeps") {};
|
|
1226
|
+
/** Wrap a bundle as the `Context<IdentityDepsTag>` the React provider consumes. */
|
|
1227
|
+
function identityDepsContext(deps) {
|
|
1228
|
+
return Context.make(IdentityDepsTag, deps);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* `identity.loadCurrent` as a single Effect requiring ONLY `IdentityDepsTag`.
|
|
1232
|
+
* Faithful transcription of the current public method body (`identity.ts`):
|
|
1233
|
+
* the leading `signal.aborted` pre-check stays in the thin method wrapper;
|
|
1234
|
+
* the program snapshots the actor session, then narrows the `IdentityPort`
|
|
1235
|
+
* failure to its `publicError`. Defects/sync-throws surface as `UNKNOWN`
|
|
1236
|
+
* through `toCapxulResult`'s `catchAllDefect` (and the atom's runtime), which
|
|
1237
|
+
* replaces the old outer `try/catch`.
|
|
1238
|
+
*/
|
|
1239
|
+
const loadIdentityProgram = Effect.gen(function* () {
|
|
1240
|
+
const deps = yield* IdentityDepsTag;
|
|
1241
|
+
if (deps.actor === void 0) return yield* Effect.fail(Errors.notAuthenticated());
|
|
1242
|
+
const state = deps.actor.snapshot();
|
|
1243
|
+
if (state.phase === "faulted") return yield* Effect.fail(publicIdentityFailure(state.failure));
|
|
1244
|
+
const authUserId = sessionFromActor(deps.actor)?.authUserId;
|
|
1245
|
+
if (authUserId === void 0) return null;
|
|
1246
|
+
return yield* deps.identityPort.loadByAuthUserId(authUserId).pipe(Effect.catchDefect((cause) => Effect.fail(identityErrorFromCapxul("loadByAuthUserId", asCapxulError(cause), cause))), Effect.mapError((failure) => failure.publicError));
|
|
1247
|
+
});
|
|
1248
|
+
/**
|
|
1249
|
+
* THE ONE boundary grammar for those optional fields. Both `completeProfile`
|
|
1250
|
+
* entry points — the method bundle here and the identity runtime in
|
|
1251
|
+
* `create-capxul-client.ts` — run it, so a malformed value is refused the same
|
|
1252
|
+
* way at each and never reaches the port.
|
|
1253
|
+
*/
|
|
1254
|
+
function checkProfileCarriage(profile) {
|
|
1255
|
+
const carriage = {};
|
|
1256
|
+
if (profile.imageStorageId !== void 0) {
|
|
1257
|
+
if (typeof profile.imageStorageId !== "string" || profile.imageStorageId.trim().length === 0) return {
|
|
1258
|
+
ok: false,
|
|
1259
|
+
field: "imageStorageId",
|
|
1260
|
+
message: "must be a non-empty storage id"
|
|
1261
|
+
};
|
|
1262
|
+
carriage.imageStorageId = profile.imageStorageId.trim();
|
|
1263
|
+
}
|
|
1264
|
+
if (profile.payoutAddresses !== void 0) {
|
|
1265
|
+
if (!Array.isArray(profile.payoutAddresses)) return {
|
|
1266
|
+
ok: false,
|
|
1267
|
+
field: "payoutAddresses",
|
|
1268
|
+
message: "must be an array"
|
|
1269
|
+
};
|
|
1270
|
+
if (profile.payoutAddresses.length > 10) return {
|
|
1271
|
+
ok: false,
|
|
1272
|
+
field: "payoutAddresses",
|
|
1273
|
+
message: `must carry at most 10 addresses`
|
|
1274
|
+
};
|
|
1275
|
+
const entries = [];
|
|
1276
|
+
for (const entry of profile.payoutAddresses) {
|
|
1277
|
+
if (typeof entry !== "object" || entry === null || !PAYOUT_CHAINS.some((chain) => chain === entry.chain) || typeof entry.address !== "string" || entry.address.trim().length === 0) return {
|
|
1278
|
+
ok: false,
|
|
1279
|
+
field: "payoutAddresses",
|
|
1280
|
+
message: `each entry needs a chain of ${PAYOUT_CHAINS.join(", ")} and an address`
|
|
1281
|
+
};
|
|
1282
|
+
entries.push({
|
|
1283
|
+
chain: entry.chain,
|
|
1284
|
+
address: entry.address.trim()
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
carriage.payoutAddresses = entries;
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
ok: true,
|
|
1291
|
+
carriage
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function makeIdentityMethods(deps) {
|
|
1295
|
+
const convexCall = deps.convexCall;
|
|
1296
|
+
return {
|
|
1297
|
+
async loadCurrent(options) {
|
|
1298
|
+
if (options?.signal?.aborted) return {
|
|
1299
|
+
ok: false,
|
|
1300
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.identity.loadCurrent })
|
|
1301
|
+
};
|
|
1302
|
+
return toCapxulResult(loadIdentityProgram, identityDepsContext(deps), deps.runPromise);
|
|
1303
|
+
},
|
|
1304
|
+
async completeProfile(profile, options) {
|
|
1305
|
+
if (options?.signal?.aborted) return {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
error: Errors.cancelled({ operation: onboardings.complete.name })
|
|
1308
|
+
};
|
|
1309
|
+
if (typeof profile !== "object" || profile === null || typeof profile.displayName !== "string" || typeof profile.country !== "string" || typeof profile.handle !== "string") return {
|
|
1310
|
+
ok: false,
|
|
1311
|
+
error: Errors.invalidInput("profile", "invalid profile")
|
|
1312
|
+
};
|
|
1313
|
+
const displayName = profile.displayName.trim();
|
|
1314
|
+
const handle = profile.handle.trim();
|
|
1315
|
+
if (displayName.length === 0) return {
|
|
1316
|
+
ok: false,
|
|
1317
|
+
error: Errors.invalidInput("displayName", "must not be empty")
|
|
1318
|
+
};
|
|
1319
|
+
if (handle.length === 0) return {
|
|
1320
|
+
ok: false,
|
|
1321
|
+
error: Errors.invalidInput("handle", "must not be empty")
|
|
1322
|
+
};
|
|
1323
|
+
let country;
|
|
1324
|
+
try {
|
|
1325
|
+
country = toCountryCode(profile.country);
|
|
1326
|
+
} catch {
|
|
1327
|
+
return {
|
|
1328
|
+
ok: false,
|
|
1329
|
+
error: Errors.invalidInput("country", "unsupported country")
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const carried = checkProfileCarriage(profile);
|
|
1333
|
+
if (!carried.ok) return {
|
|
1334
|
+
ok: false,
|
|
1335
|
+
error: Errors.invalidInput(carried.field, carried.message)
|
|
1336
|
+
};
|
|
1337
|
+
const session = deps.actor?.authSession();
|
|
1338
|
+
if (session === void 0 || session === null) return {
|
|
1339
|
+
ok: false,
|
|
1340
|
+
error: Errors.notAuthenticated()
|
|
1341
|
+
};
|
|
1342
|
+
return runPortEffect(deps.identityPort.completeOnboarding({
|
|
1343
|
+
authUserId: session.authUserId,
|
|
1344
|
+
email: session.email,
|
|
1345
|
+
displayName,
|
|
1346
|
+
country,
|
|
1347
|
+
handle,
|
|
1348
|
+
...carried.carriage
|
|
1349
|
+
}), options, onboardings.complete.name, deps.runPromise);
|
|
1350
|
+
},
|
|
1351
|
+
async handleAvailable(handle, options) {
|
|
1352
|
+
const row = details.handleAvailable;
|
|
1353
|
+
if (options?.signal?.aborted) return {
|
|
1354
|
+
ok: false,
|
|
1355
|
+
error: Errors.cancelled({ operation: row.name })
|
|
1356
|
+
};
|
|
1357
|
+
if (convexCall === void 0) return {
|
|
1358
|
+
ok: false,
|
|
1359
|
+
error: Errors.notImplemented("identity", "handleAvailable")
|
|
1360
|
+
};
|
|
1361
|
+
return runCapxulEffect(callOperation(convexCall, row, { handle }), options, row.name, deps.runPromise);
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/profiles/person-onboarding.ts
|
|
1367
|
+
function normalizeHandleInput(input) {
|
|
1368
|
+
if (typeof input !== "string") return {
|
|
1369
|
+
ok: false,
|
|
1370
|
+
error: Errors.invalidInput("handle", "is required")
|
|
1371
|
+
};
|
|
1372
|
+
try {
|
|
1373
|
+
return {
|
|
1374
|
+
ok: true,
|
|
1375
|
+
value: validateHandle(input)
|
|
1376
|
+
};
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
return failed(error);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function isClaimedIdentityState(state) {
|
|
1382
|
+
return state.phase === "authenticated" && state.account.at === "claimed";
|
|
1383
|
+
}
|
|
1384
|
+
function accountInFlight(state) {
|
|
1385
|
+
return state.phase === "authenticated" && (state.account.at === "deriving" || state.account.at === "claiming");
|
|
1386
|
+
}
|
|
1387
|
+
function settledAccount(deps, options) {
|
|
1388
|
+
return new Promise((resolve) => {
|
|
1389
|
+
let done = false;
|
|
1390
|
+
let unsubscribe = null;
|
|
1391
|
+
const onAbort = () => finish({
|
|
1392
|
+
ok: false,
|
|
1393
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.onboarding.completeOrganization })
|
|
1394
|
+
});
|
|
1395
|
+
const finish = (result) => {
|
|
1396
|
+
if (done) return;
|
|
1397
|
+
done = true;
|
|
1398
|
+
unsubscribe?.();
|
|
1399
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
1400
|
+
resolve(result);
|
|
1401
|
+
};
|
|
1402
|
+
if (options?.signal?.aborted) {
|
|
1403
|
+
onAbort();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1407
|
+
const stop = deps.subscribeIdentity((state) => {
|
|
1408
|
+
if (!accountInFlight(state)) finish({
|
|
1409
|
+
ok: true,
|
|
1410
|
+
value: state
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
if (done) {
|
|
1414
|
+
stop();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
unsubscribe = stop;
|
|
1418
|
+
const current = deps.snapshotIdentity();
|
|
1419
|
+
if (!accountInFlight(current)) finish({
|
|
1420
|
+
ok: true,
|
|
1421
|
+
value: current
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
async function reachClaimedAccount(deps, options) {
|
|
1426
|
+
let state = deps.snapshotIdentity();
|
|
1427
|
+
if (accountInFlight(state)) {
|
|
1428
|
+
const settled = await settledAccount(deps, options);
|
|
1429
|
+
if (!settled.ok) return settled;
|
|
1430
|
+
state = settled.value;
|
|
1431
|
+
}
|
|
1432
|
+
if (state.phase !== "authenticated" || state.account.at === "unknown") {
|
|
1433
|
+
const ensured = await deps.sendIdentity({ _tag: "EnsureAccount" }, options);
|
|
1434
|
+
if (!ensured.ok) return ensured;
|
|
1435
|
+
state = ensured.value;
|
|
1436
|
+
}
|
|
1437
|
+
if (state.phase !== "authenticated") return organizationWrongState(state.phase, ["authenticated:claimed"]);
|
|
1438
|
+
if (accountInFlight(state)) {
|
|
1439
|
+
const settled = await settledAccount(deps, options);
|
|
1440
|
+
if (!settled.ok) return settled;
|
|
1441
|
+
state = settled.value;
|
|
1442
|
+
if (state.phase !== "authenticated") return organizationWrongState(state.phase, ["authenticated:claimed"]);
|
|
1443
|
+
}
|
|
1444
|
+
if (state.account.at !== "claimed") {
|
|
1445
|
+
const event = state.account.at === "failed" ? { _tag: "RetryAccount" } : state.account.at === "counterfactual" ? { _tag: "ClaimAccount" } : { _tag: "EnsureAccount" };
|
|
1446
|
+
const claimed = await deps.sendIdentity(event, options);
|
|
1447
|
+
if (!claimed.ok) return claimed;
|
|
1448
|
+
state = claimed.value;
|
|
1449
|
+
}
|
|
1450
|
+
return isClaimedIdentityState(state) ? {
|
|
1451
|
+
ok: true,
|
|
1452
|
+
value: state
|
|
1453
|
+
} : organizationWrongState(state.phase === "authenticated" ? state.account.at : state.phase, ["claimed"]);
|
|
1454
|
+
}
|
|
1455
|
+
const cancelledPerson = () => ({
|
|
1456
|
+
ok: false,
|
|
1457
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.onboarding.completePerson })
|
|
1458
|
+
});
|
|
1459
|
+
const joinPerson = (pending, signal) => {
|
|
1460
|
+
if (signal === void 0) return pending;
|
|
1461
|
+
if (signal.aborted) return Promise.resolve(cancelledPerson());
|
|
1462
|
+
return new Promise((resolve) => {
|
|
1463
|
+
let done = false;
|
|
1464
|
+
const onAbort = () => finish(cancelledPerson());
|
|
1465
|
+
const finish = (result) => {
|
|
1466
|
+
if (done) return;
|
|
1467
|
+
done = true;
|
|
1468
|
+
signal.removeEventListener("abort", onAbort);
|
|
1469
|
+
resolve(result);
|
|
1470
|
+
};
|
|
1471
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1472
|
+
if (signal.aborted) onAbort();
|
|
1473
|
+
pending.then(finish, (cause) => finish(failed(cause)));
|
|
1474
|
+
});
|
|
1475
|
+
};
|
|
1476
|
+
const waitForRestoreReady = (ready, signal) => {
|
|
1477
|
+
if (signal === void 0) return ready.then(() => true);
|
|
1478
|
+
if (signal.aborted) return Promise.resolve(false);
|
|
1479
|
+
return new Promise((resolve) => {
|
|
1480
|
+
let done = false;
|
|
1481
|
+
const onAbort = () => finish(false);
|
|
1482
|
+
const finish = (value) => {
|
|
1483
|
+
if (done) return;
|
|
1484
|
+
done = true;
|
|
1485
|
+
signal.removeEventListener("abort", onAbort);
|
|
1486
|
+
resolve(value);
|
|
1487
|
+
};
|
|
1488
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1489
|
+
if (signal.aborted) onAbort();
|
|
1490
|
+
ready.then(() => finish(true), () => finish(false));
|
|
1491
|
+
});
|
|
1492
|
+
};
|
|
1493
|
+
const samePersonInput = (left, right) => {
|
|
1494
|
+
if (left.displayName !== right.displayName || left.country !== right.country || left.imageStorageId !== right.imageStorageId) return false;
|
|
1495
|
+
const a = left.payoutAddresses;
|
|
1496
|
+
const b = right.payoutAddresses;
|
|
1497
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
1498
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
1499
|
+
return a.every((address, index) => address?.chain === b[index]?.chain && address?.address === b[index]?.address);
|
|
1500
|
+
};
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region src/profiles/organization-onboarding.ts
|
|
1503
|
+
function normalizeOrganization(input) {
|
|
1504
|
+
if (typeof input !== "object" || input === null) return {
|
|
1505
|
+
ok: false,
|
|
1506
|
+
error: Errors.invalidInput("organization", "is required")
|
|
1507
|
+
};
|
|
1508
|
+
const name = typeof input.name === "string" ? input.name.trim() : "";
|
|
1509
|
+
if (name.length === 0) return {
|
|
1510
|
+
ok: false,
|
|
1511
|
+
error: Errors.invalidInput("name", "is required")
|
|
1512
|
+
};
|
|
1513
|
+
const handle = normalizeHandleInput(input.handle);
|
|
1514
|
+
if (!handle.ok) return handle;
|
|
1515
|
+
if (input.bio !== void 0 && typeof input.bio !== "string") return {
|
|
1516
|
+
ok: false,
|
|
1517
|
+
error: Errors.invalidInput("bio", "must be text")
|
|
1518
|
+
};
|
|
1519
|
+
if (input.size !== void 0 && typeof input.size !== "string") return {
|
|
1520
|
+
ok: false,
|
|
1521
|
+
error: Errors.invalidInput("size", "must be text")
|
|
1522
|
+
};
|
|
1523
|
+
if (input.logoStorageId !== void 0 && typeof input.logoStorageId !== "string") return {
|
|
1524
|
+
ok: false,
|
|
1525
|
+
error: Errors.invalidInput("logoStorageId", "must be a storage ID")
|
|
1526
|
+
};
|
|
1527
|
+
let country;
|
|
1528
|
+
try {
|
|
1529
|
+
country = toCountryCode(input.country);
|
|
1530
|
+
} catch {
|
|
1531
|
+
return {
|
|
1532
|
+
ok: false,
|
|
1533
|
+
error: Errors.invalidInput("country", "must be an ISO country code")
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const bio = input.bio?.trim() ?? "";
|
|
1537
|
+
const size = input.size?.trim() ?? "";
|
|
1538
|
+
return {
|
|
1539
|
+
ok: true,
|
|
1540
|
+
value: {
|
|
1541
|
+
name,
|
|
1542
|
+
handle: handle.value,
|
|
1543
|
+
country,
|
|
1544
|
+
...bio.length === 0 ? {} : { bio },
|
|
1545
|
+
...size.length === 0 ? {} : { size },
|
|
1546
|
+
...input.logoStorageId === void 0 ? {} : { logoStorageId: input.logoStorageId }
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function organizationWrongState(currentState, validStates, organizationId) {
|
|
1551
|
+
return {
|
|
1552
|
+
ok: false,
|
|
1553
|
+
error: Errors.wrongState({
|
|
1554
|
+
method: CAPXUL_OPERATIONS.onboarding.completeOrganization,
|
|
1555
|
+
currentState,
|
|
1556
|
+
validStates: [...validStates],
|
|
1557
|
+
...organizationId === void 0 ? {} : { organizationId }
|
|
1558
|
+
})
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function requestCancelled(options) {
|
|
1562
|
+
return options?.signal?.aborted === true;
|
|
1563
|
+
}
|
|
1564
|
+
async function beginOrganization(deps, input, options) {
|
|
1565
|
+
const normalized = normalizeOrganization(input);
|
|
1566
|
+
if (!normalized.ok) return normalized;
|
|
1567
|
+
const setup = deps.organizationSetup;
|
|
1568
|
+
if (setup === void 0) return {
|
|
1569
|
+
ok: false,
|
|
1570
|
+
error: Errors.notImplemented("organizationSetup", "begin")
|
|
1571
|
+
};
|
|
1572
|
+
const captured = deps.authLifetime();
|
|
1573
|
+
try {
|
|
1574
|
+
const session = await deps.restoreSession(options);
|
|
1575
|
+
if (!session.ok) return session;
|
|
1576
|
+
if (session.value === null) return {
|
|
1577
|
+
ok: false,
|
|
1578
|
+
error: Errors.notAuthenticated()
|
|
1579
|
+
};
|
|
1580
|
+
const authUserId = String(session.value.authUserId);
|
|
1581
|
+
const current = () => deps.matchesAuthLifetime(captured, authUserId) && !requestCancelled(options);
|
|
1582
|
+
if (!current()) return cancelledOrganization();
|
|
1583
|
+
const profile = await deps.loadProfile(options);
|
|
1584
|
+
if (!current()) return cancelledOrganization();
|
|
1585
|
+
if (!profile.ok) return profile;
|
|
1586
|
+
if (profile.value?.displayName === null || profile.value?.handle === null || profile.value === null) return organizationWrongState("profile-incomplete", ["profile-complete"]);
|
|
1587
|
+
const refreshed = await deps.refreshIdentity(options);
|
|
1588
|
+
if (!current()) return cancelledOrganization();
|
|
1589
|
+
if (!refreshed.ok) return refreshed;
|
|
1590
|
+
const claimed = await reachClaimedAccount(deps, options);
|
|
1591
|
+
if (!current()) return cancelledOrganization();
|
|
1592
|
+
if (!claimed.ok) return claimed;
|
|
1593
|
+
const begun = await setup.begin(normalized.value);
|
|
1594
|
+
if (!current()) return cancelledOrganization();
|
|
1595
|
+
if (begun.ok) {
|
|
1596
|
+
await deps.sendIdentity({
|
|
1597
|
+
_tag: "AttachOrganization",
|
|
1598
|
+
orgId: String(begun.value.orgId)
|
|
1599
|
+
}, options);
|
|
1600
|
+
if (!current()) return cancelledOrganization();
|
|
1601
|
+
}
|
|
1602
|
+
return begun;
|
|
1603
|
+
} catch (cause) {
|
|
1604
|
+
return failed(cause);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function setupFailure(setup) {
|
|
1608
|
+
const issue = setup.issue;
|
|
1609
|
+
const details = {
|
|
1610
|
+
...issue?.error?.details,
|
|
1611
|
+
organizationId: String(setup.orgId),
|
|
1612
|
+
...issue !== null && { setupReason: issue.reason }
|
|
1613
|
+
};
|
|
1614
|
+
if (issue?.error !== void 0) {
|
|
1615
|
+
const error = issue.error;
|
|
1616
|
+
return new CapxulError(error.code, error.message, {
|
|
1617
|
+
details,
|
|
1618
|
+
...error.mode === void 0 ? {} : { mode: error.mode },
|
|
1619
|
+
...error.chain === void 0 ? {} : { chain: error.chain },
|
|
1620
|
+
...error.layer === void 0 ? {} : { layer: error.layer },
|
|
1621
|
+
...error.correlationId === void 0 ? {} : { correlationId: error.correlationId }
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
const fallback = Errors.wrongState({
|
|
1625
|
+
method: CAPXUL_OPERATIONS.onboarding.completeOrganization,
|
|
1626
|
+
currentState: setup.status,
|
|
1627
|
+
validStates: ["ready"],
|
|
1628
|
+
organizationId: String(setup.orgId)
|
|
1629
|
+
});
|
|
1630
|
+
return new CapxulError(fallback.code, fallback.message, { details: {
|
|
1631
|
+
...fallback.details,
|
|
1632
|
+
...details
|
|
1633
|
+
} });
|
|
1634
|
+
}
|
|
1635
|
+
const cancelledOrganization = () => ({
|
|
1636
|
+
ok: false,
|
|
1637
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.onboarding.completeOrganization })
|
|
1638
|
+
});
|
|
1639
|
+
function awaitReady(deps, orgId, options) {
|
|
1640
|
+
const setup = deps.organizationSetup;
|
|
1641
|
+
if (setup === void 0) return Promise.resolve({
|
|
1642
|
+
ok: false,
|
|
1643
|
+
error: Errors.notImplemented("organizationSetup", "subscribe")
|
|
1644
|
+
});
|
|
1645
|
+
const lifetime = deps.authLifetime();
|
|
1646
|
+
const isCurrent = () => deps.matchesAuthLifetime(lifetime, lifetime.authUserId);
|
|
1647
|
+
return new Promise((resolve) => {
|
|
1648
|
+
let done = false;
|
|
1649
|
+
let stopRemote = null;
|
|
1650
|
+
let stopIdentity = null;
|
|
1651
|
+
const finish = (result) => {
|
|
1652
|
+
if (done) return;
|
|
1653
|
+
done = true;
|
|
1654
|
+
stopRemote?.();
|
|
1655
|
+
stopIdentity?.();
|
|
1656
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
1657
|
+
resolve(result);
|
|
1658
|
+
};
|
|
1659
|
+
const onAbort = () => finish(cancelledOrganization());
|
|
1660
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1661
|
+
const releaseIdentity = deps.subscribeIdentity((state) => {
|
|
1662
|
+
if (state.phase !== "authenticated" || !deps.matchesAuthLifetime(lifetime, state.session.authUserId)) finish(cancelledOrganization());
|
|
1663
|
+
});
|
|
1664
|
+
if (done) releaseIdentity();
|
|
1665
|
+
else stopIdentity = releaseIdentity;
|
|
1666
|
+
if (options?.signal?.aborted || !isCurrent()) return onAbort();
|
|
1667
|
+
setup.subscribe(orgId, (snapshot) => {
|
|
1668
|
+
if (done) return;
|
|
1669
|
+
if (!isCurrent()) return finish(cancelledOrganization());
|
|
1670
|
+
if (snapshot.status === "loading") return;
|
|
1671
|
+
if (snapshot.status === "error") return finish({
|
|
1672
|
+
ok: false,
|
|
1673
|
+
error: snapshot.error
|
|
1674
|
+
});
|
|
1675
|
+
if (snapshot.value.status === "ready") return finish({
|
|
1676
|
+
ok: true,
|
|
1677
|
+
value: { organizationId: orgId }
|
|
1678
|
+
});
|
|
1679
|
+
if (snapshot.value.status === "needsAttention" || snapshot.value.status === "failed") finish({
|
|
1680
|
+
ok: false,
|
|
1681
|
+
error: setupFailure(snapshot.value)
|
|
1682
|
+
});
|
|
1683
|
+
}, options).then((result) => {
|
|
1684
|
+
if (!result.ok) return finish(result);
|
|
1685
|
+
if (done) result.value();
|
|
1686
|
+
else stopRemote = result.value;
|
|
1687
|
+
}).catch((cause) => finish(failed(cause)));
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
//#endregion
|
|
1691
|
+
//#region src/profiles/onboardings.ts
|
|
1692
|
+
/** A thrown failure as a result: a CapxulError keeps its code; anything else is a defect. */
|
|
1693
|
+
function failed(cause) {
|
|
1694
|
+
return {
|
|
1695
|
+
ok: false,
|
|
1696
|
+
error: isCapxulError(cause) ? cause : Errors.unknown(cause)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function makeOnboardingMethods(deps) {
|
|
1700
|
+
let personFlight = null;
|
|
1701
|
+
return {
|
|
1702
|
+
async listOrganizationSetups(input, options) {
|
|
1703
|
+
if (requestCancelled(options)) return cancelledOrganization();
|
|
1704
|
+
if (deps.organizationSetup === void 0) return {
|
|
1705
|
+
ok: false,
|
|
1706
|
+
error: Errors.notImplemented("organizationSetup", "listMine")
|
|
1707
|
+
};
|
|
1708
|
+
const lifetime = deps.authLifetime();
|
|
1709
|
+
const session = await deps.restoreSession(options);
|
|
1710
|
+
if (!session.ok) return session;
|
|
1711
|
+
if (session.value === null) return {
|
|
1712
|
+
ok: false,
|
|
1713
|
+
error: Errors.notAuthenticated()
|
|
1714
|
+
};
|
|
1715
|
+
if (!deps.matchesAuthLifetime(lifetime, String(session.value.authUserId)) || requestCancelled(options)) return cancelledOrganization();
|
|
1716
|
+
const listed = await deps.organizationSetup.listMine(input, options);
|
|
1717
|
+
return deps.matchesAuthLifetime(lifetime, String(session.value.authUserId)) && !requestCancelled(options) ? listed : cancelledOrganization();
|
|
1718
|
+
},
|
|
1719
|
+
async completePerson(input, options) {
|
|
1720
|
+
if (options?.signal?.aborted) return cancelledPerson();
|
|
1721
|
+
if (typeof input !== "object" || input === null || typeof input.displayName !== "string" || typeof input.country !== "string") return {
|
|
1722
|
+
ok: false,
|
|
1723
|
+
error: Errors.invalidInput("profile", "displayName and country are required")
|
|
1724
|
+
};
|
|
1725
|
+
const handle = normalizeHandleInput(input.handle);
|
|
1726
|
+
if (!handle.ok) return handle;
|
|
1727
|
+
const entryAuthority = deps.authLifetime();
|
|
1728
|
+
if (personFlight !== null && personFlight.generation === entryAuthority.generation) {
|
|
1729
|
+
if (personFlight.handle !== handle.value || !samePersonInput(personFlight.input, input)) return {
|
|
1730
|
+
ok: false,
|
|
1731
|
+
error: Errors.wrongState({
|
|
1732
|
+
method: CAPXUL_OPERATIONS.onboarding.completePerson,
|
|
1733
|
+
currentState: "settingUp",
|
|
1734
|
+
validStates: ["ready"]
|
|
1735
|
+
})
|
|
1736
|
+
};
|
|
1737
|
+
const held = personFlight;
|
|
1738
|
+
if (!await waitForRestoreReady(held.ready, options?.signal)) return cancelledPerson();
|
|
1739
|
+
const verified = await deps.restoreSession(options);
|
|
1740
|
+
if (!verified.ok) return verified;
|
|
1741
|
+
if (verified.value === null) return {
|
|
1742
|
+
ok: false,
|
|
1743
|
+
error: Errors.notAuthenticated()
|
|
1744
|
+
};
|
|
1745
|
+
if (held.authUserId() === String(verified.value.authUserId) && deps.matchesAuthLifetime({
|
|
1746
|
+
generation: held.generation,
|
|
1747
|
+
authUserId: held.authUserId()
|
|
1748
|
+
}, held.authUserId())) {
|
|
1749
|
+
const result = await joinPerson(held.pending, options?.signal);
|
|
1750
|
+
return deps.matchesAuthLifetime({
|
|
1751
|
+
generation: held.generation,
|
|
1752
|
+
authUserId: held.authUserId()
|
|
1753
|
+
}, held.authUserId()) ? result : cancelledPerson();
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
let ownerAuthUserId = null;
|
|
1757
|
+
let readyReported = false;
|
|
1758
|
+
let markReady;
|
|
1759
|
+
const ready = new Promise((resolve) => {
|
|
1760
|
+
markReady = resolve;
|
|
1761
|
+
});
|
|
1762
|
+
const pending = (async () => {
|
|
1763
|
+
try {
|
|
1764
|
+
const isCancelled = () => options?.signal?.aborted === true;
|
|
1765
|
+
const session = await deps.restoreSession(options);
|
|
1766
|
+
if (!session.ok) return session;
|
|
1767
|
+
if (session.value === null) return {
|
|
1768
|
+
ok: false,
|
|
1769
|
+
error: Errors.notAuthenticated()
|
|
1770
|
+
};
|
|
1771
|
+
ownerAuthUserId = String(session.value.authUserId);
|
|
1772
|
+
readyReported = true;
|
|
1773
|
+
markReady();
|
|
1774
|
+
const isCurrent = () => {
|
|
1775
|
+
return deps.matchesAuthLifetime(entryAuthority, ownerAuthUserId);
|
|
1776
|
+
};
|
|
1777
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1778
|
+
const available = await deps.handleAvailable(handle.value, options);
|
|
1779
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1780
|
+
if (!available.ok) return available;
|
|
1781
|
+
if (!available.value.available) return {
|
|
1782
|
+
ok: false,
|
|
1783
|
+
error: Errors.invalidInput("handle", "handle is already taken")
|
|
1784
|
+
};
|
|
1785
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1786
|
+
const completed = await deps.completeProfile({
|
|
1787
|
+
...input,
|
|
1788
|
+
handle: available.value.normalized
|
|
1789
|
+
}, options);
|
|
1790
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1791
|
+
if (!completed.ok) return completed;
|
|
1792
|
+
const refreshed = await deps.refreshIdentity(options);
|
|
1793
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1794
|
+
if (!refreshed.ok) return refreshed;
|
|
1795
|
+
if (isCancelled()) return {
|
|
1796
|
+
ok: false,
|
|
1797
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.onboarding.completePerson })
|
|
1798
|
+
};
|
|
1799
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1800
|
+
const ensured = await deps.ensureAccount(options);
|
|
1801
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1802
|
+
if (!ensured.ok) return ensured;
|
|
1803
|
+
if (isCancelled()) return {
|
|
1804
|
+
ok: false,
|
|
1805
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.onboarding.completePerson })
|
|
1806
|
+
};
|
|
1807
|
+
const lifecycle = await deps.getLifecycle();
|
|
1808
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1809
|
+
if (!lifecycle.ok) return lifecycle;
|
|
1810
|
+
if (isCancelled()) return {
|
|
1811
|
+
ok: false,
|
|
1812
|
+
error: Errors.cancelled({ operation: CAPXUL_OPERATIONS.onboarding.completePerson })
|
|
1813
|
+
};
|
|
1814
|
+
if (lifecycle.value.status !== "ready") return {
|
|
1815
|
+
ok: false,
|
|
1816
|
+
error: Errors.wrongState({
|
|
1817
|
+
method: CAPXUL_OPERATIONS.onboarding.completePerson,
|
|
1818
|
+
currentState: lifecycle.value.status,
|
|
1819
|
+
validStates: ["ready"]
|
|
1820
|
+
})
|
|
1821
|
+
};
|
|
1822
|
+
const profile = await deps.loadProfile(options);
|
|
1823
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1824
|
+
if (!profile.ok) return profile;
|
|
1825
|
+
if (profile.value === null || profile.value.displayName === null || profile.value.handle !== available.value.normalized) return {
|
|
1826
|
+
ok: false,
|
|
1827
|
+
error: Errors.wrongState({
|
|
1828
|
+
method: CAPXUL_OPERATIONS.onboarding.completePerson,
|
|
1829
|
+
currentState: "profile-incomplete",
|
|
1830
|
+
validStates: ["profile-complete"]
|
|
1831
|
+
})
|
|
1832
|
+
};
|
|
1833
|
+
if (!isCurrent()) return cancelledPerson();
|
|
1834
|
+
return {
|
|
1835
|
+
ok: true,
|
|
1836
|
+
value: {
|
|
1837
|
+
profile: {
|
|
1838
|
+
...profile.value,
|
|
1839
|
+
displayName: profile.value.displayName,
|
|
1840
|
+
handle: profile.value.handle
|
|
1841
|
+
},
|
|
1842
|
+
account: lifecycle.value
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
} finally {
|
|
1846
|
+
if (!readyReported) markReady();
|
|
1847
|
+
}
|
|
1848
|
+
})();
|
|
1849
|
+
personFlight = {
|
|
1850
|
+
input,
|
|
1851
|
+
handle: handle.value,
|
|
1852
|
+
generation: entryAuthority.generation,
|
|
1853
|
+
ready,
|
|
1854
|
+
authUserId: () => ownerAuthUserId,
|
|
1855
|
+
pending
|
|
1856
|
+
};
|
|
1857
|
+
try {
|
|
1858
|
+
return await pending;
|
|
1859
|
+
} finally {
|
|
1860
|
+
if (personFlight?.pending === pending) personFlight = null;
|
|
1861
|
+
}
|
|
1862
|
+
},
|
|
1863
|
+
beginOrganization(input, options) {
|
|
1864
|
+
return beginOrganization(deps, input, options);
|
|
1865
|
+
},
|
|
1866
|
+
async completeOrganization(input, options) {
|
|
1867
|
+
try {
|
|
1868
|
+
const begun = await beginOrganization(deps, input, options);
|
|
1869
|
+
if (!begun.ok) return begun;
|
|
1870
|
+
const lifetime = deps.authLifetime();
|
|
1871
|
+
const isCurrent = () => deps.matchesAuthLifetime(lifetime, lifetime.authUserId);
|
|
1872
|
+
if (!isCurrent()) return cancelledOrganization();
|
|
1873
|
+
const { orgId } = begun.value;
|
|
1874
|
+
let setup = begun.value.setup;
|
|
1875
|
+
if (setup.status === "awaitingAuthorization") {
|
|
1876
|
+
const authorized = await deps.organizationSetup.authorize(orgId, options);
|
|
1877
|
+
if (!isCurrent()) return cancelledOrganization();
|
|
1878
|
+
if (!authorized.ok) return authorized;
|
|
1879
|
+
setup = authorized.value;
|
|
1880
|
+
}
|
|
1881
|
+
if (requestCancelled(options) || !isCurrent()) return cancelledOrganization();
|
|
1882
|
+
if (setup.status === "ready") return {
|
|
1883
|
+
ok: true,
|
|
1884
|
+
value: { organizationId: orgId }
|
|
1885
|
+
};
|
|
1886
|
+
if (setup.status === "needsAttention" || setup.status === "failed") return {
|
|
1887
|
+
ok: false,
|
|
1888
|
+
error: setupFailure(setup)
|
|
1889
|
+
};
|
|
1890
|
+
return awaitReady(deps, orgId, options);
|
|
1891
|
+
} catch (cause) {
|
|
1892
|
+
return failed(cause);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
//#endregion
|
|
1898
|
+
//#region src/platform/production-sessions.ts
|
|
1899
|
+
function resolveProductionObservability(input) {
|
|
1900
|
+
const observation = input.observability?.failures ?? input.observation;
|
|
1901
|
+
const telemetry = input.observability?.product ?? input.telemetry;
|
|
1902
|
+
const invocationObservability = input.invocationObservability;
|
|
1903
|
+
return {
|
|
1904
|
+
observation,
|
|
1905
|
+
telemetry,
|
|
1906
|
+
invocationObservation: invocationObservability?.failures ?? observation,
|
|
1907
|
+
invocationTelemetry: invocationObservability?.product ?? telemetry
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function hasConnectionObserver(port) {
|
|
1911
|
+
return "observeConnection" in port && typeof port.observeConnection === "function";
|
|
1912
|
+
}
|
|
1913
|
+
function observeProductionConvexConnection(convexCall, context) {
|
|
1914
|
+
if (!hasConnectionObserver(convexCall)) return () => {};
|
|
1915
|
+
const runPromise = Effect.runPromiseWith(context);
|
|
1916
|
+
try {
|
|
1917
|
+
return convexCall.observeConnection((diagnostic) => {
|
|
1918
|
+
const message = diagnostic.transition === "connected" ? Effect.logInfo("convex.connection.changed") : Effect.logWarning("convex.connection.changed");
|
|
1919
|
+
runPromise(message.pipe(Effect.annotateLogs(diagnostic), Effect.ignoreCause)).catch(() => void 0);
|
|
1920
|
+
});
|
|
1921
|
+
} catch {
|
|
1922
|
+
return () => {};
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function refreshConvexAuthOnSession(authClient, refresh) {
|
|
1926
|
+
return {
|
|
1927
|
+
...authClient,
|
|
1928
|
+
verifyOtp: (input, options) => authClient.verifyOtp(input, options).pipe(Effect.tap(() => Effect.sync(refresh))),
|
|
1929
|
+
signOut: (options) => authClient.signOut(options).pipe(Effect.tap(() => Effect.sync(refresh)))
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Signer-session reset, as a port wrapper INSIDE the Layer graph (blueprint §2:
|
|
1934
|
+
* "wrappers become Layers, not post-hoc spreads/Proxies").
|
|
1935
|
+
*
|
|
1936
|
+
* This was a spread over the assembled client — `{...client, auth: {...,
|
|
1937
|
+
* signOut}}` — applied after composition finished, so the reset lived on one
|
|
1938
|
+
* particular object rather than on the auth seam itself. Here it wraps
|
|
1939
|
+
* `AuthClientPort.signOut`, so it holds for every route to a sign-out.
|
|
1940
|
+
*
|
|
1941
|
+
* Capture the sign-out exit before reset. This preserves the old `try/finally`
|
|
1942
|
+
* order and keeps a reset failure in the typed error channel.
|
|
1943
|
+
*/
|
|
1944
|
+
function resetSignerSessionOnSignOut(authClient, resetRef) {
|
|
1945
|
+
return {
|
|
1946
|
+
...authClient,
|
|
1947
|
+
signOut: (options) => Effect.gen(function* () {
|
|
1948
|
+
const exit = yield* Effect.exit(authClient.signOut(options));
|
|
1949
|
+
yield* Effect.try({
|
|
1950
|
+
try: () => resetRef.current?.(),
|
|
1951
|
+
catch: (cause) => new AuthClientError({
|
|
1952
|
+
operation: "signOut",
|
|
1953
|
+
kind: "signer",
|
|
1954
|
+
cause
|
|
1955
|
+
})
|
|
1956
|
+
});
|
|
1957
|
+
return yield* exit;
|
|
1958
|
+
})
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region src/platform/transport/browser-lifecycle.ts
|
|
1963
|
+
var BrowserLifecycleMonitor = class {
|
|
1964
|
+
#listeners = [];
|
|
1965
|
+
#last = {};
|
|
1966
|
+
constructor() {
|
|
1967
|
+
const windowTarget = eventTarget(globalThis.window);
|
|
1968
|
+
const documentTarget = eventTarget(globalThis.document);
|
|
1969
|
+
if (windowTarget === void 0 || documentTarget === void 0) return;
|
|
1970
|
+
this.#listen(documentTarget, "visibilitychange", () => {
|
|
1971
|
+
this.#record(globalThis.document?.visibilityState === "hidden" ? "visibility-hidden" : "visibility-visible");
|
|
1972
|
+
});
|
|
1973
|
+
this.#listen(windowTarget, "pagehide", () => this.#record("pagehide"));
|
|
1974
|
+
this.#listen(windowTarget, "pageshow", () => this.#record("pageshow"));
|
|
1975
|
+
this.#listen(windowTarget, "online", () => this.#record("online"));
|
|
1976
|
+
this.#listen(windowTarget, "offline", () => this.#record("offline"));
|
|
1977
|
+
}
|
|
1978
|
+
socketClose(event) {
|
|
1979
|
+
const now = Date.now();
|
|
1980
|
+
return {
|
|
1981
|
+
code: event.code,
|
|
1982
|
+
wasClean: event.wasClean,
|
|
1983
|
+
reasonPresent: event.reason.length > 0,
|
|
1984
|
+
...this.#last.event === void 0 ? {} : { browserEvent: this.#last.event },
|
|
1985
|
+
...this.#last.at === void 0 ? {} : { msSinceBrowserEvent: Math.max(0, now - this.#last.at) },
|
|
1986
|
+
...globalThis.document?.visibilityState === void 0 ? {} : { documentVisibility: globalThis.document.visibilityState },
|
|
1987
|
+
...globalThis.navigator?.onLine === void 0 ? {} : { navigatorOnline: globalThis.navigator.onLine }
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
close() {
|
|
1991
|
+
for (const [target, name, listener] of this.#listeners) target.removeEventListener(name, listener);
|
|
1992
|
+
this.#listeners.length = 0;
|
|
1993
|
+
}
|
|
1994
|
+
#listen(target, name, listener) {
|
|
1995
|
+
target.addEventListener(name, listener);
|
|
1996
|
+
this.#listeners.push([
|
|
1997
|
+
target,
|
|
1998
|
+
name,
|
|
1999
|
+
listener
|
|
2000
|
+
]);
|
|
2001
|
+
}
|
|
2002
|
+
#record(event) {
|
|
2003
|
+
this.#last = {
|
|
2004
|
+
event,
|
|
2005
|
+
at: Date.now()
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
function eventTarget(value) {
|
|
2010
|
+
return isEventTarget(value) ? value : void 0;
|
|
2011
|
+
}
|
|
2012
|
+
function isEventTarget(value) {
|
|
2013
|
+
return isRecord(value) && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function" && typeof value.dispatchEvent === "function";
|
|
2014
|
+
}
|
|
2015
|
+
//#endregion
|
|
2016
|
+
//#region src/platform/transport/connection-monitor.ts
|
|
2017
|
+
var ConvexConnectionMonitor = class {
|
|
2018
|
+
#connectionId = makeConnectionId();
|
|
2019
|
+
#browser = new BrowserLifecycleMonitor();
|
|
2020
|
+
#lastClose;
|
|
2021
|
+
observedWebSocketConstructor() {
|
|
2022
|
+
const NativeWebSocket = globalThis.WebSocket;
|
|
2023
|
+
if (typeof globalThis.window === "undefined" || NativeWebSocket === void 0) return void 0;
|
|
2024
|
+
return new Proxy(NativeWebSocket, { construct: (Target, args) => {
|
|
2025
|
+
const socket = new Target(...args);
|
|
2026
|
+
socket.addEventListener("close", (event) => {
|
|
2027
|
+
this.#lastClose = this.#browser.socketClose(event);
|
|
2028
|
+
});
|
|
2029
|
+
return socket;
|
|
2030
|
+
} });
|
|
2031
|
+
}
|
|
2032
|
+
close() {
|
|
2033
|
+
this.#browser.close();
|
|
2034
|
+
}
|
|
2035
|
+
observe(client, observer) {
|
|
2036
|
+
const subscribe = client.subscribeToConnectionState?.bind(client);
|
|
2037
|
+
if (subscribe === void 0) return () => this.#browser.close();
|
|
2038
|
+
let previous;
|
|
2039
|
+
const unsubscribe = subscribe((next) => {
|
|
2040
|
+
const diagnostics = this.#diagnostics(previous, next);
|
|
2041
|
+
previous = next;
|
|
2042
|
+
for (const diagnostic of diagnostics) emitDiagnostic(observer, diagnostic);
|
|
2043
|
+
});
|
|
2044
|
+
if (previous === void 0) {
|
|
2045
|
+
const current = safeConnectionState(client);
|
|
2046
|
+
if (current !== void 0) {
|
|
2047
|
+
previous = current;
|
|
2048
|
+
for (const diagnostic of this.#diagnostics(void 0, current)) emitDiagnostic(observer, diagnostic);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
let closed = false;
|
|
2052
|
+
return () => {
|
|
2053
|
+
if (closed) return;
|
|
2054
|
+
closed = true;
|
|
2055
|
+
try {
|
|
2056
|
+
unsubscribe();
|
|
2057
|
+
} finally {
|
|
2058
|
+
this.#browser.close();
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
reconnectEvidence(start, end) {
|
|
2063
|
+
if (!connectionRestarted(start, end)) return void 0;
|
|
2064
|
+
return {
|
|
2065
|
+
kind: "connection-lost-in-flight",
|
|
2066
|
+
connection_id: this.#connectionId,
|
|
2067
|
+
start_connection_count: start.connectionCount,
|
|
2068
|
+
connection_count: end.connectionCount,
|
|
2069
|
+
...end.connectionRetries === void 0 ? {} : { connection_retries: end.connectionRetries },
|
|
2070
|
+
...closeFields(this.#lastClose)
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
#diagnostics(previous, next) {
|
|
2074
|
+
const transitions = connectionTransitions(previous, next);
|
|
2075
|
+
if (transitions.length === 0) return [];
|
|
2076
|
+
const oldest = next.timeOfOldestInflightRequest;
|
|
2077
|
+
const oldestInflightMs = oldest instanceof Date && Number.isFinite(oldest.getTime()) ? Math.max(0, Date.now() - oldest.getTime()) : void 0;
|
|
2078
|
+
const shared = {
|
|
2079
|
+
previous_connected: previous?.isWebSocketConnected ?? false,
|
|
2080
|
+
connected: next.isWebSocketConnected,
|
|
2081
|
+
...next.hasEverConnected === void 0 ? {} : { has_ever_connected: next.hasEverConnected },
|
|
2082
|
+
connection_count: next.connectionCount,
|
|
2083
|
+
...next.connectionRetries === void 0 ? {} : { connection_retries: next.connectionRetries },
|
|
2084
|
+
...next.hasInflightRequests === void 0 ? {} : { has_inflight_requests: next.hasInflightRequests },
|
|
2085
|
+
...next.inflightActions === void 0 ? {} : { inflight_actions: next.inflightActions },
|
|
2086
|
+
...next.inflightMutations === void 0 ? {} : { inflight_mutations: next.inflightMutations },
|
|
2087
|
+
...oldestInflightMs === void 0 ? {} : { oldest_inflight_ms: oldestInflightMs },
|
|
2088
|
+
...closeFields(this.#lastClose)
|
|
2089
|
+
};
|
|
2090
|
+
return transitions.map((transition) => ({
|
|
2091
|
+
connection_id: this.#connectionId,
|
|
2092
|
+
transition,
|
|
2093
|
+
...shared
|
|
2094
|
+
}));
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
function connectionRestarted(start, end) {
|
|
2098
|
+
if (start === void 0 || end === void 0) return false;
|
|
2099
|
+
const opensBeforeCancellation = start.isWebSocketConnected ? 1 : 2;
|
|
2100
|
+
return end.connectionCount >= start.connectionCount + opensBeforeCancellation;
|
|
2101
|
+
}
|
|
2102
|
+
function connectionTransitions(previous, next) {
|
|
2103
|
+
if (previous === void 0) {
|
|
2104
|
+
if (next.isWebSocketConnected) return (next.connectionRetries ?? 0) > 0 ? ["retrying", "connected"] : ["connected"];
|
|
2105
|
+
return (next.connectionRetries ?? 0) > 0 ? ["disconnected", "retrying"] : [];
|
|
2106
|
+
}
|
|
2107
|
+
const transitions = [];
|
|
2108
|
+
if (previous.isWebSocketConnected !== next.isWebSocketConnected) transitions.push(next.isWebSocketConnected ? "connected" : "disconnected");
|
|
2109
|
+
if ((next.connectionRetries ?? 0) > (previous.connectionRetries ?? 0)) transitions.push("retrying");
|
|
2110
|
+
return transitions;
|
|
2111
|
+
}
|
|
2112
|
+
function emitDiagnostic(observer, diagnostic) {
|
|
2113
|
+
try {
|
|
2114
|
+
observer(diagnostic);
|
|
2115
|
+
} catch {}
|
|
2116
|
+
}
|
|
2117
|
+
function safeConnectionState(client) {
|
|
2118
|
+
try {
|
|
2119
|
+
return client.connectionState?.();
|
|
2120
|
+
} catch {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
function closeFields(close) {
|
|
2125
|
+
if (close === void 0) return {};
|
|
2126
|
+
return {
|
|
2127
|
+
close_code: close.code,
|
|
2128
|
+
close_was_clean: close.wasClean,
|
|
2129
|
+
close_reason_present: close.reasonPresent,
|
|
2130
|
+
...close.documentVisibility === void 0 ? {} : { document_visibility: close.documentVisibility },
|
|
2131
|
+
...close.navigatorOnline === void 0 ? {} : { navigator_online: close.navigatorOnline },
|
|
2132
|
+
...close.browserEvent === void 0 ? {} : { last_browser_event: close.browserEvent },
|
|
2133
|
+
...close.msSinceBrowserEvent === void 0 ? {} : { ms_since_browser_event: close.msSinceBrowserEvent }
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
let fallbackConnectionId = 0;
|
|
2137
|
+
function makeConnectionId() {
|
|
2138
|
+
try {
|
|
2139
|
+
return `convex_${globalThis.crypto.randomUUID()}`;
|
|
2140
|
+
} catch {
|
|
2141
|
+
fallbackConnectionId += 1;
|
|
2142
|
+
return `convex_${Date.now().toString(36)}_${fallbackConnectionId.toString(36)}`;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
//#endregion
|
|
2146
|
+
//#region src/platform/transport/convex-diagnostics.ts
|
|
2147
|
+
const writeConvexDiagnostic = (...args) => {
|
|
2148
|
+
try {
|
|
2149
|
+
console.error(...maskCapturedNetworkRequest({ requestBody: args }).requestBody);
|
|
2150
|
+
} catch {}
|
|
2151
|
+
};
|
|
2152
|
+
const stderrConvexLogger = {
|
|
2153
|
+
log: writeConvexDiagnostic,
|
|
2154
|
+
warn: writeConvexDiagnostic,
|
|
2155
|
+
error: writeConvexDiagnostic,
|
|
2156
|
+
logVerbose: () => void 0
|
|
2157
|
+
};
|
|
2158
|
+
const silentConvexLogger = {
|
|
2159
|
+
log: () => void 0,
|
|
2160
|
+
warn: () => void 0,
|
|
2161
|
+
error: () => void 0,
|
|
2162
|
+
logVerbose: () => void 0
|
|
2163
|
+
};
|
|
2164
|
+
/** Exact floor-first allowlist; every additional handler must migrate its validator first. */
|
|
2165
|
+
const OBSERVED_CONVEX_ACTIONS = new Set(OPERATIONS.filter((row) => row.kind === "action").map((row) => row.path));
|
|
2166
|
+
const OBSERVED_CONVEX_QUERIES = new Set(OPERATIONS.filter((row) => row.kind === "query").map((row) => row.path));
|
|
2167
|
+
const OBSERVED_CONVEX_MUTATIONS = new Set(OPERATIONS.filter((row) => row.kind === "mutation").map((row) => row.path));
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/platform/transport/convex-errors.ts
|
|
2170
|
+
/**
|
|
2171
|
+
* All a redacting backend says about an error it raised itself: its request
|
|
2172
|
+
* id. The WebSocket client wraps it as `[CONVEX Q(path)] … Called by client`.
|
|
2173
|
+
*/
|
|
2174
|
+
const REDACTED_ERROR = /^(?:\[CONVEX [QMA?]\([^)]*\)\] )?\[Request ID: ([^\]\s]+)\] Server Error(?:\s+Called by client)?$/u;
|
|
2175
|
+
function mapToCapxulError(operation, err, transport) {
|
|
2176
|
+
const decoded = decodeConvexError(err);
|
|
2177
|
+
if (decoded !== null) return decoded;
|
|
2178
|
+
if (err instanceof CapxulError) return err;
|
|
2179
|
+
if (transport !== void 0) {
|
|
2180
|
+
const { kind: _kind, start_connection_count: _start, ...connection } = transport;
|
|
2181
|
+
return Errors.networkError(operation, err, {
|
|
2182
|
+
provider: "convex",
|
|
2183
|
+
failure_mode: "upstream-down",
|
|
2184
|
+
reason: "connection-lost-in-flight",
|
|
2185
|
+
...connection
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
if (err instanceof Error && isTransportError(err)) return Errors.networkError(operation, err, { provider: "convex" });
|
|
2189
|
+
return backendFailure(operation, err);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* A failure the backend raised outside its own error boundary: Convex
|
|
2193
|
+
* refusing the arguments (`ArgumentValidationError`) is the caller's input; a
|
|
2194
|
+
* function this deployment does not have is `CAPABILITY_UNAVAILABLE` naming
|
|
2195
|
+
* its path, because the backend runs a different version from this client;
|
|
2196
|
+
* anything else (an uncaught "Server Error") is a backend defect. None is a
|
|
2197
|
+
* provider outage. The Convex request id rides in details, so the backend's
|
|
2198
|
+
* log line can be found.
|
|
2199
|
+
*
|
|
2200
|
+
* A deployed backend redacts these errors (`REDACT_LOGS_TO_CLIENT`, #2304):
|
|
2201
|
+
* the message is only the request id, and no structured cause exists, so the
|
|
2202
|
+
* message shape is the only discriminator. Neither case can be told apart, and
|
|
2203
|
+
* the reply says where to look instead. The text matches stay for a backend
|
|
2204
|
+
* that does not redact (development, local).
|
|
2205
|
+
*/
|
|
2206
|
+
function backendFailure(operation, err) {
|
|
2207
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
2208
|
+
const redacted = REDACTED_ERROR.exec(text.trim())?.[1];
|
|
2209
|
+
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.`, {
|
|
2210
|
+
cause: err,
|
|
2211
|
+
layer: "convex",
|
|
2212
|
+
details: {
|
|
2213
|
+
operation,
|
|
2214
|
+
provider: "convex",
|
|
2215
|
+
requestId: redacted,
|
|
2216
|
+
reason: "backend-error-redacted"
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
const requestId = /\[Request ID: ([^\]\s]+)\]/u.exec(text)?.[1];
|
|
2220
|
+
const where = {
|
|
2221
|
+
operation,
|
|
2222
|
+
provider: "convex",
|
|
2223
|
+
...requestId === void 0 ? {} : { requestId }
|
|
2224
|
+
};
|
|
2225
|
+
const argument = /ArgumentValidationError:?\s*([^\n]*)/u.exec(text);
|
|
2226
|
+
if (argument !== null) return new CapxulError("INVALID_INPUT", `Invalid arguments for ${operation}`, {
|
|
2227
|
+
cause: err,
|
|
2228
|
+
layer: "convex",
|
|
2229
|
+
details: {
|
|
2230
|
+
field: "args",
|
|
2231
|
+
reason: argument[1] ?? "",
|
|
2232
|
+
...where
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
const path = /Could not find public function for '([^']+)'/u.exec(text)?.[1];
|
|
2236
|
+
if (path !== void 0) return new CapxulError("CAPABILITY_UNAVAILABLE", `The backend has no function ${path}: it runs a different version from this client.`, {
|
|
2237
|
+
cause: err,
|
|
2238
|
+
layer: "convex",
|
|
2239
|
+
details: {
|
|
2240
|
+
reason: "backend-function-missing",
|
|
2241
|
+
path,
|
|
2242
|
+
...where
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
return new CapxulError("UNKNOWN", text, {
|
|
2246
|
+
cause: err,
|
|
2247
|
+
layer: "convex",
|
|
2248
|
+
details: where
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
function mapToConvexCallError(operation, err, transport) {
|
|
2252
|
+
return convexCallErrorFromCapxul(operation, mapToCapxulError(operation, err, transport), transport);
|
|
2253
|
+
}
|
|
2254
|
+
function logActionTransportFailure(operation, error) {
|
|
2255
|
+
if (error.transport === void 0) return Effect.void;
|
|
2256
|
+
const { kind, ...connection } = error.transport;
|
|
2257
|
+
return Effect.logWarning("convex.action.connection_lost").pipe(Effect.annotateLogs({
|
|
2258
|
+
operation,
|
|
2259
|
+
failure_mode: "upstream-down",
|
|
2260
|
+
reason: kind,
|
|
2261
|
+
...connection
|
|
2262
|
+
}), Effect.ignoreCause);
|
|
2263
|
+
}
|
|
2264
|
+
const TRANSPORT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2265
|
+
"EAI_AGAIN",
|
|
2266
|
+
"ECONNREFUSED",
|
|
2267
|
+
"ECONNRESET",
|
|
2268
|
+
"ENOTFOUND",
|
|
2269
|
+
"EPIPE",
|
|
2270
|
+
"ETIMEDOUT",
|
|
2271
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
2272
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
2273
|
+
"UND_ERR_SOCKET"
|
|
2274
|
+
]);
|
|
2275
|
+
function isTransportError(err) {
|
|
2276
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2277
|
+
let current = err;
|
|
2278
|
+
while (current !== void 0 && !seen.has(current)) {
|
|
2279
|
+
seen.add(current);
|
|
2280
|
+
const code = current.code;
|
|
2281
|
+
if (current.name === "FetchError" || current.name === "NetworkError" || typeof code === "string" && TRANSPORT_ERROR_CODES.has(code)) return true;
|
|
2282
|
+
current = current.cause instanceof Error ? current.cause : void 0;
|
|
2283
|
+
}
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
//#endregion
|
|
2287
|
+
//#region src/platform/transport/observed-args.ts
|
|
2288
|
+
/**
|
|
2289
|
+
* The args one allowlisted Convex call carries: the caller's own observation
|
|
2290
|
+
* context, the host's, the application and the trace parent, merged and
|
|
2291
|
+
* sanitized. A call off the allowlist, or with nothing to carry, is unchanged.
|
|
2292
|
+
*/
|
|
2293
|
+
function observedConvexArgs(input) {
|
|
2294
|
+
const { allowlist, path, args, traceparent } = input;
|
|
2295
|
+
if (!allowlist.has(path)) return args;
|
|
2296
|
+
const invocationSnapshot = readInvocationObservation(args);
|
|
2297
|
+
if (path === diagnostics.health.path && invocationSnapshot?.active === false) {
|
|
2298
|
+
const unobservedArgs = { ...args };
|
|
2299
|
+
delete unobservedArgs.observationContext;
|
|
2300
|
+
return unobservedArgs;
|
|
2301
|
+
}
|
|
2302
|
+
const carriedContext = sanitizeObservationContext(args.observationContext);
|
|
2303
|
+
let hostContext;
|
|
2304
|
+
if (invocationSnapshot !== void 0) hostContext = invocationSnapshot.active ? invocationSnapshot.context : void 0;
|
|
2305
|
+
else {
|
|
2306
|
+
const resolveContext = input.resolveContext;
|
|
2307
|
+
if (resolveContext !== void 0) try {
|
|
2308
|
+
hostContext = resolveContext();
|
|
2309
|
+
} catch (cause) {
|
|
2310
|
+
hostContext = void 0;
|
|
2311
|
+
input.reportContextFailure(cause);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
if (hostContext === void 0 && carriedContext === void 0 && traceparent === void 0) return args;
|
|
2315
|
+
hostContext = sanitizeObservationContext({
|
|
2316
|
+
...hostContext,
|
|
2317
|
+
...carriedContext,
|
|
2318
|
+
...input.applicationId === void 0 ? {} : { applicationId: input.applicationId },
|
|
2319
|
+
...traceparent === void 0 ? {} : { traceparent }
|
|
2320
|
+
});
|
|
2321
|
+
if (hostContext === void 0) return args;
|
|
2322
|
+
return {
|
|
2323
|
+
...args,
|
|
2324
|
+
observationContext: hostContext
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
//#endregion
|
|
2328
|
+
//#region src/platform/transport/convex-subscription.ts
|
|
2329
|
+
/**
|
|
2330
|
+
* Subscribe one query, and the connection state when the caller asks for it.
|
|
2331
|
+
* The returned stop always finishes, and a callback never runs after it.
|
|
2332
|
+
*/
|
|
2333
|
+
function subscribeConvexQuery(client, fn, args, callback, options) {
|
|
2334
|
+
const path = getFunctionName(fn);
|
|
2335
|
+
let active = true;
|
|
2336
|
+
let unsubscribeQuery;
|
|
2337
|
+
let unsubscribeConnection;
|
|
2338
|
+
const stop = () => {
|
|
2339
|
+
if (!active) return;
|
|
2340
|
+
active = false;
|
|
2341
|
+
try {
|
|
2342
|
+
unsubscribeQuery?.();
|
|
2343
|
+
} catch {}
|
|
2344
|
+
try {
|
|
2345
|
+
unsubscribeConnection?.();
|
|
2346
|
+
} catch {}
|
|
2347
|
+
};
|
|
2348
|
+
try {
|
|
2349
|
+
if (options?.onConnectionChange !== void 0) {
|
|
2350
|
+
if (client.connectionState === void 0 || client.subscribeToConnectionState === void 0) throw Errors.notImplemented("convex", "connectionState");
|
|
2351
|
+
let previous;
|
|
2352
|
+
const observe = (state) => {
|
|
2353
|
+
if (!active || previous === state.isWebSocketConnected) return;
|
|
2354
|
+
previous = state.isWebSocketConnected;
|
|
2355
|
+
try {
|
|
2356
|
+
options.onConnectionChange?.(previous);
|
|
2357
|
+
} catch {}
|
|
2358
|
+
};
|
|
2359
|
+
unsubscribeConnection = client.subscribeToConnectionState(observe);
|
|
2360
|
+
observe(client.connectionState());
|
|
2361
|
+
}
|
|
2362
|
+
unsubscribeQuery = client.onUpdate(fn, args, (value) => {
|
|
2363
|
+
if (active) callback({
|
|
2364
|
+
status: "ok",
|
|
2365
|
+
value
|
|
2366
|
+
});
|
|
2367
|
+
}, (err) => {
|
|
2368
|
+
if (active) callback({
|
|
2369
|
+
status: "error",
|
|
2370
|
+
error: mapToCapxulError(path, err)
|
|
2371
|
+
});
|
|
2372
|
+
});
|
|
2373
|
+
if (!active) unsubscribeQuery();
|
|
2374
|
+
return stop;
|
|
2375
|
+
} catch (cause) {
|
|
2376
|
+
stop();
|
|
2377
|
+
throw cause;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
//#endregion
|
|
2381
|
+
//#region src/platform/transport/convex-adapter.ts
|
|
2382
|
+
/**
|
|
2383
|
+
* A socket that has not emitted `close` within this window is not going to.
|
|
2384
|
+
* ponytail: one fixed bound; `closeTimeoutMs` is the knob if a slow runtime
|
|
2385
|
+
* ever needs a different one.
|
|
2386
|
+
*/
|
|
2387
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5e3;
|
|
2388
|
+
/**
|
|
2389
|
+
* The fetcher the Convex client's auth handshake gets. Convex first asks for
|
|
2390
|
+
* "a possibly cached token" (`forceRefreshToken: false`) and, as soon as the
|
|
2391
|
+
* server accepts it, for a fresh one (`true`): the token whose `exp` it
|
|
2392
|
+
* schedules the next refresh from. The Better Auth clients keep no token: each
|
|
2393
|
+
* ask mints one over the network, so the cached ask was a second token fetch
|
|
2394
|
+
* and a second server verification (a JWKS read) on every session restore. It
|
|
2395
|
+
* answers "none" instead, and Convex makes its one fetch forced, as a fresh
|
|
2396
|
+
* token. Every forced ask still reaches the auth client: that first fetch, the
|
|
2397
|
+
* refresh before `exp`, and the retry after the server rejects a token.
|
|
2398
|
+
* ponytail: the OAuth bearer client can hold a cached exchange; it now pays one
|
|
2399
|
+
* exchange for the first authentication instead of reusing it (never more
|
|
2400
|
+
* fetches than before). Pass the cache through if that start ever matters.
|
|
2401
|
+
*/
|
|
2402
|
+
function freshTokenHandshake(fetchToken) {
|
|
2403
|
+
return async (args) => args.forceRefreshToken ? fetchToken(args) : null;
|
|
2404
|
+
}
|
|
2405
|
+
/** Run a Convex call under the current trace parent, when there is one. */
|
|
2406
|
+
const withTraceparent = (run) => Effect.flatMap(Effect.serviceOption(Tracer.ParentSpan), (parent) => run(parent._tag === "Some" ? formatTraceparent(parent.value) : void 0));
|
|
2407
|
+
var ConvexCallAdapter = class {
|
|
2408
|
+
#client;
|
|
2409
|
+
#convexUrl;
|
|
2410
|
+
#logger;
|
|
2411
|
+
#tokenProvider;
|
|
2412
|
+
#applicationId;
|
|
2413
|
+
#observation;
|
|
2414
|
+
#connectionMonitor;
|
|
2415
|
+
#closeTimeoutMs;
|
|
2416
|
+
constructor(deps) {
|
|
2417
|
+
this.#convexUrl = deps.convexUrl;
|
|
2418
|
+
this.#logger = deps.localDiagnostics === true ? stderrConvexLogger : silentConvexLogger;
|
|
2419
|
+
this.#closeTimeoutMs = deps.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
2420
|
+
this.#connectionMonitor = new ConvexConnectionMonitor();
|
|
2421
|
+
const webSocketConstructor = this.#connectionMonitor.observedWebSocketConstructor();
|
|
2422
|
+
this.#client = deps.client ?? new ConvexClient(deps.convexUrl, {
|
|
2423
|
+
...webSocketConstructor === void 0 ? {} : { webSocketConstructor },
|
|
2424
|
+
logger: this.#logger
|
|
2425
|
+
});
|
|
2426
|
+
this.#tokenProvider = deps.tokenProvider;
|
|
2427
|
+
this.#applicationId = deps.applicationId;
|
|
2428
|
+
this.#observation = deps.observation;
|
|
2429
|
+
this.refreshAuth();
|
|
2430
|
+
}
|
|
2431
|
+
refreshAuth() {
|
|
2432
|
+
if (this.#tokenProvider) this.#client.setAuth(freshTokenHandshake(this.#tokenProvider));
|
|
2433
|
+
}
|
|
2434
|
+
observeConnection(observer) {
|
|
2435
|
+
return this.#connectionMonitor.observe(this.#client, observer);
|
|
2436
|
+
}
|
|
2437
|
+
query(fn, args, options) {
|
|
2438
|
+
const path = getFunctionName(fn);
|
|
2439
|
+
return withTraceparent((traceparent) => {
|
|
2440
|
+
return Effect.tryPromise({
|
|
2441
|
+
try: (signal) => {
|
|
2442
|
+
const observed = this.#observedArgs(OBSERVED_CONVEX_QUERIES, path, args, traceparent);
|
|
2443
|
+
return options?.fresh === true ? this.#freshQuery(fn, observed, signal, path) : this.#client.query(fn, observed);
|
|
2444
|
+
},
|
|
2445
|
+
catch: (cause) => mapToConvexCallError(path, cause)
|
|
2446
|
+
});
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
async #freshQuery(fn, args, signal, path) {
|
|
2450
|
+
const token = await this.#tokenProvider?.({ forceRefreshToken: false });
|
|
2451
|
+
if (signal.aborted) throw Errors.cancelled({ operation: path });
|
|
2452
|
+
const client = new ConvexHttpClient(this.#convexUrl, {
|
|
2453
|
+
logger: this.#logger,
|
|
2454
|
+
fetch: (input, init) => globalThis.fetch(input, {
|
|
2455
|
+
...init,
|
|
2456
|
+
signal
|
|
2457
|
+
})
|
|
2458
|
+
});
|
|
2459
|
+
if (token !== void 0 && token !== null) client.setAuth(token);
|
|
2460
|
+
const query = fn;
|
|
2461
|
+
return client.query(query, args);
|
|
2462
|
+
}
|
|
2463
|
+
mutation(fn, args) {
|
|
2464
|
+
const path = getFunctionName(fn);
|
|
2465
|
+
return withTraceparent((traceparent) => {
|
|
2466
|
+
return Effect.tryPromise({
|
|
2467
|
+
try: () => this.#client.mutation(fn, this.#observedArgs(OBSERVED_CONVEX_MUTATIONS, path, args, traceparent)),
|
|
2468
|
+
catch: (cause) => mapToConvexCallError(path, cause)
|
|
2469
|
+
});
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
action(fn, args) {
|
|
2473
|
+
const path = getFunctionName(fn);
|
|
2474
|
+
return withTraceparent((traceparent) => {
|
|
2475
|
+
const connectionAtStart = connectionState(this.#client);
|
|
2476
|
+
return Effect.tryPromise({
|
|
2477
|
+
try: () => this.#client.action(fn, this.#observedArgs(OBSERVED_CONVEX_ACTIONS, path, args, traceparent)),
|
|
2478
|
+
catch: (cause) => {
|
|
2479
|
+
const transport = this.#connectionMonitor.reconnectEvidence(connectionAtStart, connectionState(this.#client));
|
|
2480
|
+
return mapToConvexCallError(path, cause, transport);
|
|
2481
|
+
}
|
|
2482
|
+
}).pipe(Effect.tapError((error) => logActionTransportFailure(path, error)));
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
#observedArgs(allowlist, path, args, traceparent) {
|
|
2486
|
+
return observedConvexArgs({
|
|
2487
|
+
allowlist,
|
|
2488
|
+
path,
|
|
2489
|
+
args,
|
|
2490
|
+
traceparent,
|
|
2491
|
+
applicationId: this.#applicationId,
|
|
2492
|
+
resolveContext: this.#observation?.resolveContext,
|
|
2493
|
+
reportContextFailure: (cause) => this.#reportContextFailure(cause)
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
#contextFailureReported = false;
|
|
2497
|
+
#reportContextFailure(cause) {
|
|
2498
|
+
if (this.#contextFailureReported) return;
|
|
2499
|
+
this.#contextFailureReported = true;
|
|
2500
|
+
console.warn("[capxul-sdk] observation resolveContext failed; calls are uncorrelated", { reason: cause instanceof Error ? cause.message : String(cause) });
|
|
2501
|
+
}
|
|
2502
|
+
subscribe(fn, args, callback, options) {
|
|
2503
|
+
return Effect.try({
|
|
2504
|
+
try: () => subscribeConvexQuery(this.#client, fn, args, callback, options),
|
|
2505
|
+
catch: (cause) => mapToConvexCallError(getFunctionName(fn), cause)
|
|
2506
|
+
}).pipe(Effect.tap(() => Effect.sync(() => {
|
|
2507
|
+
callback({ status: "loading" });
|
|
2508
|
+
})));
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Teardown always completes. `ConvexClient.close()` resolves only when the
|
|
2512
|
+
* socket emits `close`, and `WebSocketManager.close()` in the `connecting`
|
|
2513
|
+
* state arms `ws.onopen = () => ws.close()` without ever closing the socket
|
|
2514
|
+
* itself — so a handshake that stalls right after a server-initiated close
|
|
2515
|
+
* (code 1012, which is what a Convex deploy sends) leaves that promise
|
|
2516
|
+
* pending for as long as the page lives. Convex marks the client closed and
|
|
2517
|
+
* terminates the socket manager synchronously before returning it, so
|
|
2518
|
+
* abandoning it drops nothing. A rejection is dropped for the same reason —
|
|
2519
|
+
* the layer release turns it into a defect that fails the whole scope close,
|
|
2520
|
+
* and teardown has no caller who can act on it.
|
|
2521
|
+
*/
|
|
2522
|
+
async close() {
|
|
2523
|
+
this.#connectionMonitor.close();
|
|
2524
|
+
await settledWithin(this.#client.close(), this.#closeTimeoutMs);
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
function settledWithin(promise, timeoutMs) {
|
|
2528
|
+
return new Promise((resolve) => {
|
|
2529
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
2530
|
+
const finish = () => {
|
|
2531
|
+
clearTimeout(timer);
|
|
2532
|
+
resolve();
|
|
2533
|
+
};
|
|
2534
|
+
promise.then(finish, finish);
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
function ConvexCallLayer(deps) {
|
|
2538
|
+
return Layer.effect(ConvexCallPortTag, Effect.acquireRelease(Effect.sync(() => new ConvexCallAdapter(deps)), (adapter) => Effect.promise(() => adapter.close()).pipe(Effect.orDie)));
|
|
2539
|
+
}
|
|
2540
|
+
function connectionState(client) {
|
|
2541
|
+
try {
|
|
2542
|
+
const state = client.connectionState?.();
|
|
2543
|
+
return typeof state?.connectionCount === "number" ? state : void 0;
|
|
2544
|
+
} catch {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
//#endregion
|
|
2549
|
+
//#region src/observability/telemetry/clone.ts
|
|
2550
|
+
function cloneTelemetryProps(props) {
|
|
2551
|
+
const cloned = {};
|
|
2552
|
+
for (const [key, value] of Object.entries(props)) cloned[key] = cloneTelemetryValue(value);
|
|
2553
|
+
return cloned;
|
|
2554
|
+
}
|
|
2555
|
+
function cloneTelemetryEvent(event) {
|
|
2556
|
+
return event.props === void 0 ? { name: event.name } : {
|
|
2557
|
+
name: event.name,
|
|
2558
|
+
props: cloneTelemetryProps(event.props)
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
function cloneTelemetryValue(value) {
|
|
2562
|
+
if (Array.isArray(value)) return value.map(cloneTelemetryValue);
|
|
2563
|
+
if (value === null || typeof value !== "object") return value;
|
|
2564
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return value;
|
|
2565
|
+
const cloned = {};
|
|
2566
|
+
for (const [key, nested] of Object.entries(value)) cloned[key] = cloneTelemetryValue(nested);
|
|
2567
|
+
return cloned;
|
|
2568
|
+
}
|
|
2569
|
+
//#endregion
|
|
2570
|
+
//#region src/observability/telemetry/posthog-adapter.ts
|
|
2571
|
+
/** The PostHog transport refused one call; its reason rides the drop log. */
|
|
2572
|
+
var TelemetryTransportRejected = class extends Data.TaggedError("TelemetryTransportRejected") {};
|
|
2573
|
+
var PostHogTelemetryAdapter = class {
|
|
2574
|
+
#capture;
|
|
2575
|
+
#identify;
|
|
2576
|
+
#group;
|
|
2577
|
+
#reset;
|
|
2578
|
+
constructor(deps) {
|
|
2579
|
+
this.#capture = deps.capture;
|
|
2580
|
+
this.#identify = deps.identify ?? (() => void 0);
|
|
2581
|
+
this.#group = deps.group ?? (() => void 0);
|
|
2582
|
+
this.#reset = deps.reset ?? (() => void 0);
|
|
2583
|
+
}
|
|
2584
|
+
emit(event) {
|
|
2585
|
+
return this.#run(() => this.#capture(event.name, redactTelemetryProps(event.name, cloneTelemetryEvent(event).props), event), "emit", event.name);
|
|
2586
|
+
}
|
|
2587
|
+
identify(input) {
|
|
2588
|
+
return this.#run(() => this.#identify(cloneIdentifyInput(input)), "identify");
|
|
2589
|
+
}
|
|
2590
|
+
group(input) {
|
|
2591
|
+
return this.#run(() => this.#group(cloneGroupInput(input)), "group");
|
|
2592
|
+
}
|
|
2593
|
+
reset() {
|
|
2594
|
+
return this.#run(() => this.#reset(), "reset");
|
|
2595
|
+
}
|
|
2596
|
+
#run(operation, operationName, eventName) {
|
|
2597
|
+
const diagnose = (reason) => Effect.logWarning("product.telemetry.transport.dropped").pipe(Effect.annotateLogs({
|
|
2598
|
+
operation: operationName,
|
|
2599
|
+
reason: dropReason(reason),
|
|
2600
|
+
...eventName === void 0 ? {} : { product_event: eventName }
|
|
2601
|
+
}), Effect.ignoreCause);
|
|
2602
|
+
return Effect.suspend(() => {
|
|
2603
|
+
let pending;
|
|
2604
|
+
try {
|
|
2605
|
+
pending = operation();
|
|
2606
|
+
} catch (reason) {
|
|
2607
|
+
return diagnose(reason);
|
|
2608
|
+
}
|
|
2609
|
+
if (pending === void 0) return Effect.void;
|
|
2610
|
+
const transport = Effect.tryPromise({
|
|
2611
|
+
try: () => pending,
|
|
2612
|
+
catch: (reason) => new TelemetryTransportRejected({ reason })
|
|
2613
|
+
}).pipe(Effect.catch((rejected) => diagnose(rejected.reason)));
|
|
2614
|
+
return Effect.forkDetach(transport, { startImmediately: true }).pipe(Effect.asVoid);
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
const DROP_REASON_MAX = 200;
|
|
2619
|
+
function dropReason(reason) {
|
|
2620
|
+
const text = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
|
|
2621
|
+
return text.length <= DROP_REASON_MAX ? text : `${text.slice(0, DROP_REASON_MAX)}…`;
|
|
2622
|
+
}
|
|
2623
|
+
function PostHogTelemetryLayer(deps) {
|
|
2624
|
+
return Layer.succeed(TelemetryPortTag, new PostHogTelemetryAdapter(deps));
|
|
2625
|
+
}
|
|
2626
|
+
function cloneIdentifyInput(input) {
|
|
2627
|
+
const traits = input.traits === void 0 ? void 0 : cloneTelemetryProps(input.traits);
|
|
2628
|
+
const properties = input.properties === void 0 ? void 0 : cloneTelemetryProps(input.properties);
|
|
2629
|
+
return {
|
|
2630
|
+
distinctId: input.distinctId,
|
|
2631
|
+
...input.anonDistinctId === void 0 ? {} : { anonDistinctId: input.anonDistinctId },
|
|
2632
|
+
...traits === void 0 ? {} : { traits },
|
|
2633
|
+
...properties === void 0 ? {} : { properties }
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
function cloneGroupInput(input) {
|
|
2637
|
+
const properties = input.properties === void 0 ? void 0 : cloneTelemetryProps(input.properties);
|
|
2638
|
+
return properties === void 0 ? {
|
|
2639
|
+
groupType: input.groupType,
|
|
2640
|
+
groupKey: input.groupKey
|
|
2641
|
+
} : {
|
|
2642
|
+
groupType: input.groupType,
|
|
2643
|
+
groupKey: input.groupKey,
|
|
2644
|
+
properties
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
//#endregion
|
|
2648
|
+
//#region src/observability/telemetry/from-posthog.ts
|
|
2649
|
+
/**
|
|
2650
|
+
* Drop props that must never cross to a host-owned external sink. Today that is
|
|
2651
|
+
* the `$exception` `details` blob: `captureException` serializes
|
|
2652
|
+
* `CapxulError.details` (e.g. `{ asset, available, required }`, `{ name }`,
|
|
2653
|
+
* `{ accountId }` — errors.ts) into it, and the shared redactor has no
|
|
2654
|
+
* `$exception` rule, so it is stripped here at the boundary (infra#1037). The
|
|
2655
|
+
* safe fields (error code, operation, failure_mode, the fixed leak-safe message,
|
|
2656
|
+
* stack frames) are preserved.
|
|
2657
|
+
*/
|
|
2658
|
+
function stripHostUnsafeProps(props) {
|
|
2659
|
+
if (props === void 0) return void 0;
|
|
2660
|
+
const { details: _details, ...safe } = props;
|
|
2661
|
+
return safe;
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* The subset of a posthog-js client the seam calls. `identify` / `group` /
|
|
2665
|
+
* `reset` are optional — a host that only wants event capture can omit them.
|
|
2666
|
+
*/
|
|
2667
|
+
/**
|
|
2668
|
+
* Adapt the host's already-initialized posthog-like client into a
|
|
2669
|
+
* `TelemetryPort` for the `telemetry` prop / input. This port SUPPLANTS the
|
|
2670
|
+
* SDK's no-op default telemetry sink (`production.ts` binds it via
|
|
2671
|
+
* `Layer.succeed`, not `compose` — there is no client-side success relay to
|
|
2672
|
+
* compose with); it is additive to Capxul's backend first-party record and
|
|
2673
|
+
* never owns the client.
|
|
2674
|
+
*/
|
|
2675
|
+
function postHogProductTelemetry(policy, fixedSnapshot) {
|
|
2676
|
+
const telemetry = new PostHogTelemetryAdapter({
|
|
2677
|
+
capture: (name, props, source) => {
|
|
2678
|
+
const snapshot = readInvocationObservation(source) ?? fixedSnapshot ?? policy.snapshot();
|
|
2679
|
+
if (!snapshot.active || policy.client === null || policy.client === void 0) return;
|
|
2680
|
+
const event = stampTelemetryEnvelope({
|
|
2681
|
+
name,
|
|
2682
|
+
props
|
|
2683
|
+
}, {
|
|
2684
|
+
capxul_env: policy.capxulEnv,
|
|
2685
|
+
producer: "sdk"
|
|
2686
|
+
});
|
|
2687
|
+
policy.deliver(() => policy.client.capture(name, {
|
|
2688
|
+
...stripHostUnsafeProps(event.props),
|
|
2689
|
+
...observationContextProps(snapshot.context)
|
|
2690
|
+
}));
|
|
2691
|
+
},
|
|
2692
|
+
identify: (input) => {
|
|
2693
|
+
if (!(fixedSnapshot ?? policy.snapshot()).active || policy.client?.identify === void 0) return;
|
|
2694
|
+
policy.deliver(() => policy.client.identify(input.distinctId, {
|
|
2695
|
+
...input.traits,
|
|
2696
|
+
...input.properties
|
|
2697
|
+
}));
|
|
2698
|
+
},
|
|
2699
|
+
group: (input) => {
|
|
2700
|
+
if (!(fixedSnapshot ?? policy.snapshot()).active || policy.client?.group === void 0) return;
|
|
2701
|
+
policy.deliver(() => policy.client.group(input.groupType, input.groupKey, input.properties === void 0 ? void 0 : { ...input.properties }));
|
|
2702
|
+
},
|
|
2703
|
+
reset: () => {
|
|
2704
|
+
if (!(fixedSnapshot ?? policy.snapshot()).active || policy.client?.reset === void 0) return;
|
|
2705
|
+
policy.deliver(() => policy.client.reset());
|
|
2706
|
+
}
|
|
2707
|
+
});
|
|
2708
|
+
registerProductTelemetryInvocation(telemetry, (source) => postHogProductTelemetry(policy, readInvocationObservation(source) ?? fixedSnapshot ?? policy.snapshot()));
|
|
2709
|
+
return telemetry;
|
|
2710
|
+
}
|
|
2711
|
+
//#endregion
|
|
2712
|
+
//#region src/observability/host.ts
|
|
2713
|
+
const HOST_INTERNALS = /* @__PURE__ */ new WeakMap();
|
|
2714
|
+
/** Build the SDK's one host module around an already-initialized PostHog client. */
|
|
2715
|
+
function postHogObservability(client, options = {}) {
|
|
2716
|
+
const inactive = Object.freeze({
|
|
2717
|
+
active: false,
|
|
2718
|
+
contextProps: Object.freeze({})
|
|
2719
|
+
});
|
|
2720
|
+
const snapshot = () => {
|
|
2721
|
+
if (client === null || client === void 0) return inactive;
|
|
2722
|
+
try {
|
|
2723
|
+
if (!(typeof options.enabled === "function" ? options.enabled() : options.enabled ?? true)) return inactive;
|
|
2724
|
+
const rawContext = typeof options.context === "function" ? options.context() : options.context;
|
|
2725
|
+
const sanitized = sanitizeObservationContext(rawContext);
|
|
2726
|
+
const context = sanitized === void 0 ? void 0 : Object.freeze({ ...sanitized });
|
|
2727
|
+
return Object.freeze({
|
|
2728
|
+
active: true,
|
|
2729
|
+
...context === void 0 ? {} : { context },
|
|
2730
|
+
contextProps: Object.freeze(observationContextProps(context))
|
|
2731
|
+
});
|
|
2732
|
+
} catch {
|
|
2733
|
+
return inactive;
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
const policy = {
|
|
2737
|
+
client,
|
|
2738
|
+
capxulEnv: options.capxulEnv ?? "unknown",
|
|
2739
|
+
snapshot,
|
|
2740
|
+
deliver: (capture) => {
|
|
2741
|
+
try {
|
|
2742
|
+
const delivery = capture();
|
|
2743
|
+
if (Predicate.isPromiseLike(delivery)) Promise.resolve(delivery).catch(() => void 0);
|
|
2744
|
+
} catch {}
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
const module = {
|
|
2748
|
+
failures: postHogFailureObservation(policy),
|
|
2749
|
+
product: postHogProductTelemetry(policy)
|
|
2750
|
+
};
|
|
2751
|
+
HOST_INTERNALS.set(module, {
|
|
2752
|
+
engineeringPolicy: Object.freeze({
|
|
2753
|
+
...options.engineeringDeliveryBudget === void 0 ? {} : { engineeringDeliveryBudget: options.engineeringDeliveryBudget },
|
|
2754
|
+
...options.engineeringEnabled === void 0 ? {} : { engineeringEnabled: options.engineeringEnabled },
|
|
2755
|
+
...options.engineeringPrivacy === void 0 ? {} : { engineeringPrivacy: options.engineeringPrivacy }
|
|
2756
|
+
}),
|
|
2757
|
+
bind: () => {
|
|
2758
|
+
const invocation = snapshot();
|
|
2759
|
+
return {
|
|
2760
|
+
failures: postHogFailureObservation(policy, invocation),
|
|
2761
|
+
product: postHogProductTelemetry(policy, invocation)
|
|
2762
|
+
};
|
|
2763
|
+
},
|
|
2764
|
+
snapshot
|
|
2765
|
+
});
|
|
2766
|
+
return module;
|
|
2767
|
+
}
|
|
2768
|
+
/** @internal Bind both projections to one call-start decision/context snapshot. */
|
|
2769
|
+
function bindHostObservabilityInvocation(observability) {
|
|
2770
|
+
if (observability === void 0) return void 0;
|
|
2771
|
+
return HOST_INTERNALS.get(observability)?.bind() ?? observability;
|
|
2772
|
+
}
|
|
2773
|
+
/** @internal Read one call-start snapshot for actor/transition carriage. */
|
|
2774
|
+
function snapshotHostObservability(observability) {
|
|
2775
|
+
const snapshot = observability === void 0 ? void 0 : HOST_INTERNALS.get(observability)?.snapshot();
|
|
2776
|
+
return snapshot === void 0 ? void 0 : {
|
|
2777
|
+
active: snapshot.active,
|
|
2778
|
+
...snapshot.context === void 0 ? {} : { context: snapshot.context }
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
/** @internal Carry the explicit host engineering policy into production composition. */
|
|
2782
|
+
function hostEngineeringPolicy(observability) {
|
|
2783
|
+
const policy = observability === void 0 ? void 0 : HOST_INTERNALS.get(observability)?.engineeringPolicy;
|
|
2784
|
+
return {
|
|
2785
|
+
...policy?.engineeringDeliveryBudget === void 0 ? {} : { deliveryBudget: policy.engineeringDeliveryBudget },
|
|
2786
|
+
...policy?.engineeringEnabled === void 0 ? {} : { enabled: policy.engineeringEnabled },
|
|
2787
|
+
...policy?.engineeringPrivacy === void 0 ? {} : { privacy: policy.engineeringPrivacy }
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
//#endregion
|
|
2791
|
+
//#region ../observability/src/delivery/admission.ts
|
|
2792
|
+
function newDeliveryState(budget, ownsMetrics) {
|
|
2793
|
+
return {
|
|
2794
|
+
budget,
|
|
2795
|
+
ownsMetrics,
|
|
2796
|
+
closed: Deferred.makeUnsafe(),
|
|
2797
|
+
isClosed: false,
|
|
2798
|
+
metricsRunning: false,
|
|
2799
|
+
logs: [],
|
|
2800
|
+
spans: /* @__PURE__ */ new Map(),
|
|
2801
|
+
terminalBodies: /* @__PURE__ */ new WeakSet(),
|
|
2802
|
+
bodyLeases: /* @__PURE__ */ new WeakMap(),
|
|
2803
|
+
pendingBodies: /* @__PURE__ */ new Set()
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
/** Hold the records a body carries until its request finishes or the body is discarded. */
|
|
2807
|
+
function holdBody(state, body, releases) {
|
|
2808
|
+
if (releases.length === 0) return;
|
|
2809
|
+
const lease = {
|
|
2810
|
+
started: false,
|
|
2811
|
+
release: () => {
|
|
2812
|
+
if (!state.pendingBodies.delete(lease)) return;
|
|
2813
|
+
state.bodyLeases.delete(body);
|
|
2814
|
+
for (const release of releases) release();
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
state.bodyLeases.set(body, lease);
|
|
2818
|
+
state.pendingBodies.add(lease);
|
|
2819
|
+
}
|
|
2820
|
+
const discardBody = (state, body) => state.bodyLeases.get(body)?.release();
|
|
2821
|
+
const drainLogs = (state) => {
|
|
2822
|
+
for (const release of state.logs.splice(0)) release();
|
|
2823
|
+
};
|
|
2824
|
+
function closeDelivery(state) {
|
|
2825
|
+
state.isClosed = true;
|
|
2826
|
+
Deferred.doneUnsafe(state.closed, Effect.void);
|
|
2827
|
+
for (const lease of state.pendingBodies) if (!lease.started) lease.release();
|
|
2828
|
+
drainLogs(state);
|
|
2829
|
+
for (const release of state.spans.values()) release();
|
|
2830
|
+
state.spans.clear();
|
|
2831
|
+
if (state.ownsMetrics) state.budget.metricRegistry.clear();
|
|
2832
|
+
}
|
|
2833
|
+
const text = (value) => typeof value === "string" ? value.slice(0, 128) : "capxul.engineering";
|
|
2834
|
+
const attributes = (input, limit = 32) => {
|
|
2835
|
+
const out = {};
|
|
2836
|
+
let count = 0;
|
|
2837
|
+
let bytes = 0;
|
|
2838
|
+
try {
|
|
2839
|
+
for (const key in input) {
|
|
2840
|
+
if (count++ >= limit) break;
|
|
2841
|
+
if (key.length > 64) continue;
|
|
2842
|
+
const value = Object.getOwnPropertyDescriptor(input, key)?.value;
|
|
2843
|
+
const scalar = typeof value === "string" ? value.slice(0, 128) : typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2844
|
+
if (scalar === void 0) continue;
|
|
2845
|
+
bytes += new TextEncoder().encode(JSON.stringify([key, scalar])).length;
|
|
2846
|
+
if (bytes > (limit >= 24 ? 2e3 : 300)) break;
|
|
2847
|
+
out[key] = scalar;
|
|
2848
|
+
}
|
|
2849
|
+
} catch {}
|
|
2850
|
+
return out;
|
|
2851
|
+
};
|
|
2852
|
+
const terminalName = (name) => name === "cli.command.completed" || name === "cli.wallet.step" || CLI_COMMAND_NAMES.some((command) => name === `cli.command.${command}`);
|
|
2853
|
+
//#endregion
|
|
2854
|
+
//#region ../observability/src/delivery/records.ts
|
|
2855
|
+
function admittedLogger(state, delegate) {
|
|
2856
|
+
return Logger.make((options) => {
|
|
2857
|
+
if (state.isClosed) return;
|
|
2858
|
+
let release;
|
|
2859
|
+
try {
|
|
2860
|
+
const message = text(Array.isArray(options.message) ? Object.getOwnPropertyDescriptor(options.message, "0")?.value : options.message);
|
|
2861
|
+
release = state.budget.admitRecord(terminalName(message));
|
|
2862
|
+
if (release === void 0) return;
|
|
2863
|
+
const context = Context.add(options.fiber.context, References.CurrentLogAnnotations, attributes(options.fiber.getRef(References.CurrentLogAnnotations)));
|
|
2864
|
+
const fiber = Object.create(options.fiber);
|
|
2865
|
+
Object.defineProperty(fiber, "getRef", { value: (ref) => Context.get(context, ref) });
|
|
2866
|
+
state.logs.push(release);
|
|
2867
|
+
delegate.log({
|
|
2868
|
+
...options,
|
|
2869
|
+
message,
|
|
2870
|
+
cause: Cause.empty,
|
|
2871
|
+
fiber
|
|
2872
|
+
});
|
|
2873
|
+
} catch {
|
|
2874
|
+
if (release !== void 0) {
|
|
2875
|
+
const index = state.logs.indexOf(release);
|
|
2876
|
+
if (index !== -1) state.logs.splice(index, 1);
|
|
2877
|
+
release();
|
|
2878
|
+
}
|
|
2879
|
+
state.budget.drop();
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
function admittedTracer(state, delegate) {
|
|
2884
|
+
return Tracer.make({
|
|
2885
|
+
span(options) {
|
|
2886
|
+
const name = text(options.name);
|
|
2887
|
+
const release = state.isClosed ? void 0 : state.budget.admitRecord(terminalName(name));
|
|
2888
|
+
const spanOptions = {
|
|
2889
|
+
...options,
|
|
2890
|
+
name,
|
|
2891
|
+
annotations: Context.empty(),
|
|
2892
|
+
links: []
|
|
2893
|
+
};
|
|
2894
|
+
const span = release === void 0 ? new Tracer.NativeSpan({
|
|
2895
|
+
...spanOptions,
|
|
2896
|
+
sampled: false
|
|
2897
|
+
}) : delegate.span(spanOptions);
|
|
2898
|
+
if (release !== void 0) state.spans.set(span.spanId, release);
|
|
2899
|
+
const wrapped = boundedSpan(state, span);
|
|
2900
|
+
wrapped.addLinks(options.links ?? []);
|
|
2901
|
+
return wrapped;
|
|
2902
|
+
},
|
|
2903
|
+
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
/** The span as a producer sees it: at most 32 fields and 2000 attribute bytes, 2 events, 2 links. */
|
|
2907
|
+
function boundedSpan(state, span) {
|
|
2908
|
+
const { budget } = state;
|
|
2909
|
+
const wrapped = Object.create(span);
|
|
2910
|
+
let fields = 0, events = 0, links = 0, attributeBytes = 0;
|
|
2911
|
+
let ended = false;
|
|
2912
|
+
Object.defineProperties(wrapped, {
|
|
2913
|
+
attribute: { value: (key, value) => {
|
|
2914
|
+
if (fields >= 32 && !span.attributes.has(key)) {
|
|
2915
|
+
budget.drop();
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
const safe = attributes({ [key]: value }, 1);
|
|
2919
|
+
if (!Object.hasOwn(safe, key)) return;
|
|
2920
|
+
const previous = span.attributes.has(key) ? new TextEncoder().encode(JSON.stringify([key, span.attributes.get(key)])).length : 0;
|
|
2921
|
+
const next = new TextEncoder().encode(JSON.stringify([key, safe[key]])).length;
|
|
2922
|
+
if (attributeBytes - previous + next > 2e3) {
|
|
2923
|
+
budget.drop();
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
attributeBytes += next - previous;
|
|
2927
|
+
if (!span.attributes.has(key)) fields += 1;
|
|
2928
|
+
span.attribute(key, safe[key]);
|
|
2929
|
+
} },
|
|
2930
|
+
event: { value: (eventName, time, attrs) => {
|
|
2931
|
+
if (events++ >= 2) {
|
|
2932
|
+
budget.drop();
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
span.event(text(eventName), time, attributes(attrs ?? {}, 4));
|
|
2936
|
+
} },
|
|
2937
|
+
addLinks: { value: (items) => {
|
|
2938
|
+
for (const item of items.slice(0, Math.max(0, 2 - links))) {
|
|
2939
|
+
if (!/^[a-f0-9]{32}$/u.test(item.span.traceId) || !/^[a-f0-9]{16}$/u.test(item.span.spanId)) {
|
|
2940
|
+
budget.drop();
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
span.addLinks([{
|
|
2944
|
+
span: Tracer.externalSpan({
|
|
2945
|
+
traceId: item.span.traceId,
|
|
2946
|
+
spanId: item.span.spanId,
|
|
2947
|
+
sampled: item.span.sampled
|
|
2948
|
+
}),
|
|
2949
|
+
attributes: attributes(item.attributes, 4)
|
|
2950
|
+
}]);
|
|
2951
|
+
links += 1;
|
|
2952
|
+
}
|
|
2953
|
+
} },
|
|
2954
|
+
end: { value: (time, exit) => {
|
|
2955
|
+
if (ended) return;
|
|
2956
|
+
ended = true;
|
|
2957
|
+
span.end(time, Exit.isFailure(exit) ? Exit.fail("CLI operation failed") : Exit.void);
|
|
2958
|
+
} }
|
|
2959
|
+
});
|
|
2960
|
+
return wrapped;
|
|
2961
|
+
}
|
|
2962
|
+
//#endregion
|
|
2963
|
+
//#region ../observability/src/delivery/budget.ts
|
|
2964
|
+
/** Fixed admission limits for one CLI invocation and its SDK client. */
|
|
2965
|
+
const CLI_ENGINEERING_LIMITS = Object.freeze({
|
|
2966
|
+
records: 80,
|
|
2967
|
+
ordinaryRecords: 64,
|
|
2968
|
+
recordBytes: 16384,
|
|
2969
|
+
requests: 5,
|
|
2970
|
+
ordinaryRequests: 2,
|
|
2971
|
+
requestBytes: 1572864,
|
|
2972
|
+
metricSeries: 16,
|
|
2973
|
+
requestTimeoutMs: 1e3
|
|
2974
|
+
});
|
|
2975
|
+
//#endregion
|
|
2976
|
+
//#region ../observability/src/delivery/requests.ts
|
|
2977
|
+
function admittedSerialization(state, delegate) {
|
|
2978
|
+
return {
|
|
2979
|
+
logs(data) {
|
|
2980
|
+
const releases = state.logs.splice(0);
|
|
2981
|
+
try {
|
|
2982
|
+
const body = delegate.logs(data);
|
|
2983
|
+
if (data.resourceLogs.some((r) => r.scopeLogs.some((s) => s.logRecords?.some((l) => terminalName(l.body?.stringValue ?? ""))))) state.terminalBodies.add(body);
|
|
2984
|
+
holdBody(state, body, releases);
|
|
2985
|
+
return body;
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
for (const release of releases) release();
|
|
2988
|
+
throw error;
|
|
2989
|
+
}
|
|
2990
|
+
},
|
|
2991
|
+
traces(data) {
|
|
2992
|
+
const releases = [];
|
|
2993
|
+
for (const resource of data.resourceSpans) for (const scope of resource.scopeSpans) for (const span of scope.spans) {
|
|
2994
|
+
const release = state.spans.get(span.spanId);
|
|
2995
|
+
if (release !== void 0) releases.push(release);
|
|
2996
|
+
state.spans.delete(span.spanId);
|
|
2997
|
+
}
|
|
2998
|
+
try {
|
|
2999
|
+
const body = delegate.traces(data);
|
|
3000
|
+
if (data.resourceSpans.some((r) => r.scopeSpans.some((s) => s.spans.some((span) => terminalName(span.name))))) state.terminalBodies.add(body);
|
|
3001
|
+
holdBody(state, body, releases);
|
|
3002
|
+
return body;
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
for (const release of releases) release();
|
|
3005
|
+
throw error;
|
|
3006
|
+
}
|
|
3007
|
+
},
|
|
3008
|
+
metrics(data) {
|
|
3009
|
+
const body = delegate.metrics(data);
|
|
3010
|
+
state.terminalBodies.add(body);
|
|
3011
|
+
return body;
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
/** A 429's Retry-After, bounded to two seconds; any other failure retries at once. */
|
|
3016
|
+
const retryAfter = Schedule.spaced("100 millis").pipe(Schedule.addDelay(({ input }) => Effect.map(Clock.currentTimeMillis, (now) => {
|
|
3017
|
+
if (!HttpClientError.isHttpClientError(input) || input.reason._tag !== "StatusCodeError" || input.reason.response.status !== 429) return Duration.zero;
|
|
3018
|
+
const header = input.reason.response.headers["retry-after"];
|
|
3019
|
+
const numeric = Number(header);
|
|
3020
|
+
const delay = header === void 0 ? 2e3 : Number.isFinite(numeric) ? numeric * 1e3 : Date.parse(header) - now;
|
|
3021
|
+
return Duration.millis(Number.isFinite(delay) ? Math.min(2e3, Math.max(0, delay)) : 2e3);
|
|
3022
|
+
})));
|
|
3023
|
+
function admittedTransport(state, client, enabled = () => true) {
|
|
3024
|
+
const { budget } = state;
|
|
3025
|
+
const retrying = HttpClient.transform(client, (response, request) => Effect.suspend(() => {
|
|
3026
|
+
if (enabled()) return response;
|
|
3027
|
+
budget.drop();
|
|
3028
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
3029
|
+
})).pipe(HttpClient.transformResponse((response) => Effect.tap(response, (result) => {
|
|
3030
|
+
let received = 0;
|
|
3031
|
+
return Stream.runForEach(result.stream, (chunk) => Effect.sync(() => {
|
|
3032
|
+
received += chunk.byteLength;
|
|
3033
|
+
if (received > 65536) throw new Error("Engineering response limit exceeded");
|
|
3034
|
+
})).pipe(Effect.catchReason("HttpClientError", "EmptyBodyError", () => Effect.void));
|
|
3035
|
+
}))).pipe(HttpClient.filterStatusOk, HttpClient.retryTransient({
|
|
3036
|
+
times: 1,
|
|
3037
|
+
schedule: retryAfter
|
|
3038
|
+
}));
|
|
3039
|
+
return HttpClient.transform(retrying, (response, request) => Effect.suspend(() => {
|
|
3040
|
+
if (state.isClosed) {
|
|
3041
|
+
discardBody(state, request.body);
|
|
3042
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null)));
|
|
3043
|
+
}
|
|
3044
|
+
const droppedResponse = () => HttpClientResponse.fromWeb(request, new Response(null, { status: 200 }));
|
|
3045
|
+
if (request.body._tag === "Empty") {
|
|
3046
|
+
discardBody(state, request.body);
|
|
3047
|
+
return Effect.succeed(droppedResponse());
|
|
3048
|
+
}
|
|
3049
|
+
const bytes = request.body.contentLength;
|
|
3050
|
+
const release = budget.admitRequest(bytes ?? Number.POSITIVE_INFINITY, state.terminalBodies.has(request.body));
|
|
3051
|
+
if (release === void 0) {
|
|
3052
|
+
discardBody(state, request.body);
|
|
3053
|
+
return Effect.succeed(droppedResponse());
|
|
3054
|
+
}
|
|
3055
|
+
const lease = state.bodyLeases.get(request.body);
|
|
3056
|
+
if (lease !== void 0) lease.started = true;
|
|
3057
|
+
return response.pipe(Effect.asVoid, Effect.timeoutOption(CLI_ENGINEERING_LIMITS.requestTimeoutMs), Effect.raceFirst(Deferred.await(state.closed).pipe(Effect.andThen(Effect.interrupt))), Effect.tap((result) => Effect.sync(() => {
|
|
3058
|
+
if (Option.isNone(result)) budget.drop();
|
|
3059
|
+
})), Effect.catchCause(() => Effect.sync(() => {
|
|
3060
|
+
budget.drop();
|
|
3061
|
+
})), Effect.as(droppedResponse()), Effect.ensuring(Effect.sync(() => {
|
|
3062
|
+
release();
|
|
3063
|
+
discardBody(state, request.body);
|
|
3064
|
+
})));
|
|
3065
|
+
}));
|
|
3066
|
+
}
|
|
3067
|
+
//#endregion
|
|
3068
|
+
//#region ../observability/src/engineering-delivery.ts
|
|
3069
|
+
/** Native exporters retain their serialization and scheduling; this owns admission only. */
|
|
3070
|
+
function cliEngineeringDelivery(budget, ownsMetrics) {
|
|
3071
|
+
const state = newDeliveryState(budget, ownsMetrics);
|
|
3072
|
+
return {
|
|
3073
|
+
close: () => closeDelivery(state),
|
|
3074
|
+
discardBody: (body) => discardBody(state, body),
|
|
3075
|
+
metrics(run) {
|
|
3076
|
+
return Effect.suspend(() => {
|
|
3077
|
+
if (state.isClosed || state.metricsRunning) return Effect.void;
|
|
3078
|
+
state.metricsRunning = true;
|
|
3079
|
+
return run.pipe(Effect.ensuring(Effect.sync(() => {
|
|
3080
|
+
state.metricsRunning = false;
|
|
3081
|
+
})));
|
|
3082
|
+
});
|
|
3083
|
+
},
|
|
3084
|
+
logger: (delegate) => admittedLogger(state, delegate),
|
|
3085
|
+
tracer: (delegate) => admittedTracer(state, delegate),
|
|
3086
|
+
serialization: (delegate) => admittedSerialization(state, delegate),
|
|
3087
|
+
transport: (client, enabled) => admittedTransport(state, client, enabled)
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
//#endregion
|
|
3091
|
+
//#region ../observability/src/engineering/config.ts
|
|
3092
|
+
const ENGINEERING_CAPXUL_ENVS = [
|
|
3093
|
+
"development",
|
|
3094
|
+
"staging",
|
|
3095
|
+
"production",
|
|
3096
|
+
"local"
|
|
3097
|
+
];
|
|
3098
|
+
const traceHeaderFilter = (_name) => true;
|
|
3099
|
+
const credentialHeaderPattern = Object.assign(/./u, { exec: (name) => isCredentialField(name, "header") ? Object.assign([name], {
|
|
3100
|
+
index: 0,
|
|
3101
|
+
input: name
|
|
3102
|
+
}) : null });
|
|
3103
|
+
const postHogOtlpEndpoints = (host) => {
|
|
3104
|
+
const base = host.replace(/\/+$/, "");
|
|
3105
|
+
return {
|
|
3106
|
+
logs: `${base}/i/v1/logs`,
|
|
3107
|
+
traces: `${base}/i/v1/traces`,
|
|
3108
|
+
metrics: `${base}/i/v1/metrics`
|
|
3109
|
+
};
|
|
3110
|
+
};
|
|
3111
|
+
const ENGINEERING_CAPXUL_ENV_SET = new Set(ENGINEERING_CAPXUL_ENVS);
|
|
3112
|
+
const ENGINEERING_PRODUCERS = /* @__PURE__ */ new Set(["browser", "server"]);
|
|
3113
|
+
const PUBLIC_POSTHOG_AUTHORIZATION = /^Bearer phc_[A-Za-z0-9_-]{1,191}$/u;
|
|
3114
|
+
const SAFE_RESOURCE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._+@/-]{0,127}$/u;
|
|
3115
|
+
function validateEngineeringPrivacy(config) {
|
|
3116
|
+
if (config.privacy !== void 0 && config.privacy !== "minimal") throw new TypeError("engineering telemetry privacy must be minimal when specified");
|
|
3117
|
+
if (config.privacy === "minimal" && (!/^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(config.sdkVersion) || config.serviceName !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(config.serviceName))) throw new TypeError("minimal engineering resource values must not contain paths");
|
|
3118
|
+
}
|
|
3119
|
+
const validateEngineeringTelemetryConfig = (config) => {
|
|
3120
|
+
let url;
|
|
3121
|
+
try {
|
|
3122
|
+
url = new URL(config.host);
|
|
3123
|
+
} catch {
|
|
3124
|
+
throw new TypeError("engineering telemetry host must be an absolute HTTPS URL");
|
|
3125
|
+
}
|
|
3126
|
+
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");
|
|
3127
|
+
if (!ENGINEERING_CAPXUL_ENV_SET.has(config.capxulEnv)) throw new TypeError("engineering telemetry capxulEnv is not canonical");
|
|
3128
|
+
if (!ENGINEERING_PRODUCERS.has(config.producer)) throw new TypeError("engineering telemetry producer is not canonical");
|
|
3129
|
+
if (config.enabled !== void 0 && typeof config.enabled !== "boolean" && typeof config.enabled !== "function") throw new TypeError("engineering telemetry enabled must be a boolean or callback");
|
|
3130
|
+
validateEngineeringPrivacy(config);
|
|
3131
|
+
if (!SAFE_RESOURCE_VALUE.test(config.sdkVersion)) throw new TypeError("engineering telemetry sdkVersion must be a bounded safe value");
|
|
3132
|
+
if (config.serviceName !== void 0 && !SAFE_RESOURCE_VALUE.test(config.serviceName)) throw new TypeError("engineering telemetry serviceName must be a bounded safe value");
|
|
3133
|
+
const headerEntries = Object.entries(config.headers);
|
|
3134
|
+
if (headerEntries.length !== 1 || headerEntries[0]?.[0].toLowerCase() !== "authorization" || !PUBLIC_POSTHOG_AUTHORIZATION.test(headerEntries[0]?.[1] ?? "")) throw new TypeError("engineering telemetry headers must contain exactly one public PostHog authorization token");
|
|
3135
|
+
return {
|
|
3136
|
+
...config,
|
|
3137
|
+
host: url.origin,
|
|
3138
|
+
headers: Object.freeze({ authorization: headerEntries[0][1] })
|
|
3139
|
+
};
|
|
3140
|
+
};
|
|
3141
|
+
//#endregion
|
|
3142
|
+
//#region ../observability/src/engineering/tracer.ts
|
|
3143
|
+
const failureText = (failure, key, limit, source) => {
|
|
3144
|
+
if (failure === null || typeof failure !== "object") return void 0;
|
|
3145
|
+
try {
|
|
3146
|
+
let owner = failure;
|
|
3147
|
+
let descriptor;
|
|
3148
|
+
for (let depth = 0; owner !== null && depth < 4; depth += 1) {
|
|
3149
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, key);
|
|
3150
|
+
if (descriptor !== void 0 || key !== "name") break;
|
|
3151
|
+
owner = Object.getPrototypeOf(owner);
|
|
3152
|
+
}
|
|
3153
|
+
const value = descriptor?.value;
|
|
3154
|
+
return typeof value === "string" || key === "code" && typeof value === "number" ? redactSecrets(String(value), source).slice(0, limit) : void 0;
|
|
3155
|
+
} catch {
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
const exportFailure = (failure, source) => {
|
|
3160
|
+
const message = failureText(failure, "message", 2048, source) ?? (typeof failure === "string" ? redactSecrets(failure, source).slice(0, 2048) : "Engineering operation failed");
|
|
3161
|
+
const code = failureText(failure, "code", 128);
|
|
3162
|
+
const error = new Error((code === void 0 ? message : `${message} [code=${code}]`).slice(0, 2048));
|
|
3163
|
+
error.name = failureText(failure, "_tag", 128) ?? failureText(failure, "name", 128) ?? "Error";
|
|
3164
|
+
error.stack = failureText(failure, "stack", 8192, source) ?? `${error.name}: ${error.message}`;
|
|
3165
|
+
return error;
|
|
3166
|
+
};
|
|
3167
|
+
const exportFailureExit = (cause, source) => Exit.failCause(Cause.fromReasons(cause.reasons.filter((reason) => !Cause.isInterruptReason(reason)).slice(0, 8).map((reason) => Cause.isFailReason(reason) ? Cause.makeFailReason(exportFailure(reason.error, source)) : Cause.makeDieReason(exportFailure(reason.defect, source)))));
|
|
3168
|
+
const BROWSER_KEEPALIVE_BYTES = 65536;
|
|
3169
|
+
let unfinishedBrowserExportBytes = 0;
|
|
3170
|
+
const hasBrowserDocument = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
3171
|
+
const browserExportClient = (client) => HttpClient.transform(client, (response, request) => Effect.acquireUseRelease(Effect.sync(() => {
|
|
3172
|
+
const bytes = "contentLength" in request.body ? request.body.contentLength : void 0;
|
|
3173
|
+
if (request.body._tag === "Stream" || request.body._tag === "FormData" || bytes === void 0 || !Number.isSafeInteger(bytes) || bytes < 0 || unfinishedBrowserExportBytes + bytes > BROWSER_KEEPALIVE_BYTES) return;
|
|
3174
|
+
unfinishedBrowserExportBytes += bytes;
|
|
3175
|
+
return bytes;
|
|
3176
|
+
}), (bytes) => response.pipe(Effect.provideService(FetchHttpClient.RequestInit, { keepalive: bytes !== void 0 }), Effect.tap((result) => bytes === void 0 ? Effect.void : Stream.runDrain(result.stream).pipe(Effect.catchIf((error) => error.reason._tag === "EmptyBodyError", () => Effect.void)))), (bytes) => Effect.sync(() => {
|
|
3177
|
+
if (bytes !== void 0) unfinishedBrowserExportBytes -= bytes;
|
|
3178
|
+
})));
|
|
3179
|
+
const spanUpstream = (span) => {
|
|
3180
|
+
let current = span;
|
|
3181
|
+
while (current?._tag === "Span") {
|
|
3182
|
+
const upstream = current.attributes.get("chain.upstream");
|
|
3183
|
+
if (upstream === "alchemy" || upstream === "infura" || upstream === "gateway") return upstream;
|
|
3184
|
+
current = current.parent._tag === "Some" ? current.parent.value : void 0;
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
3187
|
+
function redactHttpPath(path, fullUrl, source) {
|
|
3188
|
+
let origin = "https://redaction.invalid";
|
|
3189
|
+
try {
|
|
3190
|
+
const url = new URL(String(fullUrl));
|
|
3191
|
+
if (url.protocol === "https:" || url.protocol === "http:") origin = url.origin;
|
|
3192
|
+
} catch {}
|
|
3193
|
+
const prefix = path.startsWith("/") ? origin : "";
|
|
3194
|
+
return redactUrlSecrets(`${prefix}${path}`, source).slice(prefix.length, prefix.length + 2048);
|
|
3195
|
+
}
|
|
3196
|
+
/** A span whose prototype is `span`: it answers every Span member until an own property overrides it. */
|
|
3197
|
+
function inheritSpan(span) {
|
|
3198
|
+
return Object.create(span);
|
|
3199
|
+
}
|
|
3200
|
+
/** Preserve domain exits while preventing the OTLP serializer from seeing raw causes. */
|
|
3201
|
+
const makeLeakSafeEngineeringTracer = (delegate, enabled = () => true) => Tracer.make({
|
|
3202
|
+
span(options) {
|
|
3203
|
+
const span = enabled() ? delegate.span(options) : new Tracer.NativeSpan(options);
|
|
3204
|
+
const wrapped = inheritSpan(span);
|
|
3205
|
+
Object.defineProperty(wrapped, "attribute", { value: (name, value) => {
|
|
3206
|
+
const source = spanUpstream(span);
|
|
3207
|
+
const header = /^http\.(?:request|response)\.header\.(.+)$/u.exec(name)?.[1];
|
|
3208
|
+
if (header !== void 0 && isCredentialField(header, "header")) span.attribute(name, "[REDACTED]");
|
|
3209
|
+
else if (typeof value === "string" && name === "url.query") span.attribute(name, redactUrlSecrets(`?${value}`).slice(1, 2049));
|
|
3210
|
+
else if (typeof value === "string" && (name === "url.path" || name === "http.target")) {
|
|
3211
|
+
const fullUrl = span.attributes.get("url.full") ?? span.attributes.get("http.url");
|
|
3212
|
+
const path = name === "url.path" && typeof fullUrl === "string" ? /^https?:\/\/[^/]+([^?#]*)/u.exec(fullUrl)?.[1] ?? value : value;
|
|
3213
|
+
span.attribute(name, redactHttpPath(path, fullUrl, source));
|
|
3214
|
+
} else if (typeof value === "string" && (name === "url.full" || name === "http.url" || header === "location" || header === "referer" || header === "referrer")) span.attribute(name, redactUrlSecrets(value, source).slice(0, 2048));
|
|
3215
|
+
else if (header !== void 0 && typeof value === "string") span.attribute(name, redactSecrets(value, source).slice(0, 2048));
|
|
3216
|
+
else span.attribute(name, value);
|
|
3217
|
+
} });
|
|
3218
|
+
Object.defineProperty(wrapped, "end", {
|
|
3219
|
+
configurable: false,
|
|
3220
|
+
enumerable: false,
|
|
3221
|
+
value: (endTime, exit) => {
|
|
3222
|
+
if (!enabled()) {
|
|
3223
|
+
Object.defineProperty(wrapped, "status", { value: {
|
|
3224
|
+
_tag: "Ended",
|
|
3225
|
+
startTime: span.status.startTime,
|
|
3226
|
+
endTime,
|
|
3227
|
+
exit
|
|
3228
|
+
} });
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
3232
|
+
const first = exit.cause.reasons.find((reason) => !Cause.isInterruptReason(reason));
|
|
3233
|
+
const code = failureText(first === void 0 ? void 0 : Cause.isFailReason(first) ? first.error : first.defect, "code", 128);
|
|
3234
|
+
if (code !== void 0) span.attribute("error.code", code);
|
|
3235
|
+
span.end(endTime, exportFailureExit(exit.cause, spanUpstream(span)));
|
|
3236
|
+
} else span.end(endTime, exit);
|
|
3237
|
+
},
|
|
3238
|
+
writable: false
|
|
3239
|
+
});
|
|
3240
|
+
return wrapped;
|
|
3241
|
+
},
|
|
3242
|
+
...delegate.context === void 0 ? {} : { context: delegate.context.bind(delegate) }
|
|
3243
|
+
});
|
|
3244
|
+
//#endregion
|
|
3245
|
+
//#region ../observability/src/engineering/minimal.ts
|
|
3246
|
+
const MINIMAL_LOG_NAMES = /* @__PURE__ */ new Set([
|
|
3247
|
+
"capxul_sdk_operation",
|
|
3248
|
+
"identity.transition",
|
|
3249
|
+
"convex.connection.changed",
|
|
3250
|
+
"convex.action.connection_lost",
|
|
3251
|
+
"cli.command.completed",
|
|
3252
|
+
"cli.sdk.operation.failed",
|
|
3253
|
+
"cli.signer.wait",
|
|
3254
|
+
"cli.wallet.step"
|
|
3255
|
+
]);
|
|
3256
|
+
const MINIMAL_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3257
|
+
...CAPXUL_ERROR_CODES,
|
|
3258
|
+
"CLI_USAGE",
|
|
3259
|
+
"CLI_CONFIG",
|
|
3260
|
+
"CLI_STORAGE",
|
|
3261
|
+
"CLI_DEFECT",
|
|
3262
|
+
"CLI_DEADLINE"
|
|
3263
|
+
]);
|
|
3264
|
+
const MINIMAL_NUMBERS = /* @__PURE__ */ new Set([
|
|
3265
|
+
"schema_version",
|
|
3266
|
+
"duration_ms",
|
|
3267
|
+
"wait_duration_ms",
|
|
3268
|
+
"retry_count",
|
|
3269
|
+
"http.status_code",
|
|
3270
|
+
"http.response.status_code",
|
|
3271
|
+
"http_status",
|
|
3272
|
+
"rpc.code",
|
|
3273
|
+
"connection_count",
|
|
3274
|
+
"connection_retries",
|
|
3275
|
+
"elapsed_ms",
|
|
3276
|
+
"total_ms",
|
|
3277
|
+
"token_calls",
|
|
3278
|
+
"call",
|
|
3279
|
+
"attempt",
|
|
3280
|
+
"bridge_protocol_version",
|
|
3281
|
+
"httpStatus"
|
|
3282
|
+
]);
|
|
3283
|
+
const MINIMAL_CORRELATIONS = /* @__PURE__ */ new Set([
|
|
3284
|
+
"correlation_id",
|
|
3285
|
+
"journey_id",
|
|
3286
|
+
"invocation_id",
|
|
3287
|
+
"posthogDistinctId"
|
|
3288
|
+
]);
|
|
3289
|
+
const MINIMAL_METRIC_LABELS = /* @__PURE__ */ new Set([
|
|
3290
|
+
"command",
|
|
3291
|
+
"capability",
|
|
3292
|
+
"invocation_kind",
|
|
3293
|
+
"outcome",
|
|
3294
|
+
"capxul_env",
|
|
3295
|
+
"unit"
|
|
3296
|
+
]);
|
|
3297
|
+
const MINIMAL_METRIC_NAMES = /* @__PURE__ */ new Set(["capxul.cli.command.count", "capxul.cli.command.duration"]);
|
|
3298
|
+
const traceId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{32}$/u.test(value);
|
|
3299
|
+
const spanId = (value) => typeof value === "string" && /^(?!0+$)[0-9a-f]{16}$/u.test(value);
|
|
3300
|
+
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";
|
|
3301
|
+
const MINIMAL_ENUM_VALUES = /* @__PURE__ */ new Map([
|
|
3302
|
+
["outcome", [
|
|
3303
|
+
"succeeded",
|
|
3304
|
+
"refused",
|
|
3305
|
+
"failed",
|
|
3306
|
+
"cancelled",
|
|
3307
|
+
"applied"
|
|
3308
|
+
]],
|
|
3309
|
+
["mode", FAILURE_MODES],
|
|
3310
|
+
["failure_mode", FAILURE_MODES],
|
|
3311
|
+
["capxul_env", ENGINEERING_CAPXUL_ENVS],
|
|
3312
|
+
["producer", ["browser", "server"]],
|
|
3313
|
+
["command", CLI_COMMAND_NAMES],
|
|
3314
|
+
["capability", CLI_CAPABILITIES],
|
|
3315
|
+
["invocation_kind", CLI_INVOCATION_KINDS],
|
|
3316
|
+
["diagnostic_scope", [
|
|
3317
|
+
"openfort.step",
|
|
3318
|
+
"openfort.token",
|
|
3319
|
+
"openfort.encryptionSession",
|
|
3320
|
+
"openfort.embeddedState",
|
|
3321
|
+
"openfort.storageCleared",
|
|
3322
|
+
"openfort.configure",
|
|
3323
|
+
"openfort.get",
|
|
3324
|
+
"openfort.address",
|
|
3325
|
+
"openfort.staleCacheHeal",
|
|
3326
|
+
"openfort.signerRecovery",
|
|
3327
|
+
"openfort.signerStatusListener"
|
|
3328
|
+
]],
|
|
3329
|
+
["step", [
|
|
3330
|
+
"initialize",
|
|
3331
|
+
"token",
|
|
3332
|
+
"encryptionSession",
|
|
3333
|
+
"embeddedState",
|
|
3334
|
+
"configure",
|
|
3335
|
+
"get",
|
|
3336
|
+
"healStaleCache",
|
|
3337
|
+
"walletReady"
|
|
3338
|
+
]],
|
|
3339
|
+
["provider", ["better-auth", "oidc"]],
|
|
3340
|
+
["cli_identity_kind", ["anonymous", "identified"]],
|
|
3341
|
+
["output_mode", ["human", "json"]],
|
|
3342
|
+
["setup_status", ORGANIZATION_SETUP_STATUSES],
|
|
3343
|
+
["setup_reason", ORGANIZATION_SETUP_REASONS],
|
|
3344
|
+
["signer_stage", [
|
|
3345
|
+
"address",
|
|
3346
|
+
"readiness",
|
|
3347
|
+
"sign"
|
|
3348
|
+
]],
|
|
3349
|
+
["unit", [
|
|
3350
|
+
"1",
|
|
3351
|
+
"s",
|
|
3352
|
+
"ms"
|
|
3353
|
+
]]
|
|
3354
|
+
]);
|
|
3355
|
+
const MINIMAL_CODE_KEYS = /* @__PURE__ */ new Set([
|
|
3356
|
+
"error.code",
|
|
3357
|
+
"error_code",
|
|
3358
|
+
"failure_code",
|
|
3359
|
+
"refusal_code",
|
|
3360
|
+
"capxul_error_code"
|
|
3361
|
+
]);
|
|
3362
|
+
const MINIMAL_VERSION_KEYS = /* @__PURE__ */ new Set([
|
|
3363
|
+
"sdk_version",
|
|
3364
|
+
"cli_version",
|
|
3365
|
+
"release"
|
|
3366
|
+
]);
|
|
3367
|
+
function minimalStringValue(key, text) {
|
|
3368
|
+
if (key === "operation") return isCapxulOperation(text);
|
|
3369
|
+
if (key === "organization_id") return /^org_[0-9A-Za-z]+$/u.test(text);
|
|
3370
|
+
if (MINIMAL_CODE_KEYS.has(key)) return MINIMAL_ERROR_CODES.has(text);
|
|
3371
|
+
if (MINIMAL_VERSION_KEYS.has(key)) return /^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/u.test(text);
|
|
3372
|
+
if (key === "trace_id") return traceId(text);
|
|
3373
|
+
if (key === "span_id" || key === "parent_span_id") return spanId(text);
|
|
3374
|
+
const values = MINIMAL_ENUM_VALUES.get(key);
|
|
3375
|
+
if (values !== void 0) return values.includes(text);
|
|
3376
|
+
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;
|
|
3377
|
+
}
|
|
3378
|
+
/** A named privacy projection, separate from the default credential-only redaction policy. */
|
|
3379
|
+
function minimalAttributes(attributes, metric = false) {
|
|
3380
|
+
return attributes.flatMap(({ key, value }) => {
|
|
3381
|
+
if (metric && !MINIMAL_METRIC_LABELS.has(key)) return [];
|
|
3382
|
+
const text = value.stringValue;
|
|
3383
|
+
const number = value.doubleValue ?? value.intValue;
|
|
3384
|
+
if (MINIMAL_NUMBERS.has(key) && typeof number === "number" && Number.isFinite(number)) return [{
|
|
3385
|
+
key,
|
|
3386
|
+
value: { doubleValue: number }
|
|
3387
|
+
}];
|
|
3388
|
+
if (typeof text !== "string") return [];
|
|
3389
|
+
return minimalStringValue(key, text) ? [{
|
|
3390
|
+
key,
|
|
3391
|
+
value: { stringValue: text }
|
|
3392
|
+
}] : [];
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
//#endregion
|
|
3396
|
+
//#region ../observability/src/engineering/metric-points.ts
|
|
3397
|
+
const finiteProtocolNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3398
|
+
const protocolCount = (value, maximum = Number.MAX_SAFE_INTEGER) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum ? value : void 0;
|
|
3399
|
+
const protocolFlags = (value) => protocolCount(Object.getOwnPropertyDescriptor(value, "flags")?.value, 4294967295);
|
|
3400
|
+
function protocolTime(value) {
|
|
3401
|
+
if (typeof value === "string" && /^\d{1,20}$/u.test(value) && BigInt(value) <= 18446744073709551615n) return value;
|
|
3402
|
+
const number = protocolCount(value);
|
|
3403
|
+
if (number !== void 0) return String(number);
|
|
3404
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
3405
|
+
const low = protocolCount(Object.getOwnPropertyDescriptor(value, "low")?.value, 4294967295);
|
|
3406
|
+
const high = protocolCount(Object.getOwnPropertyDescriptor(value, "high")?.value, 4294967295);
|
|
3407
|
+
return low === void 0 || high === void 0 ? void 0 : (BigInt(high) << 32n | BigInt(low)).toString();
|
|
3408
|
+
}
|
|
3409
|
+
function minimalNumberPoints(points) {
|
|
3410
|
+
return points.flatMap((point) => {
|
|
3411
|
+
const attributes = minimalAttributes(point.attributes, true);
|
|
3412
|
+
const asDouble = finiteProtocolNumber(point.asDouble);
|
|
3413
|
+
const asInt = typeof point.asInt === "number" && Number.isSafeInteger(point.asInt) ? point.asInt : void 0;
|
|
3414
|
+
if (attributes.length !== point.attributes.length || asDouble === void 0 && asInt === void 0) return [];
|
|
3415
|
+
return [{
|
|
3416
|
+
attributes,
|
|
3417
|
+
timeUnixNano: protocolTime(point.timeUnixNano),
|
|
3418
|
+
startTimeUnixNano: protocolTime(point.startTimeUnixNano),
|
|
3419
|
+
asDouble,
|
|
3420
|
+
asInt: asDouble === void 0 ? asInt : void 0,
|
|
3421
|
+
flags: protocolFlags(point)
|
|
3422
|
+
}];
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
function minimalHistogramPoints(points) {
|
|
3426
|
+
return points.flatMap((point) => {
|
|
3427
|
+
const attributes = minimalAttributes(point.attributes ?? [], true);
|
|
3428
|
+
const count = protocolCount(point.count);
|
|
3429
|
+
const bucketCounts = point.bucketCounts;
|
|
3430
|
+
const explicitBounds = point.explicitBounds;
|
|
3431
|
+
if (attributes.length !== (point.attributes?.length ?? 0) || count === void 0 || !Array.isArray(bucketCounts) || !bucketCounts.every((value) => protocolCount(value) !== void 0) || !Array.isArray(explicitBounds) || !explicitBounds.every((value) => finiteProtocolNumber(value) !== void 0) || bucketCounts.length !== explicitBounds.length + 1) return [];
|
|
3432
|
+
return [{
|
|
3433
|
+
attributes,
|
|
3434
|
+
timeUnixNano: protocolTime(point.timeUnixNano),
|
|
3435
|
+
startTimeUnixNano: protocolTime(point.startTimeUnixNano),
|
|
3436
|
+
count,
|
|
3437
|
+
sum: finiteProtocolNumber(point.sum),
|
|
3438
|
+
min: finiteProtocolNumber(point.min),
|
|
3439
|
+
max: finiteProtocolNumber(point.max),
|
|
3440
|
+
bucketCounts: bucketCounts.map((value) => value),
|
|
3441
|
+
explicitBounds: explicitBounds.map((value) => value),
|
|
3442
|
+
flags: protocolFlags(point)
|
|
3443
|
+
}];
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
//#endregion
|
|
3447
|
+
//#region ../observability/src/engineering/serialization.ts
|
|
3448
|
+
function minimalSerialization(config) {
|
|
3449
|
+
const service = config.serviceName ?? "capxul-sdk";
|
|
3450
|
+
const resource = { attributes: Object.entries({
|
|
3451
|
+
"service.name": service,
|
|
3452
|
+
"service.version": config.sdkVersion,
|
|
3453
|
+
sdk_version: config.sdkVersion,
|
|
3454
|
+
capxul_env: config.capxulEnv,
|
|
3455
|
+
producer: config.producer
|
|
3456
|
+
}).map(([key, value]) => ({
|
|
3457
|
+
key,
|
|
3458
|
+
value: { stringValue: value }
|
|
3459
|
+
})) };
|
|
3460
|
+
return {
|
|
3461
|
+
logs: (data) => HttpBody.jsonUnsafe({ resourceLogs: data.resourceLogs.map((entry) => ({
|
|
3462
|
+
resource,
|
|
3463
|
+
scopeLogs: entry.scopeLogs.map((scope) => ({
|
|
3464
|
+
scope: { name: service },
|
|
3465
|
+
logRecords: (scope.logRecords ?? []).map((record) => ({
|
|
3466
|
+
timeUnixNano: protocolTime(record.timeUnixNano),
|
|
3467
|
+
observedTimeUnixNano: protocolTime(record.observedTimeUnixNano),
|
|
3468
|
+
severityNumber: protocolCount(record.severityNumber, 24),
|
|
3469
|
+
severityText: [
|
|
3470
|
+
"All",
|
|
3471
|
+
"Trace",
|
|
3472
|
+
"Debug",
|
|
3473
|
+
"Info",
|
|
3474
|
+
"Warn",
|
|
3475
|
+
"Error",
|
|
3476
|
+
"Fatal",
|
|
3477
|
+
"None"
|
|
3478
|
+
].includes(record.severityText ?? "") ? record.severityText : void 0,
|
|
3479
|
+
body: { stringValue: minimalName(record.body?.stringValue) },
|
|
3480
|
+
attributes: minimalAttributes(record.attributes),
|
|
3481
|
+
droppedAttributesCount: protocolCount(record.droppedAttributesCount) ?? 0,
|
|
3482
|
+
traceId: traceId(record.traceId) ? record.traceId : void 0,
|
|
3483
|
+
spanId: spanId(record.spanId) ? record.spanId : void 0,
|
|
3484
|
+
flags: protocolFlags(record)
|
|
3485
|
+
}))
|
|
3486
|
+
}))
|
|
3487
|
+
})) }),
|
|
3488
|
+
traces: (data) => HttpBody.jsonUnsafe({ resourceSpans: data.resourceSpans.map((entry) => ({
|
|
3489
|
+
resource,
|
|
3490
|
+
scopeSpans: entry.scopeSpans.map((scope) => ({
|
|
3491
|
+
scope: { name: service },
|
|
3492
|
+
spans: scope.spans.filter((span) => traceId(span.traceId) && spanId(span.spanId)).map((span) => {
|
|
3493
|
+
const attributes = minimalAttributes(span.attributes);
|
|
3494
|
+
const code = attributes.find((attribute) => attribute.key === "error.code");
|
|
3495
|
+
return {
|
|
3496
|
+
traceId: span.traceId,
|
|
3497
|
+
spanId: span.spanId,
|
|
3498
|
+
name: minimalName(span.name),
|
|
3499
|
+
kind: protocolCount(span.kind, 5) ?? 0,
|
|
3500
|
+
startTimeUnixNano: protocolTime(span.startTimeUnixNano),
|
|
3501
|
+
endTimeUnixNano: protocolTime(span.endTimeUnixNano),
|
|
3502
|
+
attributes,
|
|
3503
|
+
droppedAttributesCount: protocolCount(span.droppedAttributesCount) ?? 0,
|
|
3504
|
+
parentSpanId: spanId(span.parentSpanId) ? span.parentSpanId : void 0,
|
|
3505
|
+
flags: protocolFlags(span),
|
|
3506
|
+
status: { code: protocolCount(span.status.code, 2) ?? 0 },
|
|
3507
|
+
events: span.events.slice(0, 8).map((event) => ({
|
|
3508
|
+
timeUnixNano: protocolTime(event.timeUnixNano),
|
|
3509
|
+
droppedAttributesCount: protocolCount(event.droppedAttributesCount) ?? 0,
|
|
3510
|
+
name: event.name === "exception" ? "exception" : minimalName(event.name),
|
|
3511
|
+
attributes: event.name === "exception" ? [{
|
|
3512
|
+
key: "exception.type",
|
|
3513
|
+
value: code?.value ?? { stringValue: "Error" }
|
|
3514
|
+
}] : minimalAttributes(event.attributes)
|
|
3515
|
+
})),
|
|
3516
|
+
droppedEventsCount: protocolCount(span.droppedEventsCount) ?? 0,
|
|
3517
|
+
links: span.links.filter((link) => traceId(link.traceId) && spanId(link.spanId)).slice(0, 8).map((link) => ({
|
|
3518
|
+
traceId: link.traceId,
|
|
3519
|
+
spanId: link.spanId,
|
|
3520
|
+
flags: protocolFlags(link),
|
|
3521
|
+
attributes: minimalAttributes(link.attributes),
|
|
3522
|
+
droppedAttributesCount: protocolCount(link.droppedAttributesCount) ?? 0
|
|
3523
|
+
})),
|
|
3524
|
+
droppedLinksCount: protocolCount(span.droppedLinksCount) ?? 0
|
|
3525
|
+
};
|
|
3526
|
+
})
|
|
3527
|
+
}))
|
|
3528
|
+
})) }),
|
|
3529
|
+
metrics: (data) => {
|
|
3530
|
+
const resourceMetrics = data.resourceMetrics.map((entry) => ({
|
|
3531
|
+
resource,
|
|
3532
|
+
scopeMetrics: entry.scopeMetrics.map((scope) => ({
|
|
3533
|
+
scope: { name: service },
|
|
3534
|
+
metrics: scope.metrics.filter((metric) => MINIMAL_METRIC_NAMES.has(metric.name)).flatMap((metric) => {
|
|
3535
|
+
const gauge = metric.gauge === void 0 ? void 0 : { dataPoints: minimalNumberPoints(metric.gauge.dataPoints) };
|
|
3536
|
+
const sum = metric.sum === void 0 ? void 0 : {
|
|
3537
|
+
aggregationTemporality: protocolCount(metric.sum.aggregationTemporality, 2) ?? 0,
|
|
3538
|
+
isMonotonic: metric.sum.isMonotonic,
|
|
3539
|
+
dataPoints: minimalNumberPoints(metric.sum.dataPoints)
|
|
3540
|
+
};
|
|
3541
|
+
const histogram = metric.histogram === void 0 ? void 0 : {
|
|
3542
|
+
aggregationTemporality: protocolCount(metric.histogram.aggregationTemporality, 2) ?? 0,
|
|
3543
|
+
dataPoints: minimalHistogramPoints(metric.histogram.dataPoints)
|
|
3544
|
+
};
|
|
3545
|
+
if (![
|
|
3546
|
+
gauge,
|
|
3547
|
+
sum,
|
|
3548
|
+
histogram
|
|
3549
|
+
].some((series) => (series?.dataPoints.length ?? 0) > 0)) return [];
|
|
3550
|
+
return [{
|
|
3551
|
+
name: metric.name,
|
|
3552
|
+
unit: [
|
|
3553
|
+
"1",
|
|
3554
|
+
"s",
|
|
3555
|
+
"ms"
|
|
3556
|
+
].includes(metric.unit ?? "") ? metric.unit : "1",
|
|
3557
|
+
...gauge === void 0 ? {} : { gauge },
|
|
3558
|
+
...sum === void 0 ? {} : { sum },
|
|
3559
|
+
...histogram === void 0 ? {} : { histogram }
|
|
3560
|
+
}];
|
|
3561
|
+
})
|
|
3562
|
+
}))
|
|
3563
|
+
}));
|
|
3564
|
+
return resourceMetrics.some((entry) => entry.scopeMetrics.some((scope) => scope.metrics.length > 0)) ? HttpBody.jsonUnsafe({ resourceMetrics }) : HttpBody.empty;
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
Effect.flatMap(OtlpExporter.Flusher, (flusher) => flusher.flush);
|
|
3569
|
+
var EngineeringLog = class extends Context.Service()("capxul/EngineeringLog") {};
|
|
3570
|
+
//#endregion
|
|
3571
|
+
//#region ../observability/src/engineering.ts
|
|
3572
|
+
/** Shared browser/server OTLP layer. OtlpLogger merges with incumbent loggers once. */
|
|
3573
|
+
const makeEngineeringTelemetryLayer = (config) => {
|
|
3574
|
+
const validated = validateEngineeringTelemetryConfig(config);
|
|
3575
|
+
return Layer.suspend(() => {
|
|
3576
|
+
const bounded = validated.deliveryBudget === void 0 ? void 0 : cliEngineeringDelivery(validated.deliveryBudget, validated.serviceName === "capxul-cli");
|
|
3577
|
+
const endpoints = postHogOtlpEndpoints(validated.host);
|
|
3578
|
+
let disabled = false;
|
|
3579
|
+
const enabled = () => {
|
|
3580
|
+
if (disabled) return false;
|
|
3581
|
+
try {
|
|
3582
|
+
disabled = (typeof validated.enabled === "function" ? validated.enabled() : validated.enabled ?? true) !== true;
|
|
3583
|
+
} catch {
|
|
3584
|
+
disabled = true;
|
|
3585
|
+
}
|
|
3586
|
+
return !disabled;
|
|
3587
|
+
};
|
|
3588
|
+
const delivery = {
|
|
3589
|
+
...validated.shutdownTimeout === void 0 ? {} : { shutdownTimeout: validated.shutdownTimeout },
|
|
3590
|
+
...validated.exportInterval === void 0 ? {} : { exportInterval: validated.exportInterval },
|
|
3591
|
+
...bounded === void 0 ? {} : {
|
|
3592
|
+
maxBatchSize: Number.MAX_SAFE_INTEGER,
|
|
3593
|
+
exportInterval: 1e3,
|
|
3594
|
+
shutdownTimeout: validated.serviceName === "capxul-cli" ? 0 : 500
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
const resource = {
|
|
3598
|
+
serviceName: validated.serviceName ?? "capxul-sdk",
|
|
3599
|
+
serviceVersion: validated.sdkVersion,
|
|
3600
|
+
attributes: {
|
|
3601
|
+
capxul_env: validated.capxulEnv,
|
|
3602
|
+
producer: validated.producer,
|
|
3603
|
+
sdk_version: validated.sdkVersion
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
const tracing = Layer.effect(Tracer.Tracer, OtlpTracer.make({
|
|
3607
|
+
url: endpoints.traces,
|
|
3608
|
+
headers: validated.headers,
|
|
3609
|
+
resource,
|
|
3610
|
+
...delivery
|
|
3611
|
+
}).pipe(Effect.map((tracer) => makeLeakSafeEngineeringTracer(bounded?.tracer(tracer) ?? tracer, enabled)))).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|
|
3612
|
+
const ownedLogging = Layer.effect(EngineeringLog, OtlpLogger.make({
|
|
3613
|
+
url: endpoints.logs,
|
|
3614
|
+
headers: validated.headers,
|
|
3615
|
+
resource,
|
|
3616
|
+
...delivery,
|
|
3617
|
+
...bounded === void 0 ? {} : { excludeLogSpans: true }
|
|
3618
|
+
}).pipe(Effect.map((nativeLogger) => {
|
|
3619
|
+
const logger = bounded?.logger(nativeLogger) ?? nativeLogger;
|
|
3620
|
+
return Logger.make((options) => {
|
|
3621
|
+
if (enabled()) logger.log(options);
|
|
3622
|
+
});
|
|
3623
|
+
}))).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|
|
3624
|
+
const logging = Logger.layer([EngineeringLog], { mergeWithExisting: true }).pipe(Layer.provideMerge(ownedLogging));
|
|
3625
|
+
const metricConfig = {
|
|
3626
|
+
url: endpoints.metrics,
|
|
3627
|
+
headers: validated.headers,
|
|
3628
|
+
resource,
|
|
3629
|
+
temporality: "delta",
|
|
3630
|
+
...delivery
|
|
3631
|
+
};
|
|
3632
|
+
const metrics = bounded === void 0 ? OtlpMetrics.layer(metricConfig) : validated.serviceName !== "capxul-cli" ? Layer.empty : Layer.effectDiscard(Effect.gen(function* () {
|
|
3633
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
3634
|
+
yield* OtlpMetrics.make({
|
|
3635
|
+
...metricConfig,
|
|
3636
|
+
exportInterval: Number.POSITIVE_INFINITY,
|
|
3637
|
+
shutdownTimeout: 0
|
|
3638
|
+
}).pipe(Effect.provideService(OtlpExporter.Flusher, {
|
|
3639
|
+
...flusher,
|
|
3640
|
+
register: (run) => flusher.register(bounded.metrics(run))
|
|
3641
|
+
}));
|
|
3642
|
+
})).pipe(Layer.provideMerge(OtlpExporter.layerFlusher));
|
|
3643
|
+
const headerPolicy = Layer.merge(Layer.succeed(HttpClient.TracerHeaderFilter, traceHeaderFilter), Layer.succeed(Headers.CurrentRedactedNames, [credentialHeaderPattern]));
|
|
3644
|
+
const browserLifecycle = Layer.effectDiscard(Effect.gen(function* () {
|
|
3645
|
+
if (validated.producer !== "browser" || !hasBrowserDocument()) return;
|
|
3646
|
+
const browserWindow = window;
|
|
3647
|
+
const browserDocument = document;
|
|
3648
|
+
const flusher = yield* OtlpExporter.Flusher;
|
|
3649
|
+
const run = yield* FiberSet.makeRuntime();
|
|
3650
|
+
const flush = () => {
|
|
3651
|
+
run(flusher.flush);
|
|
3652
|
+
};
|
|
3653
|
+
const visibilityChanged = () => {
|
|
3654
|
+
if (browserDocument.visibilityState === "hidden") flush();
|
|
3655
|
+
};
|
|
3656
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
3657
|
+
browserDocument.addEventListener("visibilitychange", visibilityChanged);
|
|
3658
|
+
browserWindow.addEventListener("pagehide", flush);
|
|
3659
|
+
}), () => Effect.sync(() => {
|
|
3660
|
+
browserDocument.removeEventListener("visibilitychange", visibilityChanged);
|
|
3661
|
+
browserWindow.removeEventListener("pagehide", flush);
|
|
3662
|
+
}));
|
|
3663
|
+
}));
|
|
3664
|
+
const transport = Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (client) => {
|
|
3665
|
+
const originalTransport = validated.producer === "browser" && hasBrowserDocument() ? browserExportClient(client) : client;
|
|
3666
|
+
const selectedTransport = bounded?.transport(originalTransport, enabled) ?? originalTransport;
|
|
3667
|
+
return HttpClient.transform(selectedTransport, (response, request) => Effect.suspend(() => {
|
|
3668
|
+
if (enabled() && request.body._tag !== "Empty") return response;
|
|
3669
|
+
bounded?.discardBody(request.body);
|
|
3670
|
+
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })));
|
|
3671
|
+
}));
|
|
3672
|
+
})).pipe(Layer.provide(FetchHttpClient.layer));
|
|
3673
|
+
const serialization = Layer.effect(OtlpSerialization.OtlpSerialization, Effect.map(OtlpSerialization.OtlpSerialization, (json) => {
|
|
3674
|
+
const projected = minimalSerialization(validated);
|
|
3675
|
+
const selected = validated.privacy === "minimal" ? projected : {
|
|
3676
|
+
...json,
|
|
3677
|
+
metrics: projected.metrics
|
|
3678
|
+
};
|
|
3679
|
+
return bounded?.serialization(selected) ?? selected;
|
|
3680
|
+
})).pipe(Layer.provide(OtlpSerialization.layerJson));
|
|
3681
|
+
const combined = browserLifecycle.pipe(Layer.provideMerge(Layer.mergeAll(tracing, logging, metrics, headerPolicy)), Layer.provide(serialization), Layer.provide(transport));
|
|
3682
|
+
const scoped = bounded === void 0 ? combined : combined.pipe(Layer.provideMerge(Layer.effectDiscard(Effect.addFinalizer(() => Effect.sync(bounded.close)))));
|
|
3683
|
+
return validated.deliveryBudget === void 0 ? scoped : scoped.pipe(Layer.provideMerge(Layer.succeed(Metric.MetricRegistry, validated.serviceName === "capxul-cli" ? validated.deliveryBudget.metricRegistry : validated.deliveryBudget.emptyMetricRegistry())));
|
|
3684
|
+
});
|
|
3685
|
+
};
|
|
3686
|
+
function derivedBrowserOrigin(runtime) {
|
|
3687
|
+
if (runtime !== "browser") throw Errors.invalidInput("origin", "required outside browser runtime");
|
|
3688
|
+
const origin = readField(readField(globalThis, "location"), "origin");
|
|
3689
|
+
if (typeof origin === "string" && origin.length > 0) return origin;
|
|
3690
|
+
throw Errors.invalidInput("origin", "required when browser location is unavailable");
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Browser local dev serves `/api/auth` via the Vite proxy on `window.location.origin`
|
|
3694
|
+
* while bootstrap returns the remote Convex site host. Openfort wallet setup reads
|
|
3695
|
+
* Better Auth cookies from `get-session` — those only attach on the same origin the
|
|
3696
|
+
* OTP flow used, so rewrite when hosts differ.
|
|
3697
|
+
*/
|
|
3698
|
+
function resolveBrowserAuthBaseUrl(input) {
|
|
3699
|
+
if (input.override !== void 0) return normalizeHttpUrl("authBaseUrl", input.override);
|
|
3700
|
+
const bootstrapUrl = normalizeHttpUrl("authBaseUrl", input.bootstrapAuthBaseUrl);
|
|
3701
|
+
if (input.runtime !== "browser") return bootstrapUrl;
|
|
3702
|
+
try {
|
|
3703
|
+
const localAuthBase = normalizeHttpUrl("authBaseUrl", `${derivedBrowserOrigin(input.runtime)}/api/auth`);
|
|
3704
|
+
const remoteAuthBase = bootstrapUrl.endsWith("/api/auth") ? bootstrapUrl : `${bootstrapUrl}/api/auth`;
|
|
3705
|
+
if (new URL(remoteAuthBase).host !== new URL(localAuthBase).host) return localAuthBase;
|
|
3706
|
+
return bootstrapUrl;
|
|
3707
|
+
} catch {
|
|
3708
|
+
return bootstrapUrl;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
function resolveRuntimeUrls(bootstrap, runtime, authBaseUrlOverride) {
|
|
3712
|
+
try {
|
|
3713
|
+
return {
|
|
3714
|
+
ok: true,
|
|
3715
|
+
value: {
|
|
3716
|
+
authBaseUrl: resolveBrowserAuthBaseUrl({
|
|
3717
|
+
bootstrapAuthBaseUrl: bootstrap.authBaseUrl,
|
|
3718
|
+
runtime,
|
|
3719
|
+
...authBaseUrlOverride === void 0 ? {} : { override: authBaseUrlOverride }
|
|
3720
|
+
}),
|
|
3721
|
+
convexUrl: normalizeHttpUrl("convexUrl", bootstrap.convexUrl)
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
} catch (cause) {
|
|
3725
|
+
return {
|
|
3726
|
+
ok: false,
|
|
3727
|
+
error: toPublicError(cause)
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
function normalizeHttpUrl(field, raw) {
|
|
3732
|
+
let parsed;
|
|
3733
|
+
try {
|
|
3734
|
+
parsed = new URL(raw);
|
|
3735
|
+
} catch {
|
|
3736
|
+
throw Errors.invalidInput(field, "must be an http or https URL");
|
|
3737
|
+
}
|
|
3738
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw Errors.invalidInput(field, "must be an http or https URL");
|
|
3739
|
+
return parsed.toString().replace(/\/$/, "");
|
|
3740
|
+
}
|
|
3741
|
+
//#endregion
|
|
3742
|
+
//#region src/platform/production-ports.ts
|
|
3743
|
+
function productionBootstrapPortLayer(bootstrap) {
|
|
3744
|
+
return Layer.succeed(BootstrapPortTag, bootstrap);
|
|
3745
|
+
}
|
|
3746
|
+
function productionAuthClientLayer(input, refreshConvexAuthRef, resetSignerSessionRef) {
|
|
3747
|
+
return (input.authClient !== void 0 ? Layer.succeed(AuthClientPortTag, input.authClient) : input.runtime === "browser" ? BetterAuthBrowserLayer({
|
|
3748
|
+
authBaseUrl: input.runtimeUrls.authBaseUrl,
|
|
3749
|
+
...input.fetch === void 0 ? {} : { fetch: input.fetch },
|
|
3750
|
+
...input.observation === void 0 ? {} : { observation: input.observation }
|
|
3751
|
+
}) : BetterAuthNodeLayer({
|
|
3752
|
+
...input.nodeSessionStorage === void 0 ? {} : { nodeSessionStorage: input.nodeSessionStorage },
|
|
3753
|
+
authBaseUrl: input.runtimeUrls.authBaseUrl,
|
|
3754
|
+
...input.origin === void 0 ? {} : { origin: input.origin },
|
|
3755
|
+
...input.fetch === void 0 ? {} : { fetch: input.fetch },
|
|
3756
|
+
...input.observation === void 0 ? {} : { observation: input.observation }
|
|
3757
|
+
})).pipe(Layer.flatMap((context) => {
|
|
3758
|
+
const refreshed = refreshConvexAuthOnSession(Context.get(context, AuthClientPortTag), () => {
|
|
3759
|
+
const refresh = refreshConvexAuthRef.current;
|
|
3760
|
+
if (refresh === null) {
|
|
3761
|
+
refreshConvexAuthRef.pending = true;
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
refresh();
|
|
3765
|
+
});
|
|
3766
|
+
return Layer.succeedContext(Context.make(AuthClientPortTag, resetSignerSessionRef === void 0 ? refreshed : resetSignerSessionOnSignOut(refreshed, resetSignerSessionRef)));
|
|
3767
|
+
}));
|
|
3768
|
+
}
|
|
3769
|
+
function productionAuthCacheLayer(input) {
|
|
3770
|
+
return Layer.succeed(AuthCachePortTag, input.authCache ?? detectAuthCacheAdapter());
|
|
3771
|
+
}
|
|
3772
|
+
function productionConvexCallLayer(input, refreshConvexAuthRef) {
|
|
3773
|
+
return Layer.unwrap(Effect.map(AuthClientPortTag, (authClient) => ConvexCallLayer({
|
|
3774
|
+
convexUrl: input.runtimeUrls.convexUrl,
|
|
3775
|
+
localDiagnostics: input.localDiagnostics,
|
|
3776
|
+
...input.applicationId === void 0 ? {} : { applicationId: input.applicationId },
|
|
3777
|
+
...input.observation === void 0 ? {} : { observation: input.observation },
|
|
3778
|
+
...input.convexClient === void 0 ? {} : { client: input.convexClient },
|
|
3779
|
+
tokenProvider: async ({ forceRefreshToken }) => {
|
|
3780
|
+
const tokenResult = await Effect.runPromise(Effect.result(authClient.getConvexJwt({
|
|
3781
|
+
forceRefresh: forceRefreshToken,
|
|
3782
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
3783
|
+
})));
|
|
3784
|
+
if (Result.isFailure(tokenResult)) return null;
|
|
3785
|
+
return String(tokenResult.success.token);
|
|
3786
|
+
}
|
|
3787
|
+
}).pipe(Layer.flatMap((context) => {
|
|
3788
|
+
const convexCall = Context.get(context, ConvexCallPortTag);
|
|
3789
|
+
if (isRefreshableConvexCallPort(convexCall)) {
|
|
3790
|
+
refreshConvexAuthRef.current = () => convexCall.refreshAuth();
|
|
3791
|
+
if (refreshConvexAuthRef.pending) {
|
|
3792
|
+
refreshConvexAuthRef.pending = false;
|
|
3793
|
+
refreshConvexAuthRef.current();
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
return Layer.succeedContext(context);
|
|
3797
|
+
}))));
|
|
3798
|
+
}
|
|
3799
|
+
function isRefreshableConvexCallPort(convexCall) {
|
|
3800
|
+
return "refreshAuth" in convexCall && typeof convexCall.refreshAuth === "function";
|
|
3801
|
+
}
|
|
3802
|
+
function optionalEngineeringTelemetryLayer(policy, runtime, hostPolicy) {
|
|
3803
|
+
if (policy === void 0) return Layer.empty;
|
|
3804
|
+
try {
|
|
3805
|
+
return makeEngineeringTelemetryLayer({
|
|
3806
|
+
host: policy.host,
|
|
3807
|
+
headers: { authorization: `Bearer ${policy.projectToken}` },
|
|
3808
|
+
capxulEnv: policy.capxulEnv,
|
|
3809
|
+
producer: runtime === "browser" ? "browser" : "server",
|
|
3810
|
+
sdkVersion: SDK_VERSION,
|
|
3811
|
+
...hostPolicy
|
|
3812
|
+
});
|
|
3813
|
+
} catch {
|
|
3814
|
+
return Layer.empty;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
function localDiagnosticsLayer(enabled, runtime) {
|
|
3818
|
+
if (!enabled) return Logger.layer([]);
|
|
3819
|
+
return runtime === "node" ? Layer.succeed(Logger.LogToStderr, true) : Layer.empty;
|
|
3820
|
+
}
|
|
3821
|
+
function productionLocalDiagnostics(input) {
|
|
3822
|
+
return input.localDiagnostics !== false;
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* Build the Ponder read port for this client, or `undefined` when no indexer
|
|
3826
|
+
* origin was supplied. The token provider is the same
|
|
3827
|
+
* `AuthClientPort.getConvexJwt` closure `ConvexCallPort` uses, so the indexer
|
|
3828
|
+
* and Convex see one session with one scope.
|
|
3829
|
+
*/
|
|
3830
|
+
function productionIndexerReadPort(input, ports, observation) {
|
|
3831
|
+
if (input.activityReadSource === "convex") return void 0;
|
|
3832
|
+
if (input.indexerUrl === void 0 || input.indexerUrl === "") return void 0;
|
|
3833
|
+
const baseUrl = normalizeHttpUrl("indexerUrl", input.indexerUrl);
|
|
3834
|
+
return new PonderIndexerAdapter({
|
|
3835
|
+
baseUrl,
|
|
3836
|
+
token: async ({ forceRefreshToken, signal }) => {
|
|
3837
|
+
const tokenSignal = input.signal === void 0 ? signal : signal === void 0 ? input.signal : AbortSignal.any([input.signal, signal]);
|
|
3838
|
+
const token = await Effect.runPromise(Effect.result(ports.authClient.getConvexJwt({
|
|
3839
|
+
forceRefresh: forceRefreshToken,
|
|
3840
|
+
...tokenSignal === void 0 ? {} : { signal: tokenSignal }
|
|
3841
|
+
})));
|
|
3842
|
+
if (Result.isFailure(token)) throw toPublicError(token.failure);
|
|
3843
|
+
return String(token.success.token);
|
|
3844
|
+
},
|
|
3845
|
+
...input.fetch === void 0 ? {} : { fetch: input.fetch },
|
|
3846
|
+
...observation === void 0 ? {} : { observation }
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
//#endregion
|
|
3850
|
+
//#region src/platform/production-layers.ts
|
|
3851
|
+
/**
|
|
3852
|
+
* Project the built graph into the flat `FlowPorts` record the method bundles
|
|
3853
|
+
* still take. It is a VIEW of `ProductionAdapters.context`, taken from the
|
|
3854
|
+
* graph and never instead of it — the graph stays alive in the scope and is
|
|
3855
|
+
* handed to every caller.
|
|
3856
|
+
*/
|
|
3857
|
+
const collectProductionFlowPorts = Effect.gen(function* () {
|
|
3858
|
+
const authClient = yield* AuthClientPortTag;
|
|
3859
|
+
const authCache = yield* AuthCachePortTag;
|
|
3860
|
+
const bootstrap = yield* BootstrapPortTag;
|
|
3861
|
+
const clock = yield* ClockPortTag;
|
|
3862
|
+
const convexCall = yield* ConvexCallPortTag;
|
|
3863
|
+
return {
|
|
3864
|
+
authClient,
|
|
3865
|
+
authCache,
|
|
3866
|
+
identity: yield* IdentityPortTag,
|
|
3867
|
+
smartAccount: yield* SmartAccountPortTag,
|
|
3868
|
+
accountRead: yield* AccountReadPortTag,
|
|
3869
|
+
bootstrap,
|
|
3870
|
+
clock,
|
|
3871
|
+
telemetry: yield* TelemetryPortTag,
|
|
3872
|
+
convexCall
|
|
3873
|
+
};
|
|
3874
|
+
});
|
|
3875
|
+
/**
|
|
3876
|
+
* The production port graph. Every port is named here once, so a port left
|
|
3877
|
+
* out does not compile: the return type requires all of them.
|
|
3878
|
+
*
|
|
3879
|
+
* Phase 1: authClient -> convexCall. The auth wrapper reads
|
|
3880
|
+
* RefreshConvexAuthRef at call time, after the convex layer registers the
|
|
3881
|
+
* concrete refreshAuth callback during construction. Phase 2: the ready
|
|
3882
|
+
* convexCall (and telemetry, for the reads that observe) -> the Convex-backed
|
|
3883
|
+
* domain ports. A layer used twice is built once.
|
|
3884
|
+
*/
|
|
3885
|
+
function makeProductionAdapterLayer(input) {
|
|
3886
|
+
const refreshConvexAuthRef = {
|
|
3887
|
+
current: null,
|
|
3888
|
+
pending: false
|
|
3889
|
+
};
|
|
3890
|
+
const authClient = productionAuthClientLayer(input, refreshConvexAuthRef, input.resetSignerSession);
|
|
3891
|
+
const convexCall = productionConvexCallLayer(input, refreshConvexAuthRef).pipe(Layer.provide(authClient));
|
|
3892
|
+
const telemetry = input.telemetry === void 0 ? PostHogTelemetryLayer({ capture: () => void 0 }) : Layer.succeed(TelemetryPortTag, input.telemetry);
|
|
3893
|
+
const observedConvexCall = Layer.merge(convexCall, telemetry);
|
|
3894
|
+
const org = Layer.effect(OrgPortTag, Effect.all([ConvexCallPortTag, TelemetryPortTag]).pipe(Effect.map(([convex, telemetryPort]) => new ConvexOrganizationAdapter({
|
|
3895
|
+
convex,
|
|
3896
|
+
telemetry: telemetryPort
|
|
3897
|
+
}))));
|
|
3898
|
+
return Layer.mergeAll(productionBootstrapPortLayer(input.bootstrap), authClient, productionAuthCacheLayer(input), ConvexIdentityLayer.pipe(Layer.provide(convexCall)), ConvexSmartAccountLayer.pipe(Layer.provide(convexCall)), ConvexAccountLayer().pipe(Layer.provide(observedConvexCall)), SystemClockLayer, telemetry, convexCall, org.pipe(Layer.provide(observedConvexCall)));
|
|
3899
|
+
}
|
|
3900
|
+
//#endregion
|
|
3901
|
+
//#region src/platform/production-input.ts
|
|
3902
|
+
function validateCreateCapxulClientInput(input) {
|
|
3903
|
+
const runtime = input.runtime ?? detectRuntime();
|
|
3904
|
+
if (input.signer !== void 0 && input.openfortSignerFactory !== void 0) return {
|
|
3905
|
+
ok: false,
|
|
3906
|
+
error: Errors.invalidInput("signer", "choose signer or openfortSignerFactory")
|
|
3907
|
+
};
|
|
3908
|
+
if (input.nodeSessionStorage !== void 0 && runtime === "browser") return {
|
|
3909
|
+
ok: false,
|
|
3910
|
+
error: Errors.invalidInput("nodeSessionStorage", "is only supported in Node")
|
|
3911
|
+
};
|
|
3912
|
+
if ((input.requirement ?? "none") === "deployed" && input.signer === void 0 && input.openfortSignerFactory === void 0 && runtime !== "browser") return {
|
|
3913
|
+
ok: false,
|
|
3914
|
+
error: Errors.invalidInput("signer", "required when requirement is \"deployed\"")
|
|
3915
|
+
};
|
|
3916
|
+
return {
|
|
3917
|
+
ok: true,
|
|
3918
|
+
value: void 0
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
function detectRuntime() {
|
|
3922
|
+
return readField(globalThis, "window") !== void 0 || readField(globalThis, "document") !== void 0 ? "browser" : "node";
|
|
3923
|
+
}
|
|
3924
|
+
//#endregion
|
|
3925
|
+
//#region src/platform/production-bootstrap.ts
|
|
3926
|
+
function resolveInput(input) {
|
|
3927
|
+
try {
|
|
3928
|
+
const runtime = input.runtime ?? detectRuntime();
|
|
3929
|
+
const publishableKey = toPublishableKey(input.publishableKey);
|
|
3930
|
+
const origin = input.origin === void 0 ? runtime === "browser" ? toAllowedOrigin(derivedBrowserOrigin(runtime)) : void 0 : toAllowedOrigin(input.origin);
|
|
3931
|
+
return {
|
|
3932
|
+
ok: true,
|
|
3933
|
+
value: {
|
|
3934
|
+
publishableKey,
|
|
3935
|
+
...origin === void 0 ? {} : { origin },
|
|
3936
|
+
bootstrapBaseUrl: normalizeHttpUrl("bootstrapBaseUrl", input.bootstrapBaseUrl ?? (runtime === "browser" ? derivedBrowserOrigin(runtime) : "https://api.staging.capxul.com")),
|
|
3937
|
+
runtime
|
|
3938
|
+
}
|
|
3939
|
+
};
|
|
3940
|
+
} catch (cause) {
|
|
3941
|
+
return {
|
|
3942
|
+
ok: false,
|
|
3943
|
+
error: toPublicError(cause)
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
async function runBootstrap(effect) {
|
|
3948
|
+
const result = await Effect.runPromise(Effect.result(effect));
|
|
3949
|
+
if (Result.isSuccess(result)) return {
|
|
3950
|
+
ok: true,
|
|
3951
|
+
value: result.success
|
|
3952
|
+
};
|
|
3953
|
+
return {
|
|
3954
|
+
ok: false,
|
|
3955
|
+
error: toPublicError(result.failure)
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* Bootstrap-event props. `runtime` used to be called `capxulEnv`, which was a
|
|
3960
|
+
* name collision, not a value: it carried "browser"/"node", never an
|
|
3961
|
+
* environment. ADR-0020 A1 makes `capxul_env` the environment discriminator
|
|
3962
|
+
* every synced artifact filters on, so this field was renamed to what it
|
|
3963
|
+
* actually is. `sdk_version` follows the canon envelope spelling.
|
|
3964
|
+
*/
|
|
3965
|
+
function bootstrapTelemetryEnvelope(input, resolvedInput) {
|
|
3966
|
+
return {
|
|
3967
|
+
runtime: resolvedInput.runtime,
|
|
3968
|
+
env: input.requirement ?? "none",
|
|
3969
|
+
...resolvedInput.origin === void 0 ? {} : { origin: resolvedInput.origin },
|
|
3970
|
+
sdk_version: SDK_VERSION
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3973
|
+
async function emitBootstrapTelemetry(telemetry, event) {
|
|
3974
|
+
if (telemetry === void 0) return;
|
|
3975
|
+
await Effect.runPromise(telemetry.emit(event).pipe(Effect.catchDefect(() => Effect.void)));
|
|
3976
|
+
}
|
|
3977
|
+
//#endregion
|
|
3978
|
+
//#region src/platform/production-adapters.ts
|
|
3979
|
+
async function createProductionAdapters(input) {
|
|
3980
|
+
const localDiagnostics = productionLocalDiagnostics(input);
|
|
3981
|
+
const { observation, telemetry, invocationObservation, invocationTelemetry } = resolveProductionObservability(input);
|
|
3982
|
+
const resolvedInput = resolveInput(input);
|
|
3983
|
+
if (!resolvedInput.ok) return resolvedInput;
|
|
3984
|
+
const scope = await Effect.runPromise(Scope.make());
|
|
3985
|
+
const closeScope = idempotentClose(() => Effect.runPromise(Scope.close(scope, Exit.void)));
|
|
3986
|
+
const bootstrapLayer = HttpBootstrapLayer({
|
|
3987
|
+
bootstrapBaseUrl: resolvedInput.value.bootstrapBaseUrl,
|
|
3988
|
+
...input.fetch === void 0 ? {} : { fetch: input.fetch },
|
|
3989
|
+
...invocationObservation === void 0 ? {} : { observation: invocationObservation },
|
|
3990
|
+
...input.bootstrapCache === void 0 ? {} : { cache: input.bootstrapCache }
|
|
3991
|
+
});
|
|
3992
|
+
try {
|
|
3993
|
+
const bootstrapContext = await Effect.runPromise(Layer.buildWithScope(bootstrapLayer, scope));
|
|
3994
|
+
const bootstrap = Context.get(bootstrapContext, BootstrapPortTag);
|
|
3995
|
+
const bootstrapResult = await runBootstrap(bootstrap.resolve({
|
|
3996
|
+
publishableKey: resolvedInput.value.publishableKey,
|
|
3997
|
+
...resolvedInput.value.origin === void 0 ? {} : { origin: resolvedInput.value.origin }
|
|
3998
|
+
}));
|
|
3999
|
+
if (!bootstrapResult.ok) {
|
|
4000
|
+
await emitBootstrapTelemetry(invocationTelemetry, {
|
|
4001
|
+
name: "bootstrap_failed",
|
|
4002
|
+
props: {
|
|
4003
|
+
...bootstrapTelemetryEnvelope(input, resolvedInput.value),
|
|
4004
|
+
reason: bootstrapResult.error.code,
|
|
4005
|
+
...failureDetail(bootstrapResult.error),
|
|
4006
|
+
...failureEvidenceProps(bootstrapResult.error)
|
|
4007
|
+
}
|
|
4008
|
+
});
|
|
4009
|
+
await closeScope().catch(() => void 0);
|
|
4010
|
+
return bootstrapResult;
|
|
4011
|
+
}
|
|
4012
|
+
const chainFence = assertDevKeySignerIsTestnetOnly(input.signer, bootstrapResult.value.chainId);
|
|
4013
|
+
if (!chainFence.ok) {
|
|
4014
|
+
await closeScope().catch(() => void 0);
|
|
4015
|
+
return chainFence;
|
|
4016
|
+
}
|
|
4017
|
+
const runtimeUrlsResult = resolveRuntimeUrls(bootstrapResult.value, resolvedInput.value.runtime, input.authBaseUrl);
|
|
4018
|
+
if (!runtimeUrlsResult.ok) {
|
|
4019
|
+
await emitBootstrapTelemetry(invocationTelemetry, {
|
|
4020
|
+
name: "bootstrap_failed",
|
|
4021
|
+
props: {
|
|
4022
|
+
...bootstrapTelemetryEnvelope(input, resolvedInput.value),
|
|
4023
|
+
applicationId: bootstrapResult.value.applicationId,
|
|
4024
|
+
reason: runtimeUrlsResult.error.code
|
|
4025
|
+
}
|
|
4026
|
+
});
|
|
4027
|
+
await closeScope().catch(() => void 0);
|
|
4028
|
+
return runtimeUrlsResult;
|
|
4029
|
+
}
|
|
4030
|
+
const runtimeUrls = runtimeUrlsResult;
|
|
4031
|
+
await emitBootstrapTelemetry(invocationTelemetry, {
|
|
4032
|
+
name: "bootstrap_resolved",
|
|
4033
|
+
props: {
|
|
4034
|
+
...bootstrapTelemetryEnvelope(input, resolvedInput.value),
|
|
4035
|
+
applicationId: bootstrapResult.value.applicationId
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
let injectedClient;
|
|
4039
|
+
const convexClientFactory = input.convexClientFactory;
|
|
4040
|
+
if (convexClientFactory !== void 0) try {
|
|
4041
|
+
injectedClient = convexClientFactory(runtimeUrls.value.convexUrl);
|
|
4042
|
+
} catch (cause) {
|
|
4043
|
+
await closeScope().catch(() => void 0);
|
|
4044
|
+
return {
|
|
4045
|
+
ok: false,
|
|
4046
|
+
error: toPublicError(cause)
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
const portsLayer = makeProductionAdapterLayer({
|
|
4050
|
+
bootstrap,
|
|
4051
|
+
runtime: resolvedInput.value.runtime,
|
|
4052
|
+
localDiagnostics,
|
|
4053
|
+
...resolvedInput.value.origin === void 0 ? {} : { origin: resolvedInput.value.origin },
|
|
4054
|
+
runtimeUrls: runtimeUrls.value,
|
|
4055
|
+
chainId: bootstrapResult.value.chainId,
|
|
4056
|
+
applicationId: bootstrapResult.value.applicationId,
|
|
4057
|
+
...observation === void 0 ? {} : { observation },
|
|
4058
|
+
...input.nodeSessionStorage === void 0 ? {} : { nodeSessionStorage: input.nodeSessionStorage },
|
|
4059
|
+
...input.authCache === void 0 ? {} : { authCache: input.authCache },
|
|
4060
|
+
...input.authClient === void 0 ? {} : { authClient: input.authClient },
|
|
4061
|
+
...telemetry === void 0 ? {} : { telemetry },
|
|
4062
|
+
...injectedClient === void 0 ? {} : { convexClient: injectedClient },
|
|
4063
|
+
...input.fetch === void 0 ? {} : { fetch: input.fetch },
|
|
4064
|
+
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
4065
|
+
...input.resetSignerSession === void 0 ? {} : { resetSignerSession: input.resetSignerSession }
|
|
4066
|
+
});
|
|
4067
|
+
const applicationLayer = Layer.merge(portsLayer, optionalEngineeringTelemetryLayer(bootstrapResult.value.engineeringTelemetry, resolvedInput.value.runtime, hostEngineeringPolicy(input.observability))).pipe(Layer.provideMerge(localDiagnosticsLayer(localDiagnostics, resolvedInput.value.runtime)));
|
|
4068
|
+
const context = await Effect.runPromise(Layer.buildWithScope(applicationLayer, scope));
|
|
4069
|
+
const collected = await Effect.runPromise(collectProductionFlowPorts.pipe(Effect.provide(context)));
|
|
4070
|
+
const indexerUrl = input.indexerUrl ?? bootstrapResult.value.indexerUrl;
|
|
4071
|
+
const indexerRead = productionIndexerReadPort(indexerUrl === void 0 ? input : {
|
|
4072
|
+
...input,
|
|
4073
|
+
indexerUrl
|
|
4074
|
+
}, collected, observation);
|
|
4075
|
+
const ports = indexerRead === void 0 ? collected : {
|
|
4076
|
+
...collected,
|
|
4077
|
+
indexerRead
|
|
4078
|
+
};
|
|
4079
|
+
const stopConnectionObservation = observeProductionConvexConnection(ports.convexCall, context);
|
|
4080
|
+
const close = idempotentClose(async () => {
|
|
4081
|
+
try {
|
|
4082
|
+
stopConnectionObservation();
|
|
4083
|
+
} finally {
|
|
4084
|
+
await closeScope();
|
|
4085
|
+
}
|
|
4086
|
+
});
|
|
4087
|
+
return {
|
|
4088
|
+
ok: true,
|
|
4089
|
+
value: {
|
|
4090
|
+
context,
|
|
4091
|
+
ports,
|
|
4092
|
+
bootstrap: bootstrapResult.value,
|
|
4093
|
+
close
|
|
4094
|
+
}
|
|
4095
|
+
};
|
|
4096
|
+
} catch (cause) {
|
|
4097
|
+
await closeScope().catch(() => void 0);
|
|
4098
|
+
return {
|
|
4099
|
+
ok: false,
|
|
4100
|
+
error: toPublicError(cause)
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
function idempotentClose(close) {
|
|
4105
|
+
let closed = false;
|
|
4106
|
+
return async () => {
|
|
4107
|
+
if (closed) return;
|
|
4108
|
+
closed = true;
|
|
4109
|
+
await close();
|
|
4110
|
+
};
|
|
4111
|
+
}
|
|
4112
|
+
//#endregion
|
|
4113
|
+
//#region src/platform/client/financial-ops.ts
|
|
4114
|
+
function makeFinancialOpsMethods(deps) {
|
|
4115
|
+
return {
|
|
4116
|
+
me: makeMeMethods(deps),
|
|
4117
|
+
targets: makeTargetsMethods(deps),
|
|
4118
|
+
destinations: makeDestinationsMethods(deps),
|
|
4119
|
+
payments: makePaymentsMethods(deps),
|
|
4120
|
+
approvals: makeApprovalsMethods(deps),
|
|
4121
|
+
activity: makeActivityMethods(deps),
|
|
4122
|
+
offramp: makeOfframpMethods(),
|
|
4123
|
+
paymentDocuments: makePaymentDocumentsMethods(deps)
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
//#endregion
|
|
4127
|
+
//#region src/media/images.ts
|
|
4128
|
+
/** The upload POST failed: the storage provider, not the row, refused. */
|
|
4129
|
+
function uploadFailed(cause) {
|
|
4130
|
+
return {
|
|
4131
|
+
ok: false,
|
|
4132
|
+
error: Errors.providerError("convex-storage", "uploadImage", cause)
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
/** The storage id the upload response names, if it names one. */
|
|
4136
|
+
function storageIdOf(body) {
|
|
4137
|
+
const storageId = readField(body, "storageId");
|
|
4138
|
+
return typeof storageId === "string" && storageId.length > 0 ? storageId : void 0;
|
|
4139
|
+
}
|
|
4140
|
+
function makeMediaMethods(deps) {
|
|
4141
|
+
const fetchImpl = deps.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
4142
|
+
/** One observed call of one row, recorded under the row's name. */
|
|
4143
|
+
const call = (row, args, options) => {
|
|
4144
|
+
const controls = deps.invocationControls?.(options) ?? options;
|
|
4145
|
+
return runCapxulEffect(callOperation(deps.convexCall, row, copyInvocationObservation(controls, args)), controls, row.name, deps.runPromise);
|
|
4146
|
+
};
|
|
4147
|
+
return {
|
|
4148
|
+
async uploadImage(blob, options) {
|
|
4149
|
+
const url = await call(images.generateUploadUrl, {}, options);
|
|
4150
|
+
if (!url.ok) return url;
|
|
4151
|
+
try {
|
|
4152
|
+
const response = await fetchImpl(url.value.uploadUrl, {
|
|
4153
|
+
method: "POST",
|
|
4154
|
+
headers: { "Content-Type": blob.type },
|
|
4155
|
+
body: blob,
|
|
4156
|
+
...options?.signal === void 0 ? {} : { signal: options.signal }
|
|
4157
|
+
});
|
|
4158
|
+
if (!response.ok) return uploadFailed(`upload failed with status ${response.status}`);
|
|
4159
|
+
const storageId = storageIdOf(await response.json());
|
|
4160
|
+
if (storageId === void 0) return uploadFailed("upload response carried no storageId");
|
|
4161
|
+
return {
|
|
4162
|
+
ok: true,
|
|
4163
|
+
value: { storageId }
|
|
4164
|
+
};
|
|
4165
|
+
} catch (cause) {
|
|
4166
|
+
return uploadFailed(cause);
|
|
4167
|
+
}
|
|
4168
|
+
},
|
|
4169
|
+
setProfileImage: (input, options) => call(images.setProfileImage, { storageId: input.storageId }, options),
|
|
4170
|
+
setOrgLogo: (input, options) => call(images.setOrgLogo, {
|
|
4171
|
+
orgId: input.orgId,
|
|
4172
|
+
storageId: input.storageId
|
|
4173
|
+
}, options)
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
//#endregion
|
|
4177
|
+
//#region src/operator/diagnostics.ts
|
|
4178
|
+
/** A W3C version-00 `traceparent` as the caller's span; anything else carries no parent. */
|
|
4179
|
+
const traceparentSpan = (traceparent) => {
|
|
4180
|
+
const parts = traceparent.split("-");
|
|
4181
|
+
const [version, traceId = "", spanId = "", flags = ""] = parts;
|
|
4182
|
+
if (parts.length !== 4 || version !== "00" || !/^[0-9a-f]{32}$/iu.test(traceId) || !/^[0-9a-f]{16}$/iu.test(spanId)) return void 0;
|
|
4183
|
+
return Tracer.externalSpan({
|
|
4184
|
+
traceId,
|
|
4185
|
+
spanId,
|
|
4186
|
+
sampled: (Number.parseInt(flags, 16) & 1) === 1
|
|
4187
|
+
});
|
|
4188
|
+
};
|
|
4189
|
+
function makeSystemMethods(deps) {
|
|
4190
|
+
return { health: (nonce, options) => {
|
|
4191
|
+
let controls = deps.invocationControls?.(options) ?? options;
|
|
4192
|
+
const invocation = readInvocationObservation(controls);
|
|
4193
|
+
const traceparent = invocation?.active === false ? void 0 : sanitizeObservationContext(invocation?.context)?.traceparent;
|
|
4194
|
+
const parent = traceparent === void 0 ? void 0 : traceparentSpan(traceparent);
|
|
4195
|
+
if (parent !== void 0) controls = carryInvocationParentSpan(controls, parent);
|
|
4196
|
+
return runCapxulEffect(callOperation(deps.convexCall, diagnostics.health, copyInvocationObservation(controls, { nonce })), controls, diagnostics.health.name, deps.runPromise);
|
|
4197
|
+
} };
|
|
4198
|
+
}
|
|
4199
|
+
//#endregion
|
|
4200
|
+
//#region src/platform/client/host-observation.ts
|
|
4201
|
+
function transitionError(record) {
|
|
4202
|
+
const failure = record.failure;
|
|
4203
|
+
if (failure?.error !== void 0) {
|
|
4204
|
+
const error = failure.error;
|
|
4205
|
+
if (record.correlation_id === void 0 || error.correlationId !== void 0) return error;
|
|
4206
|
+
return new CapxulError(error.code, error.message, {
|
|
4207
|
+
cause: error,
|
|
4208
|
+
correlationId: record.correlation_id,
|
|
4209
|
+
...error.details === void 0 ? {} : { details: error.details },
|
|
4210
|
+
...error.layer === void 0 ? {} : { layer: error.layer }
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
return new CapxulError(record.error_code, failure?.message ?? "Identity work failed", {
|
|
4214
|
+
...failure === void 0 ? {} : { cause: failure },
|
|
4215
|
+
details: {
|
|
4216
|
+
machine: record.machine,
|
|
4217
|
+
state: record.state,
|
|
4218
|
+
event: record.event,
|
|
4219
|
+
slot: record.slot,
|
|
4220
|
+
...failure?.mode === void 0 ? {} : { failure_mode: failure.mode }
|
|
4221
|
+
},
|
|
4222
|
+
...record.correlation_id === void 0 ? {} : { correlationId: record.correlation_id }
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
function withHostObservation(actor, snapshot) {
|
|
4226
|
+
if (snapshot === void 0) return actor;
|
|
4227
|
+
const controls = (input) => withHostObservationControls(snapshot, input, actor.authSession()?.authUserId);
|
|
4228
|
+
return {
|
|
4229
|
+
...actor,
|
|
4230
|
+
ask: (event, input) => actor.ask(event, controls(input)),
|
|
4231
|
+
tell: (event, input) => actor.tell(event, controls(input)),
|
|
4232
|
+
restoreAuthSession: (session, input) => actor.restoreAuthSession(session, controls(input))
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
function withHostObservationControls(snapshot, input, actorId) {
|
|
4236
|
+
const previous = readInvocationObservation(input);
|
|
4237
|
+
if (previous !== void 0) return attachInvocationObservation({ ...input }, previous);
|
|
4238
|
+
const captured = snapshot?.() ?? { active: true };
|
|
4239
|
+
const context = captured.active ? withCapturedAttemptContext(captured.context, input) : captured.context;
|
|
4240
|
+
return attachInvocationObservation({ ...input }, {
|
|
4241
|
+
...captured,
|
|
4242
|
+
...context === void 0 ? {} : { context },
|
|
4243
|
+
...actorId === void 0 ? {} : { actorId }
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
4246
|
+
//#endregion
|
|
4247
|
+
//#region src/platform/client/signer-readiness.ts
|
|
4248
|
+
/**
|
|
4249
|
+
* Start the signer readiness cycle when the identity becomes claimed (#1667).
|
|
4250
|
+
*
|
|
4251
|
+
* The cycle used to run only from the embedded-wallet port's `getAddress` and
|
|
4252
|
+
* `signRawDigest`. Nothing calls those after a sign-in or a session restore,
|
|
4253
|
+
* and `resetSession`, which every OTP verify performs, sets the status back to
|
|
4254
|
+
* `unknown`. Thus `client.signer.status()` never left `unknown` on a fresh
|
|
4255
|
+
* session, and each `.Actions` gate that requires `ready` stayed closed. A
|
|
4256
|
+
* claimed identity is the first point where the browser signer has a session to
|
|
4257
|
+
* recover. A signed-out client starts no cycle and reports `unknown`.
|
|
4258
|
+
*
|
|
4259
|
+
* The assembly does not await the call, on purpose: readiness must converge
|
|
4260
|
+
* without a signature. The cycle reports its own failure. It writes
|
|
4261
|
+
* `unavailable` to the same store the gate reads, and adds a DiagnosticPort
|
|
4262
|
+
* breadcrumb, and the next `getAddress` retries it, as before. The rejection is
|
|
4263
|
+
* thus observed and not discarded. A throw here would only cause an unhandled
|
|
4264
|
+
* rejection, and the actor shell removes an observer that throws.
|
|
4265
|
+
*
|
|
4266
|
+
* The `unknown` test holds this to ONE cycle per session. The call sets the
|
|
4267
|
+
* status to `recovering` synchronously, so the later claimed transitions of the
|
|
4268
|
+
* same session, such as the org lane, start no second cycle, and a
|
|
4269
|
+
* `resetSession` that sets the status back to `unknown` permits one more.
|
|
4270
|
+
*
|
|
4271
|
+
* ponytail: one cycle per claim. A cycle that fails stays `unavailable` until
|
|
4272
|
+
* a `getAddress` or a new claim, so a gate that refuses to sign while
|
|
4273
|
+
* `unavailable` needs a reload. Give the action part an explicit retry if that
|
|
4274
|
+
* becomes the common failure.
|
|
4275
|
+
*/
|
|
4276
|
+
function startSignerReadinessOnClaim(actor, signer) {
|
|
4277
|
+
if (signer?.statusStore === void 0 || signer.ensureWalletReady === void 0) return () => {};
|
|
4278
|
+
const browserSigner = signer;
|
|
4279
|
+
return actor.subscribe((state) => {
|
|
4280
|
+
if (state.phase !== "authenticated" || state.account.at !== "claimed") return;
|
|
4281
|
+
if (browserSigner.statusStore?.status() !== "unknown") return;
|
|
4282
|
+
browserSigner.ensureWalletReady?.().catch(() => {});
|
|
4283
|
+
});
|
|
4284
|
+
}
|
|
4285
|
+
/**
|
|
4286
|
+
* Report the result of each signer readiness cycle (#1962 R04).
|
|
4287
|
+
*
|
|
4288
|
+
* The readiness store already carries the cycle boundary this needs, so no
|
|
4289
|
+
* public signer hook, second identity subscriber, or render counter is added.
|
|
4290
|
+
* `ensureOpenfortWalletReady` writes `recovering` synchronously when it starts
|
|
4291
|
+
* a cycle, and only the cycle that is still the current one writes `ready` or
|
|
4292
|
+
* `unavailable` (`create-openfort-browser-signer.ts:501-521`). Callers that
|
|
4293
|
+
* join a live cycle share that single write, and a `resetSession` writes
|
|
4294
|
+
* `unknown` first, so a cycle that a reset or an account replacement outran
|
|
4295
|
+
* reports nothing.
|
|
4296
|
+
*
|
|
4297
|
+
* The host context is bound when the cycle starts, so a person who signs out
|
|
4298
|
+
* and back in while a cycle runs cannot relabel the result that began under
|
|
4299
|
+
* the previous person. A signer with no readiness cycle — a node or dev key,
|
|
4300
|
+
* and a headless client whose requirement never opens the account lane —
|
|
4301
|
+
* writes no status and reports nothing.
|
|
4302
|
+
*/
|
|
4303
|
+
function observeSignerReadinessCycles(signer, telemetry, hostObservationSnapshot, runPromise) {
|
|
4304
|
+
const statusStore = signer?.statusStore;
|
|
4305
|
+
if (statusStore === void 0 || telemetry === void 0) return () => {};
|
|
4306
|
+
let running = false;
|
|
4307
|
+
let startContext;
|
|
4308
|
+
return statusStore.subscribe((status) => {
|
|
4309
|
+
if (status === "recovering") {
|
|
4310
|
+
running = true;
|
|
4311
|
+
startContext = hostObservationSnapshot?.();
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
const cycleContext = startContext;
|
|
4315
|
+
const completesCycle = running;
|
|
4316
|
+
running = false;
|
|
4317
|
+
startContext = void 0;
|
|
4318
|
+
if (!completesCycle || status !== "ready" && status !== "unavailable") return;
|
|
4319
|
+
const event = {
|
|
4320
|
+
name: "access_recovery_completed",
|
|
4321
|
+
props: {
|
|
4322
|
+
recovery: "signer_readiness",
|
|
4323
|
+
outcome: status
|
|
4324
|
+
}
|
|
4325
|
+
};
|
|
4326
|
+
if (cycleContext !== void 0) attachInvocationObservation(event, cycleContext);
|
|
4327
|
+
runPromise(Effect.suspend(() => bindProductTelemetryInvocation(telemetry, event).emit(event)).pipe(logDroppedEvent(event.name))).catch(() => {});
|
|
4328
|
+
});
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* A fresh OTP in the same browser tab reuses Openfort's localStorage and an
|
|
4332
|
+
* in-memory cached EOA unless the signer resets before the account lane runs.
|
|
4333
|
+
* Wallet readiness then starts on the fresh session, so Openfort's cycle (tens
|
|
4334
|
+
* of seconds on a new account) overlaps the profile and Organization forms
|
|
4335
|
+
* instead of running only when the claim finally asks for the signer address.
|
|
4336
|
+
*/
|
|
4337
|
+
function resetSignerForFreshSession(signer) {
|
|
4338
|
+
if (signer === void 0 || !("resetSession" in signer) || typeof signer.resetSession !== "function") return;
|
|
4339
|
+
try {
|
|
4340
|
+
signer.resetSession();
|
|
4341
|
+
} catch (cause) {
|
|
4342
|
+
throw signerFailure(signer.source, "resetSession", cause);
|
|
4343
|
+
}
|
|
4344
|
+
if (signer.statusStore?.status() === "unknown" && typeof signer.ensureWalletReady === "function") signer.ensureWalletReady().catch(() => {});
|
|
4345
|
+
}
|
|
4346
|
+
//#endregion
|
|
4347
|
+
//#region src/platform/client/identity-runtime.ts
|
|
4348
|
+
/**
|
|
4349
|
+
* The identity runtime `_internal.identity` hands the React bindings: the
|
|
4350
|
+
* actor's state, its facade runner, raw sends that answer a refusal as a
|
|
4351
|
+
* value, and the onboarding profile write behind the same boundary grammar.
|
|
4352
|
+
*/
|
|
4353
|
+
function makeIdentityRuntime(actor, identityPort, runPromise) {
|
|
4354
|
+
return {
|
|
4355
|
+
snapshot: actor.snapshot,
|
|
4356
|
+
subscribe: actor.subscribe,
|
|
4357
|
+
subscribeTransitions: actor.subscribeTransitions,
|
|
4358
|
+
runFacade: (verb, controls, run) => runIdentityFacade(verb, controls, run, runPromise),
|
|
4359
|
+
async send(event, controls) {
|
|
4360
|
+
try {
|
|
4361
|
+
return {
|
|
4362
|
+
ok: true,
|
|
4363
|
+
state: await runPromise(actor.ask(event, controls))
|
|
4364
|
+
};
|
|
4365
|
+
} catch (cause) {
|
|
4366
|
+
if (cause instanceof ActorFailure) return {
|
|
4367
|
+
ok: false,
|
|
4368
|
+
refused: cause.reason,
|
|
4369
|
+
state: actor.snapshot()
|
|
4370
|
+
};
|
|
4371
|
+
throw cause;
|
|
4372
|
+
}
|
|
4373
|
+
},
|
|
4374
|
+
async completeProfile(profile, controls) {
|
|
4375
|
+
if (typeof profile !== "object" || profile === null || typeof profile.displayName !== "string" || typeof profile.country !== "string" || typeof profile.handle !== "string") return {
|
|
4376
|
+
ok: false,
|
|
4377
|
+
reason: "INVALID_INPUT"
|
|
4378
|
+
};
|
|
4379
|
+
const displayName = profile.displayName.trim();
|
|
4380
|
+
if (displayName.length === 0) return {
|
|
4381
|
+
ok: false,
|
|
4382
|
+
reason: "INVALID_INPUT"
|
|
4383
|
+
};
|
|
4384
|
+
let country;
|
|
4385
|
+
try {
|
|
4386
|
+
country = toCountryCode(profile.country);
|
|
4387
|
+
} catch {
|
|
4388
|
+
return {
|
|
4389
|
+
ok: false,
|
|
4390
|
+
reason: "INVALID_INPUT"
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4393
|
+
const handle = profile.handle.trim();
|
|
4394
|
+
if (handle.length === 0) return {
|
|
4395
|
+
ok: false,
|
|
4396
|
+
reason: "INVALID_INPUT"
|
|
4397
|
+
};
|
|
4398
|
+
const carried = checkProfileCarriage(profile);
|
|
4399
|
+
if (!carried.ok) return {
|
|
4400
|
+
ok: false,
|
|
4401
|
+
reason: "INVALID_INPUT"
|
|
4402
|
+
};
|
|
4403
|
+
const session = actor.authSession();
|
|
4404
|
+
if (session === null) return {
|
|
4405
|
+
ok: false,
|
|
4406
|
+
reason: "NOT_AUTHENTICATED"
|
|
4407
|
+
};
|
|
4408
|
+
const result = await runPortEffect(identityPort.completeOnboarding({
|
|
4409
|
+
authUserId: session.authUserId,
|
|
4410
|
+
email: session.email,
|
|
4411
|
+
displayName,
|
|
4412
|
+
country,
|
|
4413
|
+
handle,
|
|
4414
|
+
...carried.carriage
|
|
4415
|
+
}), controls, onboardings.complete.name, runPromise);
|
|
4416
|
+
return result.ok ? { ok: true } : {
|
|
4417
|
+
ok: false,
|
|
4418
|
+
reason: result.error.code,
|
|
4419
|
+
error: result.error
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
//#endregion
|
|
4425
|
+
//#region src/platform/client/actor-lifetime.ts
|
|
4426
|
+
/**
|
|
4427
|
+
* Boot the identity actor in its own scope and attach every observer the
|
|
4428
|
+
* client keeps on it. `stopActor` detaches them and closes the scope once.
|
|
4429
|
+
*/
|
|
4430
|
+
function bootClientActor(input, effectRunner, observation) {
|
|
4431
|
+
const scope = effectRunner.runSync(Scope.make());
|
|
4432
|
+
const actor = withHostObservation(effectRunner.runSync(Scope.provide(bootIdentityFlow({
|
|
4433
|
+
ports: input.ports,
|
|
4434
|
+
chainId: input.bootstrap.chainId,
|
|
4435
|
+
requirement: input.requirement,
|
|
4436
|
+
...input.signer === void 0 ? {} : { signer: input.signer },
|
|
4437
|
+
...input.organizationSetup === void 0 ? {} : { organizationSetup: input.organizationSetup },
|
|
4438
|
+
...input.otpTtlMs === void 0 ? {} : { otpTtlMs: input.otpTtlMs },
|
|
4439
|
+
runPromise: effectRunner.runPromise
|
|
4440
|
+
}), scope)), observation.hostObservationSnapshot);
|
|
4441
|
+
const unsubscribeProductTelemetry = actor.subscribeTransitions((record) => {
|
|
4442
|
+
effectRunner.runPromise(executeIdentityProductObservation(input.ports.telemetry, record, actor.snapshot())).catch(() => {});
|
|
4443
|
+
});
|
|
4444
|
+
const failureObservation = observation.failureObservation;
|
|
4445
|
+
const unsubscribeFailureObservation = failureObservation === void 0 ? () => {} : actor.subscribeTransitions((record) => {
|
|
4446
|
+
if (record.outcome !== "failed") return;
|
|
4447
|
+
observeFailedResult({
|
|
4448
|
+
ok: false,
|
|
4449
|
+
error: transitionError(record)
|
|
4450
|
+
}, failureObservation, normalizeCapxulOperation(`identity.${record.event}`), "machine");
|
|
4451
|
+
});
|
|
4452
|
+
const unsubscribeSignerReadiness = startSignerReadinessOnClaim(actor, input.signer);
|
|
4453
|
+
const unsubscribeReadinessObservation = observeSignerReadinessCycles(input.signer, input.ports.telemetry, observation.hostObservationSnapshot, effectRunner.runPromise);
|
|
4454
|
+
let stopPromise = null;
|
|
4455
|
+
const stopActor = () => {
|
|
4456
|
+
if (stopPromise === null) {
|
|
4457
|
+
actor.invalidateAuthLifetime();
|
|
4458
|
+
unsubscribeProductTelemetry();
|
|
4459
|
+
unsubscribeFailureObservation();
|
|
4460
|
+
unsubscribeSignerReadiness();
|
|
4461
|
+
unsubscribeReadinessObservation();
|
|
4462
|
+
stopPromise = effectRunner.runPromise(Scope.close(scope, Exit.void));
|
|
4463
|
+
}
|
|
4464
|
+
return stopPromise;
|
|
4465
|
+
};
|
|
4466
|
+
if (input.signal !== void 0) input.signal.addEventListener("abort", () => void stopActor(), { once: true });
|
|
4467
|
+
return {
|
|
4468
|
+
actor,
|
|
4469
|
+
stopActor
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
//#endregion
|
|
4473
|
+
//#region src/platform/client/close.ts
|
|
4474
|
+
/** Join the assembled actor lifetime to the adapter lifetime for internal client factories. */
|
|
4475
|
+
function withAdapterClose(client, closeAdapter, runPromise) {
|
|
4476
|
+
const closeActor = client._internal.close;
|
|
4477
|
+
let closePromise;
|
|
4478
|
+
const close = () => {
|
|
4479
|
+
closePromise ??= Promise.allSettled([Promise.resolve().then(() => closeActor?.()), Promise.resolve().then(closeAdapter)]).then((results) => {
|
|
4480
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
4481
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
4482
|
+
});
|
|
4483
|
+
return closePromise;
|
|
4484
|
+
};
|
|
4485
|
+
return {
|
|
4486
|
+
...client,
|
|
4487
|
+
close: () => closeClientResources(close, runPromise),
|
|
4488
|
+
_internal: {
|
|
4489
|
+
...client._internal,
|
|
4490
|
+
close
|
|
4491
|
+
}
|
|
4492
|
+
};
|
|
4493
|
+
}
|
|
4494
|
+
/** Keep lifecycle failures on the same public result boundary as domain work. */
|
|
4495
|
+
const closeClientResources = (close, runPromise) => toCapxulResult(Effect.promise(close), Context.empty(), runPromise);
|
|
4496
|
+
//#endregion
|
|
4497
|
+
//#region src/platform/client/internal-surface.ts
|
|
4498
|
+
/**
|
|
4499
|
+
* The client's `_internal` surface: the identity runtime, the dev faucet, the
|
|
4500
|
+
* indexer seam, the setup receipt read and the telemetry hooks first-party
|
|
4501
|
+
* bindings use. None of it is public API.
|
|
4502
|
+
*/
|
|
4503
|
+
function makeClientInternals(input, shared) {
|
|
4504
|
+
return {
|
|
4505
|
+
identity: shared.identity,
|
|
4506
|
+
bootstrap: input.bootstrap,
|
|
4507
|
+
accounts: { fund: (...args) => shared.accounts.fund(...args) },
|
|
4508
|
+
...input.ports.indexerRead === void 0 ? {} : { indexer: input.ports.indexerRead },
|
|
4509
|
+
organizationSetup: { getProofReceipt: (orgId) => runPortEffect(callOperation(input.ports.convexCall, setups.getProofReceipt, { orgId }).pipe(Effect.mapError((publicError) => ({ publicError }))), void 0, CAPXUL_OPERATIONS.runtime.port, shared.runPromise) },
|
|
4510
|
+
telemetry: input.ports.telemetry,
|
|
4511
|
+
accountSetup: shared.accountSetup,
|
|
4512
|
+
captureExternalAddressAcknowledged: (address, organizationId) => {
|
|
4513
|
+
const telemetry = input.ports.telemetry;
|
|
4514
|
+
if (telemetry === void 0) return;
|
|
4515
|
+
shared.runPromise(Effect.suspend(() => telemetry.emit({
|
|
4516
|
+
name: "quick_pay_external_address_acknowledged",
|
|
4517
|
+
props: {
|
|
4518
|
+
recipient_locator_kind: "address",
|
|
4519
|
+
recipient_resolution_status: "external_address",
|
|
4520
|
+
recipient_address: address,
|
|
4521
|
+
external_address_acknowledged: true,
|
|
4522
|
+
...organizationId === void 0 ? {} : { organization_id: organizationId }
|
|
4523
|
+
}
|
|
4524
|
+
})).pipe(logDroppedEvent("quick_pay_external_address_acknowledged")));
|
|
4525
|
+
},
|
|
4526
|
+
close: shared.close
|
|
4527
|
+
};
|
|
4528
|
+
}
|
|
4529
|
+
//#endregion
|
|
4530
|
+
//#region src/platform/client/assemble.ts
|
|
4531
|
+
function assembleCapxulClient(input) {
|
|
4532
|
+
const observation = input;
|
|
4533
|
+
const authCache = input.authCache ?? detectAuthCacheAdapter();
|
|
4534
|
+
const accountSetup = input.accountSetup ?? "automatic";
|
|
4535
|
+
const effectRunner = input.effectRunner ?? {
|
|
4536
|
+
runSync: Effect.runSync,
|
|
4537
|
+
runPromise: Effect.runPromise
|
|
4538
|
+
};
|
|
4539
|
+
const { actor, stopActor } = bootClientActor(input, effectRunner, observation);
|
|
4540
|
+
const invocationControls = (controls) => withHostObservationControls(observation.hostObservationSnapshot, controls, actor.authSession()?.authUserId);
|
|
4541
|
+
const selectedOrgPort = input.orgPort;
|
|
4542
|
+
let kickProvisioning;
|
|
4543
|
+
const financialOps = makeFinancialOpsMethods({
|
|
4544
|
+
runPromise: effectRunner.runPromise,
|
|
4545
|
+
invocationControls,
|
|
4546
|
+
actor,
|
|
4547
|
+
chainId: input.bootstrap.chainId,
|
|
4548
|
+
convexCall: input.ports.convexCall,
|
|
4549
|
+
...input.signer === void 0 ? {} : { signer: input.signer },
|
|
4550
|
+
telemetry: input.ports.telemetry,
|
|
4551
|
+
requestKeyScope: `${input.bootstrap.convexUrl}:${String(input.bootstrap.chainId)}`,
|
|
4552
|
+
...input.ports.indexerRead === void 0 ? {} : { indexerRead: input.ports.indexerRead }
|
|
4553
|
+
});
|
|
4554
|
+
const accountInput = {
|
|
4555
|
+
actor,
|
|
4556
|
+
authCache,
|
|
4557
|
+
identityPort: input.ports.identity,
|
|
4558
|
+
smartAccountPort: input.ports.smartAccount,
|
|
4559
|
+
telemetry: input.ports.telemetry,
|
|
4560
|
+
chainId: input.bootstrap.chainId,
|
|
4561
|
+
requirement: input.requirement,
|
|
4562
|
+
accountReadPort: input.ports.accountRead,
|
|
4563
|
+
profileMethods: financialOps.me,
|
|
4564
|
+
convexCall: input.ports.convexCall,
|
|
4565
|
+
runPromise: effectRunner.runPromise,
|
|
4566
|
+
invocationControls,
|
|
4567
|
+
...input.signer === void 0 ? {} : { signer: input.signer }
|
|
4568
|
+
};
|
|
4569
|
+
const accountBundle = makeAccountMethods(accountInput);
|
|
4570
|
+
kickProvisioning = accountBundle.kickProvisioning;
|
|
4571
|
+
const account = accountBundle.methods;
|
|
4572
|
+
const authMethodsInput = {
|
|
4573
|
+
actor,
|
|
4574
|
+
authClient: input.ports.authClient,
|
|
4575
|
+
authCache,
|
|
4576
|
+
clock: input.ports.clock,
|
|
4577
|
+
telemetry: input.ports.telemetry,
|
|
4578
|
+
runPromise: effectRunner.runPromise,
|
|
4579
|
+
...input.otpTtlMs === void 0 ? {} : { otpTtlMs: input.otpTtlMs },
|
|
4580
|
+
...input.invokeTimeoutMs === void 0 ? {} : { invokeTimeoutMs: input.invokeTimeoutMs },
|
|
4581
|
+
afterVerifyOtp: async () => {
|
|
4582
|
+
resetSignerForFreshSession(input.signer);
|
|
4583
|
+
if (accountSetup === "explicit") return;
|
|
4584
|
+
kickProvisioning?.();
|
|
4585
|
+
}
|
|
4586
|
+
};
|
|
4587
|
+
const authCore = makeAuthMethods(authMethodsInput);
|
|
4588
|
+
const auth = {
|
|
4589
|
+
...authCore,
|
|
4590
|
+
clientRequests: makeClientRequestMethods({
|
|
4591
|
+
authBaseUrl: input.bootstrap.authBaseUrl,
|
|
4592
|
+
signIn: authCore.signIn,
|
|
4593
|
+
verifyOtp: authCore.verifyOtp
|
|
4594
|
+
})
|
|
4595
|
+
};
|
|
4596
|
+
const smartAccountInput = {
|
|
4597
|
+
actor,
|
|
4598
|
+
smartAccountPort: input.ports.smartAccount,
|
|
4599
|
+
chainId: input.bootstrap.chainId,
|
|
4600
|
+
sessionAuthUserIdFallback: null,
|
|
4601
|
+
telemetry: input.ports.telemetry,
|
|
4602
|
+
runPromise: effectRunner.runPromise
|
|
4603
|
+
};
|
|
4604
|
+
const smartAccount = makeSmartAccountMethods(smartAccountInput);
|
|
4605
|
+
const identity = makeIdentityMethods({
|
|
4606
|
+
identityPort: input.ports.identity,
|
|
4607
|
+
actor,
|
|
4608
|
+
convexCall: input.ports.convexCall,
|
|
4609
|
+
runPromise: effectRunner.runPromise
|
|
4610
|
+
});
|
|
4611
|
+
const onboarding = makeOnboardingMethods({
|
|
4612
|
+
authLifetime: actor.authLifetime,
|
|
4613
|
+
matchesAuthLifetime: actor.matchesAuthLifetime,
|
|
4614
|
+
restoreSession: (...args) => auth.getSession(...args),
|
|
4615
|
+
refreshIdentity: (options) => runCapxulEffect(askIdentity(actor, { _tag: "ReadSession" }, options).pipe(Effect.asVoid), options, CAPXUL_OPERATIONS.identity.readSession, effectRunner.runPromise),
|
|
4616
|
+
handleAvailable: (...args) => identity.handleAvailable(...args),
|
|
4617
|
+
completeProfile: (...args) => identity.completeProfile(...args),
|
|
4618
|
+
loadProfile: (...args) => identity.loadCurrent(...args),
|
|
4619
|
+
ensureAccount: (...args) => account._internal.ensureReady(...args),
|
|
4620
|
+
getLifecycle: (...args) => account.getLifecycle(...args),
|
|
4621
|
+
snapshotIdentity: actor.snapshot,
|
|
4622
|
+
subscribeIdentity: actor.subscribe,
|
|
4623
|
+
sendIdentity: (event, options) => runCapxulEffect(askIdentity(actor, event, options), options, CAPXUL_OPERATIONS._internal.identity.send, effectRunner.runPromise),
|
|
4624
|
+
...input.organizationSetup === void 0 ? {} : { organizationSetup: input.organizationSetup }
|
|
4625
|
+
});
|
|
4626
|
+
const system = makeSystemMethods({
|
|
4627
|
+
convexCall: input.ports.convexCall,
|
|
4628
|
+
runPromise: effectRunner.runPromise,
|
|
4629
|
+
invocationControls: (controls) => withHostObservationControls(observation.hostObservationSnapshot, controls)
|
|
4630
|
+
});
|
|
4631
|
+
const media = makeMediaMethods({
|
|
4632
|
+
convexCall: input.ports.convexCall,
|
|
4633
|
+
runPromise: effectRunner.runPromise,
|
|
4634
|
+
invocationControls
|
|
4635
|
+
});
|
|
4636
|
+
const holdings = makeHoldingsMethods({
|
|
4637
|
+
convexCall: input.ports.convexCall,
|
|
4638
|
+
runPromise: effectRunner.runPromise,
|
|
4639
|
+
invocationControls,
|
|
4640
|
+
...input.ports.indexerRead === void 0 ? {} : { indexerRead: input.ports.indexerRead }
|
|
4641
|
+
});
|
|
4642
|
+
const accountsInput = {
|
|
4643
|
+
accountReadPort: input.ports.accountRead,
|
|
4644
|
+
chainId: input.bootstrap.chainId,
|
|
4645
|
+
telemetry: input.ports.telemetry,
|
|
4646
|
+
runPromise: effectRunner.runPromise,
|
|
4647
|
+
invocationControls
|
|
4648
|
+
};
|
|
4649
|
+
const accounts = makeAccountsMethods(accountsInput);
|
|
4650
|
+
const orgMethods = makeOrgMethods({
|
|
4651
|
+
actor,
|
|
4652
|
+
chainId: input.bootstrap.chainId,
|
|
4653
|
+
telemetry: input.ports.telemetry,
|
|
4654
|
+
runPromise: effectRunner.runPromise,
|
|
4655
|
+
invocationControls,
|
|
4656
|
+
requestKeyScope: `${input.bootstrap.convexUrl}:${String(input.bootstrap.chainId)}`,
|
|
4657
|
+
...selectedOrgPort === void 0 ? {} : { orgPort: selectedOrgPort },
|
|
4658
|
+
convexCall: input.ports.convexCall,
|
|
4659
|
+
...input.signer === void 0 ? {} : { signer: input.signer },
|
|
4660
|
+
...input.organizationSetup === void 0 ? {} : { organizationSetup: input.organizationSetup }
|
|
4661
|
+
});
|
|
4662
|
+
const currentUser = makeCurrentUserMethods({
|
|
4663
|
+
me: financialOps.me,
|
|
4664
|
+
accounts,
|
|
4665
|
+
smartAccount,
|
|
4666
|
+
orgs: orgMethods.orgs
|
|
4667
|
+
});
|
|
4668
|
+
const identityRuntime = makeIdentityRuntime(actor, input.ports.identity, effectRunner.runPromise);
|
|
4669
|
+
const client = {
|
|
4670
|
+
checkouts: makeCheckoutMethods({
|
|
4671
|
+
convexCall: input.ports.convexCall,
|
|
4672
|
+
runPromise: effectRunner.runPromise,
|
|
4673
|
+
...input.signer === void 0 || input.ports.convexCall === void 0 ? {} : { execution: {
|
|
4674
|
+
actor,
|
|
4675
|
+
signer: input.signer,
|
|
4676
|
+
chainId: input.bootstrap.chainId,
|
|
4677
|
+
convexCall: input.ports.convexCall,
|
|
4678
|
+
runPromise: effectRunner.runPromise,
|
|
4679
|
+
telemetry: input.ports.telemetry
|
|
4680
|
+
} },
|
|
4681
|
+
invocationControls
|
|
4682
|
+
}),
|
|
4683
|
+
auth,
|
|
4684
|
+
smartAccount,
|
|
4685
|
+
identity,
|
|
4686
|
+
onboarding,
|
|
4687
|
+
system,
|
|
4688
|
+
account,
|
|
4689
|
+
accounts,
|
|
4690
|
+
currentUser,
|
|
4691
|
+
...financialOps,
|
|
4692
|
+
holdings,
|
|
4693
|
+
media,
|
|
4694
|
+
orgs: orgMethods.orgs,
|
|
4695
|
+
org: (...args) => orgMethods.org(...args),
|
|
4696
|
+
signer: input.signer?.statusStore ?? UNOBSERVABLE_SIGNER_STATUS,
|
|
4697
|
+
_internal: makeClientInternals(input, {
|
|
4698
|
+
identity: identityRuntime,
|
|
4699
|
+
accounts,
|
|
4700
|
+
accountSetup,
|
|
4701
|
+
runPromise: effectRunner.runPromise,
|
|
4702
|
+
close: stopActor
|
|
4703
|
+
}),
|
|
4704
|
+
close: () => closeClientResources(stopActor, effectRunner.runPromise)
|
|
4705
|
+
};
|
|
4706
|
+
return observeSdkClient(client, observation.failureObservation, observation.hostObservationSnapshot);
|
|
4707
|
+
}
|
|
4708
|
+
//#endregion
|
|
4709
|
+
//#region src/observability/diagnostics/effect-log.ts
|
|
4710
|
+
/**
|
|
4711
|
+
* Diagnostic breadcrumbs as SDK log records. The signer's readiness cycle
|
|
4712
|
+
* traces (`openfort.*`) used to reach only the browser console; through this
|
|
4713
|
+
* adapter they run in the client's own Effect context, so the engineering
|
|
4714
|
+
* telemetry layer ships them to PostHog Logs beside `identity.transition`.
|
|
4715
|
+
* Logging never throws back into the cycle that emitted it.
|
|
4716
|
+
*/
|
|
4717
|
+
var EffectLogDiagnosticAdapter = class {
|
|
4718
|
+
#runPromise;
|
|
4719
|
+
#console;
|
|
4720
|
+
constructor(context, consoleFallback) {
|
|
4721
|
+
this.#runPromise = Effect.runPromiseWith(context);
|
|
4722
|
+
this.#console = consoleFallback;
|
|
4723
|
+
}
|
|
4724
|
+
trace(scope, detail) {
|
|
4725
|
+
this.#console?.trace(scope, detail);
|
|
4726
|
+
try {
|
|
4727
|
+
this.#runPromise(Effect.logInfo(scope).pipe(Effect.annotateLogs({
|
|
4728
|
+
...detail,
|
|
4729
|
+
diagnostic_scope: scope
|
|
4730
|
+
}), Effect.ignoreCause)).catch(() => void 0);
|
|
4731
|
+
} catch {}
|
|
4732
|
+
}
|
|
4733
|
+
};
|
|
4734
|
+
//#endregion
|
|
4735
|
+
//#region src/observability/diagnostics/console.ts
|
|
4736
|
+
const DEFAULT_ACCOUNT_SETUP_LOG_PREFIX = "[capxul:account-setup]";
|
|
4737
|
+
var ConsoleDiagnosticAdapter = class {
|
|
4738
|
+
prefix;
|
|
4739
|
+
constructor(prefix = DEFAULT_ACCOUNT_SETUP_LOG_PREFIX) {
|
|
4740
|
+
this.prefix = prefix;
|
|
4741
|
+
}
|
|
4742
|
+
trace(scope, detail) {
|
|
4743
|
+
globalThis.console?.debug?.(`${this.prefix} ${scope}`, detail);
|
|
4744
|
+
}
|
|
4745
|
+
};
|
|
4746
|
+
//#endregion
|
|
4747
|
+
//#region src/signing/openfort-production.ts
|
|
4748
|
+
/** @internal Resolve the signer after production bootstrap without exposing its other fields. */
|
|
4749
|
+
function resolveOpenfortProductionSigner(input) {
|
|
4750
|
+
const bootstrap = input.bootstrap;
|
|
4751
|
+
let signer = input.signer ?? input.factory?.({
|
|
4752
|
+
authBaseUrl: input.authBaseUrl,
|
|
4753
|
+
openfortPublishableKey: bootstrap.openfortPublishableKey,
|
|
4754
|
+
shieldPublishableKey: bootstrap.shieldPublishableKey,
|
|
4755
|
+
...bootstrap.openfortAuthProvider === void 0 ? {} : { openfortAuthProvider: bootstrap.openfortAuthProvider }
|
|
4756
|
+
});
|
|
4757
|
+
if (signer === void 0 && input.runtime === "browser") signer = input.createBrowserSigner({
|
|
4758
|
+
...bootstrap,
|
|
4759
|
+
authBaseUrl: input.authBaseUrl
|
|
4760
|
+
}, {
|
|
4761
|
+
diagnostic: input.diagnostic,
|
|
4762
|
+
signUserOpHash: input.embeddedSign
|
|
4763
|
+
});
|
|
4764
|
+
if (signer === void 0 || input.finalSign === void 0) return signer;
|
|
4765
|
+
return {
|
|
4766
|
+
source: signer.source,
|
|
4767
|
+
getAddress: () => signer.getAddress(),
|
|
4768
|
+
signUserOpHash: (hash) => input.finalSign(hash, signer),
|
|
4769
|
+
...signer.statusStore === void 0 ? {} : { statusStore: signer.statusStore },
|
|
4770
|
+
...signer.ensureWalletReady === void 0 ? {} : { ensureWalletReady: signer.ensureWalletReady.bind(signer) },
|
|
4771
|
+
..."resetSession" in signer ? { resetSession: () => signer.resetSession() } : {}
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
//#endregion
|
|
4775
|
+
//#region src/platform/production-client.ts
|
|
4776
|
+
/** Shared production composition. The normal factory passes no controls. */
|
|
4777
|
+
async function createCapxulClientWithSignerControls(input, controls) {
|
|
4778
|
+
const invocationObservability = bindHostObservabilityInvocation(input.observability);
|
|
4779
|
+
const observation = invocationObservability?.failures ?? input.observation;
|
|
4780
|
+
const validation = validateCreateCapxulClientInput(input);
|
|
4781
|
+
if (!validation.ok) return observeFailedResult(validation, observation, "createCapxulClient");
|
|
4782
|
+
const resetSignerSession = { current: null };
|
|
4783
|
+
const adapters = await createProductionAdapters({
|
|
4784
|
+
...input,
|
|
4785
|
+
resetSignerSession,
|
|
4786
|
+
...invocationObservability === void 0 ? {} : { invocationObservability }
|
|
4787
|
+
});
|
|
4788
|
+
if (!adapters.ok) return observeFailedResult(adapters, observation, "createCapxulClient");
|
|
4789
|
+
try {
|
|
4790
|
+
const runtime = input.runtime ?? detectRuntime();
|
|
4791
|
+
const resolvedAuthBaseUrl = resolveBrowserAuthBaseUrl({
|
|
4792
|
+
bootstrapAuthBaseUrl: adapters.value.bootstrap.authBaseUrl,
|
|
4793
|
+
runtime,
|
|
4794
|
+
...input.authBaseUrl === void 0 ? {} : { override: input.authBaseUrl }
|
|
4795
|
+
});
|
|
4796
|
+
const signer = resolveOpenfortProductionSigner({
|
|
4797
|
+
bootstrap: adapters.value.bootstrap,
|
|
4798
|
+
runtime,
|
|
4799
|
+
authBaseUrl: resolvedAuthBaseUrl,
|
|
4800
|
+
createBrowserSigner: createOpenfortBrowserSignerFromBootstrap,
|
|
4801
|
+
diagnostic: new EffectLogDiagnosticAdapter(adapters.value.context, productionLocalDiagnostics(input) ? new ConsoleDiagnosticAdapter() : void 0),
|
|
4802
|
+
signer: input.signer,
|
|
4803
|
+
factory: input.openfortSignerFactory,
|
|
4804
|
+
finalSign: controls.signUserOpHash,
|
|
4805
|
+
embeddedSign: controls.signEmbeddedUserOpHash
|
|
4806
|
+
});
|
|
4807
|
+
const factoryChainFence = assertDevKeySignerIsTestnetOnly(signer, adapters.value.bootstrap.chainId);
|
|
4808
|
+
if (!factoryChainFence.ok) {
|
|
4809
|
+
await adapters.value.close().catch(() => void 0);
|
|
4810
|
+
return observeFailedResult(factoryChainFence, observation, "createCapxulClient");
|
|
4811
|
+
}
|
|
4812
|
+
if (signer !== void 0 && "resetSession" in signer) {
|
|
4813
|
+
const { resetSession, source } = signer;
|
|
4814
|
+
resetSignerSession.current = () => {
|
|
4815
|
+
try {
|
|
4816
|
+
resetSession();
|
|
4817
|
+
} catch (cause) {
|
|
4818
|
+
throw signerFailure(source, "resetSession", cause);
|
|
4819
|
+
}
|
|
4820
|
+
};
|
|
4821
|
+
}
|
|
4822
|
+
const effectRunner = {
|
|
4823
|
+
runSync: Effect.runSyncWith(adapters.value.context),
|
|
4824
|
+
runPromise: Effect.runPromiseWith(adapters.value.context)
|
|
4825
|
+
};
|
|
4826
|
+
const ownedClient = withAdapterClose(assembleCapxulClient({
|
|
4827
|
+
ports: adapters.value.ports,
|
|
4828
|
+
bootstrap: adapters.value.bootstrap,
|
|
4829
|
+
authCache: adapters.value.ports.authCache,
|
|
4830
|
+
requirement: input.requirement ?? "none",
|
|
4831
|
+
accountSetup: input.accountSetup,
|
|
4832
|
+
...signer === void 0 ? {} : { signer },
|
|
4833
|
+
orgPort: Context.get(adapters.value.context, OrgPortTag),
|
|
4834
|
+
organizationSetup: new ConvexOrganizationSetupAdapter({
|
|
4835
|
+
convex: adapters.value.ports.convexCall,
|
|
4836
|
+
...signer === void 0 ? {} : { signer },
|
|
4837
|
+
chainId: adapters.value.bootstrap.chainId,
|
|
4838
|
+
runPromise: effectRunner.runPromise
|
|
4839
|
+
}),
|
|
4840
|
+
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
4841
|
+
...input.otpTtlMs === void 0 ? {} : { otpTtlMs: input.otpTtlMs },
|
|
4842
|
+
invokeTimeoutMs: input.invokeTimeoutMs ?? 3e4,
|
|
4843
|
+
...input.observability?.failures === void 0 && input.observation === void 0 ? {} : { failureObservation: input.observability?.failures ?? input.observation },
|
|
4844
|
+
...input.observability === void 0 ? {} : { hostObservationSnapshot: () => snapshotHostObservability(input.observability) },
|
|
4845
|
+
effectRunner
|
|
4846
|
+
}), adapters.value.close, effectRunner.runPromise);
|
|
4847
|
+
const close = idempotentClose(async () => {
|
|
4848
|
+
try {
|
|
4849
|
+
if (signer !== void 0 && "resetSession" in signer) try {
|
|
4850
|
+
signer.resetSession();
|
|
4851
|
+
} catch (cause) {
|
|
4852
|
+
throw signerFailure(signer.source, "resetSession", cause);
|
|
4853
|
+
}
|
|
4854
|
+
} finally {
|
|
4855
|
+
await ownedClient._internal.close?.();
|
|
4856
|
+
}
|
|
4857
|
+
});
|
|
4858
|
+
return {
|
|
4859
|
+
ok: true,
|
|
4860
|
+
value: {
|
|
4861
|
+
...ownedClient,
|
|
4862
|
+
close: () => closeClientResources(close, effectRunner.runPromise),
|
|
4863
|
+
_internal: {
|
|
4864
|
+
...ownedClient._internal,
|
|
4865
|
+
close
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
};
|
|
4869
|
+
} catch (cause) {
|
|
4870
|
+
await adapters.value.close().catch(() => void 0);
|
|
4871
|
+
return observeFailedResult({
|
|
4872
|
+
ok: false,
|
|
4873
|
+
error: toPublicError(cause)
|
|
4874
|
+
}, observation, "createCapxulClient");
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
const SDK_VERSION = version;
|
|
4878
|
+
async function createCapxulClient(input) {
|
|
4879
|
+
return createCapxulClientWithSignerControls(input, {});
|
|
4880
|
+
}
|
|
4881
|
+
//#endregion
|
|
4882
|
+
export { cloneTelemetryEvent as a, identityErrorFromCapxul as c, devPrivateKeySigner as d, createOpenfortBrowserSigner as f, openfortEmbeddedWalletPort as h, postHogObservability as i, bootstrapErrorFromCapxul as l, openfortEmbeddedSignerFromWallet as m, createCapxulClientWithSignerControls as n, cloneTelemetryProps as o, embeddedSigner as p, assembleCapxulClient as r, IdentityPortTag as s, createCapxulClient as t, deriveDevPrivateKey as u };
|