@capxul/sdk-react 0.0.0-rc-2cdddbb91

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.
@@ -0,0 +1,1108 @@
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";
3
+ import { CapxulError, createCapxulClient, isCapxulError, resolveIdentityDestination, toCountryCode, toHandle, toOrgId } from "@capxul/sdk";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/platform/bootstrap-context.tsx
6
+ const CapxulBootstrapContext = createContext(null);
7
+ function CapxulBootstrapProvider({ value, children }) {
8
+ return /* @__PURE__ */ jsx(CapxulBootstrapContext.Provider, {
9
+ value,
10
+ children
11
+ });
12
+ }
13
+ function useCapxul() {
14
+ const state = useContext(CapxulBootstrapContext);
15
+ if (state === null) throw new Error("useCapxul must be used within <CapxulProvider>");
16
+ return state;
17
+ }
18
+ //#endregion
19
+ //#region src/platform/client-context.tsx
20
+ const MISSING_CAPXUL_CLIENT_PROVIDER = Symbol("MISSING_CAPXUL_CLIENT_PROVIDER");
21
+ const CapxulClientContext = createContext(MISSING_CAPXUL_CLIENT_PROVIDER);
22
+ function CapxulClientProvider({ client, children }) {
23
+ return /* @__PURE__ */ jsx(CapxulClientContext.Provider, {
24
+ value: client,
25
+ children
26
+ });
27
+ }
28
+ /**
29
+ * Returns the client, or `null` while `<CapxulProvider>` is still bootstrapping.
30
+ * Data hooks use this so they can sit in `isPending` (disabled query) until the
31
+ * client resolves, rather than throwing during bootstrap.
32
+ */
33
+ function useCapxulClientOrNull() {
34
+ const client = useContext(CapxulClientContext);
35
+ if (client === MISSING_CAPXUL_CLIENT_PROVIDER) throw new Error("useCapxulClient must be used within <CapxulProvider>");
36
+ return client;
37
+ }
38
+ //#endregion
39
+ //#region src/platform/query-keys.ts
40
+ /**
41
+ * An Organization id a consumer passed as a plain string (often a URL
42
+ * parameter), as a key segment. An absent or empty id names no Organization.
43
+ */
44
+ function orgKeySegment(orgId) {
45
+ return orgId === void 0 || orgId === "" ? void 0 : toOrgId(orgId);
46
+ }
47
+ /**
48
+ * The account a read is answered FOR, as a key segment (#2007 R19).
49
+ *
50
+ * A read whose answer depends on who is asking belongs to the (scope, account)
51
+ * pair, not to the scope. Replacing one claimed account with another is not a
52
+ * state-label change. Keying is what
53
+ * makes the new person miss rather than read the previous person's answer.
54
+ * `undefined` while no account is claimed — no such read may run then anyway.
55
+ */
56
+ function actingAccount(identity) {
57
+ return identity !== null && identity.phase === "authenticated" && identity.account.at === "claimed" ? identity.account.address : void 0;
58
+ }
59
+ /** Keep invalidation prefixes shared, but never share a personal result across accounts.
60
+ * Session reads also run before an Account is claimed (for example `orgs()`).
61
+ */
62
+ function accountKey(key, identity) {
63
+ return [...key, actingAccount(identity) ?? (identity?.phase === "authenticated" ? identity.session.authUserId : null)];
64
+ }
65
+ const capxulKeys = {
66
+ root: ["capxul"],
67
+ profile: ["capxul", "profile"],
68
+ handleAvailability: (handle) => [
69
+ "capxul",
70
+ "profile",
71
+ "handle-availability",
72
+ handle
73
+ ],
74
+ account: ["capxul", "account"],
75
+ provisioning: ["capxul", "provisioning"],
76
+ binding: ["capxul", "binding"],
77
+ accountBalance: ["capxul", "accountBalance"],
78
+ orgs: ["capxul", "orgs"],
79
+ org: (orgId) => [
80
+ "capxul",
81
+ "org",
82
+ orgId ?? "pending"
83
+ ],
84
+ orgMembers: (orgId) => [
85
+ "capxul",
86
+ "org",
87
+ orgId ?? "pending",
88
+ "members"
89
+ ],
90
+ orgRoles: (orgId) => [
91
+ "capxul",
92
+ "org",
93
+ orgId ?? "pending",
94
+ "roles"
95
+ ],
96
+ orgTreasury: (orgId) => [
97
+ "capxul",
98
+ "org",
99
+ orgId ?? "pending",
100
+ "treasury"
101
+ ],
102
+ orgPermissions: (orgId) => [
103
+ "capxul",
104
+ "org",
105
+ orgId ?? "pending",
106
+ "permissions"
107
+ ],
108
+ orgMe: (orgId, account) => [
109
+ "capxul",
110
+ "org",
111
+ orgId ?? "pending",
112
+ "me",
113
+ account ?? "pending"
114
+ ],
115
+ orgLifecycle: (orgId) => [
116
+ "capxul",
117
+ "org",
118
+ orgId ?? "pending",
119
+ "lifecycle"
120
+ ],
121
+ contacts: (scope) => [
122
+ "capxul",
123
+ "contacts",
124
+ scope
125
+ ],
126
+ contactsList: (scope, includeHidden) => [
127
+ "capxul",
128
+ "contacts",
129
+ scope,
130
+ includeHidden ? "all" : "visible"
131
+ ],
132
+ payrollRuns: (orgId) => [
133
+ "capxul",
134
+ "org",
135
+ orgId ?? "pending",
136
+ "payroll",
137
+ "runs"
138
+ ],
139
+ payrollGroups: (orgId) => [
140
+ "capxul",
141
+ "org",
142
+ orgId ?? "pending",
143
+ "payroll",
144
+ "groups"
145
+ ],
146
+ payrollTerms: (orgId) => [
147
+ "capxul",
148
+ "org",
149
+ orgId ?? "pending",
150
+ "payroll",
151
+ "terms"
152
+ ],
153
+ activity: ["capxul", "activity"],
154
+ activityDetail: (reference, actor) => [
155
+ "capxul",
156
+ "activity",
157
+ actor ?? "self",
158
+ reference ?? "pending"
159
+ ],
160
+ activitySummary: (actor, window) => [
161
+ "capxul",
162
+ "activity",
163
+ "summary",
164
+ actor ?? "self",
165
+ window ?? "all"
166
+ ],
167
+ holdings: (actor) => [
168
+ "capxul",
169
+ "holdings",
170
+ actor ?? "self"
171
+ ],
172
+ payments: ["capxul", "payments"],
173
+ payment: (paymentId) => [
174
+ "capxul",
175
+ "payments",
176
+ paymentId ?? "pending"
177
+ ]
178
+ };
179
+ //#endregion
180
+ //#region src/platform/invocation-controls.ts
181
+ /**
182
+ * Keep only the five public option fields, and only when they hold the right
183
+ * kind of value. A named action verb takes its attempt as an optional FIRST
184
+ * argument, so a screen that wires one straight to a DOM handler
185
+ * (`onClick={row.hide}`) hands us a React SyntheticEvent. That is not an
186
+ * attempt: it must reach neither the SDK call nor the callback echo.
187
+ */
188
+ function toInvocationOptions(options) {
189
+ if (typeof options !== "object" || options === null) return void 0;
190
+ const attempt = {};
191
+ if (typeof options.correlationId === "string") attempt.correlationId = options.correlationId;
192
+ if (typeof options.journeyId === "string") attempt.journeyId = options.journeyId;
193
+ if (typeof options.timeoutMs === "number") attempt.timeoutMs = options.timeoutMs;
194
+ if (typeof options.deadlineMs === "number") attempt.deadlineMs = options.deadlineMs;
195
+ if (options.signal instanceof AbortSignal) attempt.signal = options.signal;
196
+ return Object.keys(attempt).length === 0 ? void 0 : attempt;
197
+ }
198
+ /**
199
+ * Map the public camel-case invocation options onto the Core SDK's control
200
+ * spelling. ONE definition, because the identity facade and every headless
201
+ * engine must hand the SDK the same shape: a caller that captured one user
202
+ * attempt before it started gets that attempt's `correlation_id`/`journey_id`
203
+ * on whichever operation it starts (#1960 R02).
204
+ *
205
+ * It creates no identifiers. An absent option stays absent, so an automatic
206
+ * read keeps its own invocation context instead of borrowing an attempt's.
207
+ */
208
+ function toInvocationControls(options) {
209
+ if (options === void 0) return void 0;
210
+ return {
211
+ ...options.signal === void 0 ? {} : { signal: options.signal },
212
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
213
+ ...options.deadlineMs === void 0 ? {} : { deadlineMs: options.deadlineMs },
214
+ ...options.correlationId === void 0 ? {} : { correlation_id: options.correlationId },
215
+ ...options.journeyId === void 0 ? {} : { journey_id: options.journeyId }
216
+ };
217
+ }
218
+ //#endregion
219
+ //#region src/platform/identity-lanes.ts
220
+ const failure = (result) => result.ok ? null : {
221
+ ok: false,
222
+ reason: result.refused
223
+ };
224
+ /** A superseded session read waits at most this long for the winning read. */
225
+ const SESSION_SETTLE_LIMIT_MS = 3e4;
226
+ const accountInFlight = (state) => state.phase === "authenticated" && (state.account.at === "deriving" || state.account.at === "claiming");
227
+ /**
228
+ * The session slot keeps the latest ReadSession only: a second reader
229
+ * interrupts the first and the machine refuses the first with SUPERSEDED.
230
+ * Two onboarding callers read at once after a reload (the controller's
231
+ * resume and the recovery submit), so the loser must wait for the winner's
232
+ * read to land and then continue on the shared state, not fail.
233
+ */
234
+ function settledSession(runtime, invocation) {
235
+ return new Promise((resolve) => {
236
+ let done = false;
237
+ let stop = null;
238
+ let timer = null;
239
+ const onAbort = () => finish({
240
+ ok: false,
241
+ reason: "CANCELLED"
242
+ });
243
+ const finish = (result) => {
244
+ if (done) return;
245
+ done = true;
246
+ stop?.();
247
+ if (timer !== null) clearTimeout(timer);
248
+ invocation?.signal?.removeEventListener("abort", onAbort);
249
+ resolve(result);
250
+ };
251
+ if (invocation?.signal?.aborted) {
252
+ onAbort();
253
+ return;
254
+ }
255
+ invocation?.signal?.addEventListener("abort", onAbort, { once: true });
256
+ const unsubscribe = runtime.subscribeTransitions((record) => {
257
+ if (record.slot !== "identity:session") return;
258
+ if (record.outcome === "applied" && record.event === "SessionRead") finish({ ok: true });
259
+ else if (record.outcome === "failed" || record.outcome === "cancelled") finish({
260
+ ok: false,
261
+ reason: record.error_code
262
+ });
263
+ });
264
+ if (done) {
265
+ unsubscribe();
266
+ return;
267
+ }
268
+ stop = unsubscribe;
269
+ timer = setTimeout(() => finish({
270
+ ok: false,
271
+ reason: "UNKNOWN",
272
+ error: new CapxulError("UNKNOWN", "The session did not settle in time.", {
273
+ mode: "timeout",
274
+ layer: "identity",
275
+ details: { limitMs: SESSION_SETTLE_LIMIT_MS }
276
+ })
277
+ }), SESSION_SETTLE_LIMIT_MS);
278
+ });
279
+ }
280
+ /** Read the session; a superseded read waits for the winning one. */
281
+ async function readSession(runtime, invocation) {
282
+ const result = await runtime.send({ _tag: "ReadSession" }, invocation);
283
+ const refused = failure(result);
284
+ if (refused === null) return { ok: true };
285
+ return refused.reason === "SUPERSEDED" ? settledSession(runtime, invocation) : refused;
286
+ }
287
+ /**
288
+ * The account lane accepts no account event while a derivation or claim is
289
+ * landing. A person who submits the Organization before their own Safe claim
290
+ * settles must wait for it, not be refused with WRONG_STATE.
291
+ */
292
+ function settledAccount(runtime, invocation) {
293
+ return new Promise((resolve) => {
294
+ let done = false;
295
+ let stop = null;
296
+ const onAbort = () => finish({
297
+ ok: false,
298
+ reason: "CANCELLED"
299
+ });
300
+ const finish = (result) => {
301
+ if (done) return;
302
+ done = true;
303
+ stop?.();
304
+ invocation?.signal?.removeEventListener("abort", onAbort);
305
+ resolve(result);
306
+ };
307
+ if (invocation?.signal?.aborted) {
308
+ onAbort();
309
+ return;
310
+ }
311
+ invocation?.signal?.addEventListener("abort", onAbort, { once: true });
312
+ const unsubscribe = runtime.subscribe((state) => {
313
+ if (!accountInFlight(state)) finish({ ok: true });
314
+ });
315
+ if (done) {
316
+ unsubscribe();
317
+ return;
318
+ }
319
+ stop = unsubscribe;
320
+ if (!accountInFlight(runtime.snapshot())) finish({ ok: true });
321
+ });
322
+ }
323
+ /** Drive the person's own Account to claimed, waiting out a landing derivation or claim. */
324
+ async function reachClaimed(runtime, invocation) {
325
+ let state = runtime.snapshot();
326
+ if (accountInFlight(state)) {
327
+ const settled = await settledAccount(runtime, invocation);
328
+ if (!settled.ok) return settled;
329
+ state = runtime.snapshot();
330
+ }
331
+ if (state.phase !== "authenticated" || state.account.at === "unknown") {
332
+ const result = await runtime.send({ _tag: "EnsureAccount" }, invocation);
333
+ const refused = failure(result);
334
+ if (refused !== null) return refused;
335
+ state = result.state;
336
+ }
337
+ if (state.phase !== "authenticated") return {
338
+ ok: false,
339
+ reason: "WRONG_STATE"
340
+ };
341
+ if (state.account.at === "claimed") return { ok: true };
342
+ if (accountInFlight(state)) {
343
+ const settled = await settledAccount(runtime, invocation);
344
+ if (!settled.ok) return settled;
345
+ state = runtime.snapshot();
346
+ if (state.phase !== "authenticated") return {
347
+ ok: false,
348
+ reason: "WRONG_STATE"
349
+ };
350
+ if (state.account.at === "claimed") return { ok: true };
351
+ }
352
+ const event = state.account.at === "failed" ? { _tag: "RetryAccount" } : state.account.at === "counterfactual" ? { _tag: "ClaimAccount" } : { _tag: "EnsureAccount" };
353
+ const result = await runtime.send(event, invocation);
354
+ const refused = failure(result);
355
+ if (refused !== null) return refused;
356
+ const next = result.state;
357
+ return next.phase === "authenticated" && next.account.at === "claimed" ? { ok: true } : {
358
+ ok: false,
359
+ reason: "WRONG_STATE"
360
+ };
361
+ }
362
+ //#endregion
363
+ //#region src/platform/organization-submission.ts
364
+ function organizationFailure(reason, state) {
365
+ const stateFailure = state.phase === "faulted" ? state.failure : state.phase === "authenticated" && state.account.at === "failed" ? state.account.failure : state.phase === "authenticated" && state.account.at === "claimed" ? state.account.org?.failure : void 0;
366
+ if (stateFailure?.code === reason) return {
367
+ ok: false,
368
+ reason,
369
+ error: stateFailure.error ?? new CapxulError(stateFailure.code === "WORK_DIED" ? "UNKNOWN" : stateFailure.code, stateFailure.message, {
370
+ ...stateFailure.mode === void 0 ? {} : { details: { failure_mode: stateFailure.mode } },
371
+ layer: "identity"
372
+ })
373
+ };
374
+ return {
375
+ ok: false,
376
+ reason,
377
+ error: new CapxulError(reason === "WORK_DIED" ? "UNKNOWN" : reason, reason === "INVALID_INPUT" ? "Organization details are invalid." : "Organization creation failed.", { layer: "identity" })
378
+ };
379
+ }
380
+ function normalizeOrganization(organization) {
381
+ const name = typeof organization.name === "string" ? organization.name.trim() : "";
382
+ const handle = typeof organization.handle === "string" ? organization.handle.trim().toLowerCase() : "";
383
+ if (name.length === 0) return null;
384
+ if (organization.bio !== void 0 && typeof organization.bio !== "string") return null;
385
+ if (organization.size !== void 0 && typeof organization.size !== "string") return null;
386
+ if (organization.logoStorageId !== void 0 && (typeof organization.logoStorageId !== "string" || organization.logoStorageId.length === 0)) return null;
387
+ try {
388
+ return {
389
+ name,
390
+ handle: toHandle(handle),
391
+ country: toCountryCode(organization.country),
392
+ ...organization.bio === void 0 ? {} : { bio: organization.bio },
393
+ ...organization.size === void 0 ? {} : { size: organization.size },
394
+ ...organization.logoStorageId === void 0 ? {} : { logoStorageId: organization.logoStorageId }
395
+ };
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+ /** The Organization id when its lane is already running or finished, else null. */
401
+ const runningOrganization = (state) => {
402
+ if (state.phase !== "authenticated" || state.account.at !== "claimed") return null;
403
+ return state.account.org?.orgId ?? null;
404
+ };
405
+ //#endregion
406
+ //#region src/platform/identity-auth.ts
407
+ async function guarded(runtime, verb, options, run) {
408
+ const invocation = toInvocationControls(options);
409
+ try {
410
+ return await (runtime.runFacade?.(verb, invocation, run) ?? run(invocation));
411
+ } catch (cause) {
412
+ const error = isCapxulError(cause) ? cause : new CapxulError("UNKNOWN", "The identity request failed unexpectedly.", {
413
+ cause,
414
+ layer: "identity"
415
+ });
416
+ return {
417
+ ok: false,
418
+ reason: error.code,
419
+ error
420
+ };
421
+ }
422
+ }
423
+ /** The auth verbs behind `useCapxulAuth`, each run as one guarded facade call. */
424
+ function createAuth(client, clearAuthenticatedQueries) {
425
+ const runtime = client._internal.identity;
426
+ const completeProfile = async (profile, invocation) => {
427
+ const result = await runtime.completeProfile(profile, invocation);
428
+ return result.ok ? { ok: true } : result;
429
+ };
430
+ const prepareOrganization = async (submission, invocation) => {
431
+ const organization = normalizeOrganization(submission.organization);
432
+ if (organization === null) return {
433
+ ok: false,
434
+ reason: "INVALID_INPUT"
435
+ };
436
+ const refreshed = await readSession(runtime, invocation);
437
+ if (!refreshed.ok) return refreshed;
438
+ const current = runtime.snapshot();
439
+ if (current.phase !== "authenticated" || !current.profileComplete) {
440
+ const completed = await runtime.completeProfile(submission.profileDetails, invocation);
441
+ if (!completed.ok) return completed;
442
+ }
443
+ const claimed = await reachClaimed(runtime, invocation);
444
+ return claimed.ok ? {
445
+ ok: true,
446
+ organization
447
+ } : claimed;
448
+ };
449
+ return {
450
+ requestCode: (email, options) => guarded(runtime, "requestCode", options, async (invocation) => {
451
+ const result = await client.auth.signIn({ email }, invocation);
452
+ if (!result.ok) return {
453
+ ok: false,
454
+ reason: result.error.code
455
+ };
456
+ const state = runtime.snapshot();
457
+ return state.phase === "otp_pending" ? {
458
+ ok: true,
459
+ requestedAt: state.requestedAt
460
+ } : {
461
+ ok: false,
462
+ reason: state.phase === "faulted" ? state.failure.code : "UNKNOWN"
463
+ };
464
+ }),
465
+ verifyCode: (otp, options) => guarded(runtime, "verifyCode", options, async (invocation) => {
466
+ const state = runtime.snapshot();
467
+ const email = state.phase === "otp_pending" ? state.email : state.phase === "faulted" && state.resume !== null ? state.resume.email : "";
468
+ if (!/^\d{6}$/.test(otp)) {
469
+ const refused = await runtime.send({
470
+ _tag: "VerifyOtp",
471
+ email,
472
+ otp,
473
+ now: Date.now()
474
+ }, invocation);
475
+ return failure(refused) ?? {
476
+ ok: false,
477
+ reason: "UNKNOWN"
478
+ };
479
+ }
480
+ const result = await client.auth.verifyOtp({
481
+ email,
482
+ code: otp
483
+ }, invocation);
484
+ if (!result.ok) return {
485
+ ok: false,
486
+ reason: result.error.code
487
+ };
488
+ const next = runtime.snapshot();
489
+ return next.phase === "authenticated" ? {
490
+ ok: true,
491
+ authUserId: next.session.authUserId,
492
+ profileComplete: next.profileComplete
493
+ } : {
494
+ ok: false,
495
+ reason: next.phase === "faulted" ? next.failure.code : "UNKNOWN"
496
+ };
497
+ }),
498
+ signOut: (options) => guarded(runtime, "signOut", options, async (invocation) => {
499
+ const result = await client.auth.signOut(invocation);
500
+ if (!result.ok) return {
501
+ ok: false,
502
+ reason: result.error.code
503
+ };
504
+ await clearAuthenticatedQueries();
505
+ return { ok: true };
506
+ }),
507
+ completePersonal: (profile, options) => guarded(runtime, "completePersonal", options, async (invocation) => {
508
+ const completed = await completeProfile(profile, invocation);
509
+ if (!completed.ok) return {
510
+ ok: false,
511
+ reason: completed.reason
512
+ };
513
+ const refreshed = await readSession(runtime, invocation);
514
+ if (!refreshed.ok) return refreshed;
515
+ return failure(await runtime.send({ _tag: "EnsureAccount" }, invocation)) ?? { ok: true };
516
+ }),
517
+ createOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
518
+ const current = runtime.snapshot();
519
+ const running = runningOrganization(current);
520
+ if (running !== null) return {
521
+ ok: true,
522
+ orgId: running
523
+ };
524
+ const prepared = await prepareOrganization(submission, invocation);
525
+ if (!prepared.ok) return prepared;
526
+ const opened = runningOrganization(runtime.snapshot());
527
+ if (opened !== null) return {
528
+ ok: true,
529
+ orgId: opened
530
+ };
531
+ const begun = await client.onboarding.beginOrganization(prepared.organization, invocation);
532
+ if (!begun.ok) return {
533
+ ok: false,
534
+ reason: begun.error.code,
535
+ error: begun.error
536
+ };
537
+ const { orgId, setup } = begun.value;
538
+ if (setup.status === "awaitingAuthorization") {
539
+ const authorized = await client.org(orgId).setup.authorize(invocation);
540
+ if (!authorized.ok) return {
541
+ ok: false,
542
+ reason: authorized.error.code,
543
+ error: authorized.error
544
+ };
545
+ }
546
+ return {
547
+ ok: true,
548
+ orgId: String(orgId)
549
+ };
550
+ }).then((result) => {
551
+ if (result.ok) return result;
552
+ if ("error" in result && result.error instanceof CapxulError) return {
553
+ ok: false,
554
+ reason: result.reason,
555
+ error: result.error
556
+ };
557
+ return organizationFailure(result.reason, runtime.snapshot());
558
+ }),
559
+ retry: (options) => guarded(runtime, "retry", options, async (invocation) => {
560
+ const state = runtime.snapshot();
561
+ const orgId = runningOrganization(state);
562
+ if (orgId !== null) {
563
+ const scoped = client.org(toOrgId(orgId));
564
+ const current = await scoped.setup.get(invocation);
565
+ if (!current.ok) return {
566
+ ok: false,
567
+ reason: current.error.code
568
+ };
569
+ const result = current.value.status === "awaitingAuthorization" ? await scoped.setup.authorize(invocation) : await scoped.setup.resume(invocation);
570
+ return result.ok ? { ok: true } : {
571
+ ok: false,
572
+ reason: result.error.code
573
+ };
574
+ }
575
+ const result = await runtime.send({ _tag: "RetryAccount" }, invocation);
576
+ return failure(result) ?? { ok: true };
577
+ }),
578
+ resumeSubmittedOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
579
+ const prepared = await prepareOrganization(submission, invocation);
580
+ return prepared.ok ? { ok: true } : {
581
+ ok: false,
582
+ reason: prepared.reason
583
+ };
584
+ })
585
+ };
586
+ }
587
+ //#endregion
588
+ //#region src/platform/identity.tsx
589
+ const MISSING_IDENTITY_PROVIDER = Symbol("MISSING_IDENTITY_PROVIDER");
590
+ const IdentityContext = createContext(MISSING_IDENTITY_PROVIDER);
591
+ function CapxulIdentityProvider({ client, children }) {
592
+ const queryClient = useQueryClient();
593
+ const runtime = client?._internal.identity ?? null;
594
+ const listeners = useRef(/* @__PURE__ */ new Set());
595
+ useEffect(() => {
596
+ if (client === null || runtime === null) return;
597
+ const controller = new AbortController();
598
+ let active = true;
599
+ Promise.resolve().then(async () => {
600
+ const restored = await client.auth.getSession({ signal: controller.signal });
601
+ if (!active || !restored.ok) return;
602
+ let state = runtime.snapshot();
603
+ if (state.phase === "authenticated" && state.account.at === "unknown") {
604
+ await runtime.send({ _tag: "ReadSession" }, { signal: controller.signal });
605
+ if (!active) return;
606
+ state = runtime.snapshot();
607
+ }
608
+ if (client._internal.accountSetup !== "explicit" && state.phase === "authenticated" && state.profileComplete && state.account.at === "unknown") await runtime.send({ _tag: "EnsureAccount" }, { signal: controller.signal });
609
+ }).catch(() => void 0);
610
+ return () => {
611
+ active = false;
612
+ controller.abort();
613
+ };
614
+ }, [client, runtime]);
615
+ useEffect(() => {
616
+ if (runtime === null) return;
617
+ let previousAccount = actingAccount(runtime.snapshot());
618
+ return runtime.subscribeTransitions((record) => {
619
+ const state = runtime.snapshot();
620
+ const account = actingAccount(state);
621
+ const accountChanged = previousAccount !== void 0 && account !== void 0 && account !== previousAccount;
622
+ previousAccount = account;
623
+ if (record.outcome === "applied" && (record.from !== record.to || accountChanged) && (record.to === "authenticated:claimed" || record.to === "authenticated:claimed:organization")) queryClient.invalidateQueries({
624
+ queryKey: capxulKeys.root,
625
+ ...accountChanged ? { refetchType: "none" } : {}
626
+ });
627
+ for (const listener of listeners.current) try {
628
+ listener(record, state);
629
+ } catch {
630
+ listeners.current.delete(listener);
631
+ }
632
+ });
633
+ }, [queryClient, runtime]);
634
+ const addTransitionListener = useCallback((listener) => {
635
+ listeners.current.add(listener);
636
+ return () => listeners.current.delete(listener);
637
+ }, []);
638
+ const value = useMemo(() => {
639
+ if (client === null || runtime === null) return null;
640
+ const send = (event, options) => runtime.send(event, toInvocationControls(options));
641
+ const clearAuthenticatedQueries = async () => {
642
+ await queryClient.cancelQueries({ queryKey: capxulKeys.root });
643
+ await queryClient.resetQueries({ queryKey: capxulKeys.root });
644
+ };
645
+ const auth = createAuth(client, clearAuthenticatedQueries);
646
+ return {
647
+ runtime,
648
+ send,
649
+ auth,
650
+ resumeSubmittedOrganization: auth.resumeSubmittedOrganization,
651
+ addTransitionListener
652
+ };
653
+ }, [
654
+ client,
655
+ runtime,
656
+ addTransitionListener,
657
+ queryClient
658
+ ]);
659
+ return /* @__PURE__ */ jsx(IdentityContext.Provider, {
660
+ value,
661
+ children
662
+ });
663
+ }
664
+ function useIdentityContext() {
665
+ const value = useContext(IdentityContext);
666
+ if (value === MISSING_IDENTITY_PROVIDER) throw new Error("identity hooks must be used within <CapxulProvider>");
667
+ if (value === null) throw new Error("identity hooks require a ready <CapxulProvider>");
668
+ return value;
669
+ }
670
+ const noSubscribe = () => () => void 0;
671
+ const noState = () => null;
672
+ function useCapxulIdentityOrNull() {
673
+ const value = useContext(IdentityContext);
674
+ if (value === MISSING_IDENTITY_PROVIDER) throw new Error("identity hooks must be used within <CapxulProvider>");
675
+ return useSyncExternalStore(value?.runtime.subscribe ?? noSubscribe, value?.runtime.snapshot ?? noState, value?.runtime.snapshot ?? noState);
676
+ }
677
+ function useCapxulIdentity() {
678
+ const state = useCapxulIdentityOrNull();
679
+ if (state === null) throw new Error("identity hooks require a ready <CapxulProvider>");
680
+ return state;
681
+ }
682
+ function useCapxulSend() {
683
+ return useIdentityContext().send;
684
+ }
685
+ function useCapxulAuth() {
686
+ return useIdentityContext().auth;
687
+ }
688
+ function useResumeSubmittedOrganization() {
689
+ return useIdentityContext().resumeSubmittedOrganization;
690
+ }
691
+ function useCapxulDestination() {
692
+ const state = useCapxulIdentity();
693
+ const next = resolveIdentityDestination(state);
694
+ const held = useRef(null);
695
+ const key = JSON.stringify(next);
696
+ if (held.current?.key !== key) held.current = {
697
+ key,
698
+ value: next
699
+ };
700
+ return held.current.value;
701
+ }
702
+ function useCapxulTransitions(listener) {
703
+ const { addTransitionListener } = useIdentityContext();
704
+ useEffect(() => addTransitionListener(listener), [addTransitionListener, listener]);
705
+ }
706
+ const entered = (record, target) => record.outcome === "applied" && record.from !== target && record.to === target;
707
+ //#endregion
708
+ //#region src/platform/provider.tsx
709
+ /**
710
+ * Transient failure codes worth a retry — network blips, rate limits, and
711
+ * upstream provider/unknown hiccups that a later attempt may clear. Everything
712
+ * else (including any future code) is deterministic and NOT retried: retrying a
713
+ * deterministic failure only multiplies the failed backend actions. A fresh
714
+ * user with no Safe yet hits `SMART_ACCOUNT_MISSING` on every attempt, so the
715
+ * old blanket `retry: 2` tripled that (and every other deterministic) failed
716
+ * action for zero benefit (#1031).
717
+ */
718
+ const RETRYABLE_QUERY_ERROR_CODES = /* @__PURE__ */ new Set([
719
+ "NETWORK_ERROR",
720
+ "RATE_LIMITED",
721
+ "PROVIDER_ERROR",
722
+ "UNKNOWN"
723
+ ]);
724
+ /** Matches the previous `retry: 2` budget (initial attempt + up to 2 retries). */
725
+ const MAX_CAPXUL_QUERY_RETRIES = 2;
726
+ /**
727
+ * TanStack `retry` predicate: `failureCount` is 0-indexed and checked before
728
+ * increment, so `< MAX` reproduces the old numeric budget for retryable codes.
729
+ * Exported for direct unit coverage of the deterministic-vs-transient split.
730
+ */
731
+ function shouldRetryCapxulQuery(failureCount, error) {
732
+ if (failureCount >= MAX_CAPXUL_QUERY_RETRIES) return false;
733
+ if (!isCapxulError(error)) return false;
734
+ if (error.chain !== void 0) return error.chain.retryable;
735
+ return RETRYABLE_QUERY_ERROR_CODES.has(error.code);
736
+ }
737
+ /**
738
+ * The default query client used when the host injects none. Exported so a test
739
+ * can pin that `queries.retry` is wired to `shouldRetryCapxulQuery` — reverting
740
+ * it to the old blanket `retry: 2` must fail a test (#1031).
741
+ */
742
+ function makeDefaultQueryClient() {
743
+ return new QueryClient({ defaultOptions: {
744
+ queries: {
745
+ retry: shouldRetryCapxulQuery,
746
+ staleTime: 3e4
747
+ },
748
+ mutations: { retry: 0 }
749
+ } });
750
+ }
751
+ function isCapxulQueryKey(queryKey) {
752
+ return queryKey[0] === "capxul";
753
+ }
754
+ function clearClientScopedQueries(queryClient, ownsQueryClient) {
755
+ if (ownsQueryClient) {
756
+ queryClient.clear();
757
+ return;
758
+ }
759
+ queryClient.removeQueries({ predicate: (query) => isCapxulQueryKey(query.queryKey) });
760
+ }
761
+ function CapxulProvider(props) {
762
+ const { publishableKey, client: injectedClient, requirement, signer, observability, bootstrapBaseUrl, queryClient, children } = props;
763
+ const accountSetup = "accountSetup" in props ? props.accountSetup : void 0;
764
+ const [resolvedQueryClient] = useState(() => queryClient ?? makeDefaultQueryClient());
765
+ const [ownsQueryClient] = useState(() => queryClient === void 0);
766
+ const [attempt, setAttempt] = useState(0);
767
+ const retry = useCallback(() => {
768
+ setAttempt((n) => n + 1);
769
+ }, []);
770
+ const bootstrapInput = useMemo(() => publishableKey === void 0 ? null : {
771
+ publishableKey,
772
+ ...requirement === void 0 ? {} : { requirement },
773
+ ...signer === void 0 ? {} : { signer },
774
+ ...observability === void 0 ? {} : { observability },
775
+ ...bootstrapBaseUrl === void 0 ? {} : { bootstrapBaseUrl },
776
+ ...accountSetup === void 0 ? {} : { accountSetup }
777
+ }, [
778
+ publishableKey,
779
+ requirement,
780
+ signer,
781
+ observability,
782
+ bootstrapBaseUrl,
783
+ accountSetup,
784
+ attempt
785
+ ]);
786
+ const [ownedBootstrap, setOwnedBootstrap] = useState(null);
787
+ const activeOwnedBootstrap = bootstrapInput !== null && ownedBootstrap?.input === bootstrapInput ? ownedBootstrap : null;
788
+ const client = injectedClient ?? activeOwnedBootstrap?.client ?? null;
789
+ const previousClientRef = useRef(injectedClient ?? null);
790
+ useEffect(() => {
791
+ if (bootstrapInput === null) return;
792
+ let cancelled = false;
793
+ let created = null;
794
+ setOwnedBootstrap({
795
+ input: bootstrapInput,
796
+ client: null,
797
+ status: "bootstrapping",
798
+ error: null
799
+ });
800
+ (async () => {
801
+ const result = await createCapxulClient(bootstrapInput);
802
+ if (cancelled) {
803
+ if (result.ok) await result.value._internal.close?.();
804
+ return;
805
+ }
806
+ if (result.ok) {
807
+ created = result.value;
808
+ setOwnedBootstrap({
809
+ input: bootstrapInput,
810
+ client: result.value,
811
+ status: "ready",
812
+ error: null
813
+ });
814
+ } else setOwnedBootstrap({
815
+ input: bootstrapInput,
816
+ client: null,
817
+ status: "error",
818
+ error: result.error
819
+ });
820
+ })();
821
+ return () => {
822
+ cancelled = true;
823
+ created?._internal.close?.();
824
+ };
825
+ }, [bootstrapInput]);
826
+ useEffect(() => {
827
+ const previous = previousClientRef.current;
828
+ if (previous !== null && previous !== client) clearClientScopedQueries(resolvedQueryClient, ownsQueryClient);
829
+ previousClientRef.current = client;
830
+ }, [
831
+ client,
832
+ ownsQueryClient,
833
+ resolvedQueryClient
834
+ ]);
835
+ const bootstrapState = useMemo(() => ({
836
+ status: injectedClient === void 0 ? activeOwnedBootstrap?.status ?? "bootstrapping" : "ready",
837
+ error: injectedClient === void 0 ? activeOwnedBootstrap?.error ?? null : null,
838
+ retry
839
+ }), [
840
+ activeOwnedBootstrap,
841
+ injectedClient,
842
+ retry
843
+ ]);
844
+ if (publishableKey === void 0 === (injectedClient === void 0)) throw new Error("CapxulProvider requires exactly one of `publishableKey` or `client`");
845
+ return /* @__PURE__ */ jsx(QueryClientProvider, {
846
+ client: resolvedQueryClient,
847
+ children: /* @__PURE__ */ jsx(CapxulBootstrapProvider, {
848
+ value: bootstrapState,
849
+ children: /* @__PURE__ */ jsx(CapxulClientProvider, {
850
+ client,
851
+ children: /* @__PURE__ */ jsx(CapxulIdentityProvider, {
852
+ client,
853
+ children
854
+ })
855
+ })
856
+ })
857
+ });
858
+ }
859
+ //#endregion
860
+ //#region ../protocol/src/errors/failure-modes.ts
861
+ /**
862
+ * Why an operation failed — the one flat vocabulary. The first four are the
863
+ * auth/provider diagnoses that predate this file. The rest name the chain
864
+ * path. `FailureMode` is derived from this list so a runtime membership check
865
+ * (the SDK's `$exception` projection, the observability schema) and the type
866
+ * can never drift apart.
867
+ */
868
+ const FAILURE_MODES = [
869
+ "auth-origin-mismatch",
870
+ "stale-openfort-cache",
871
+ "app-env-allowlist",
872
+ "no-secure-context",
873
+ "signer-not-ready",
874
+ "rate-limited",
875
+ "timeout",
876
+ "upstream-down",
877
+ "revert",
878
+ "insufficient-balance",
879
+ "insufficient-allowance",
880
+ "rejected",
881
+ "unauthorized",
882
+ "forbidden",
883
+ "bad-request",
884
+ "method-unsupported",
885
+ "unknown"
886
+ ];
887
+ function isFailureMode(value) {
888
+ return typeof value === "string" && FAILURE_MODES.includes(value);
889
+ }
890
+ //#endregion
891
+ //#region ../protocol/src/errors/model.ts
892
+ /**
893
+ * `Code` narrows to the literal a factory passes, so `Errors.invalidInput(...)`
894
+ * is a `CapxulError<"INVALID_INPUT">`. An operation's error channel is then the
895
+ * union of the codes it can actually produce.
896
+ */
897
+ var CapxulError$1 = class extends Error {
898
+ code;
899
+ details;
900
+ correlationId;
901
+ layer;
902
+ /** The one-word cause (`FailureMode`). `chain.mode` when a chain cause is set. */
903
+ mode;
904
+ /** The structured chain-path cause. Never rewritten by a later layer. */
905
+ chain;
906
+ constructor(code, message, options = {}) {
907
+ super(message, "cause" in options ? { cause: options.cause } : void 0);
908
+ this.name = "CapxulError";
909
+ this.code = code;
910
+ if (options.details !== void 0) this.details = options.details;
911
+ if (options.correlationId !== void 0) this.correlationId = options.correlationId;
912
+ if (options.layer !== void 0) this.layer = options.layer;
913
+ const legacy = options.details?.failure_mode;
914
+ const mode = options.chain?.mode ?? (isFailureMode(options.mode) ? options.mode : void 0) ?? (isFailureMode(legacy) ? legacy : void 0);
915
+ if (mode !== void 0) this.mode = mode;
916
+ if (options.chain !== void 0) this.chain = options.chain;
917
+ }
918
+ };
919
+ //#endregion
920
+ //#region ../protocol/src/primitives/absurd.ts
921
+ /**
922
+ * The default branch of an exhaustive switch: `default: return absurd(value)`.
923
+ * The compiler proves it unreachable; if a value the types rule out arrives at
924
+ * run time anyway, it fails as a named defect instead of falling through.
925
+ */
926
+ function absurd(value) {
927
+ throw new CapxulError$1("UNKNOWN", "An exhaustive switch received a value its type rules out.", { details: {
928
+ defect: "unreachable_case",
929
+ value: String(value)
930
+ } });
931
+ }
932
+ //#endregion
933
+ //#region src/access/controllers.tsx
934
+ const action = async (send, event) => {
935
+ const result = await send(event);
936
+ return result.ok ? { ok: true } : {
937
+ ok: false,
938
+ reason: result.refused
939
+ };
940
+ };
941
+ function CapxulAuthenticationController({ slots }) {
942
+ const state = useCapxulIdentity();
943
+ const destination = useCapxulDestination();
944
+ const auth = useCapxulAuth();
945
+ const send = useCapxulSend();
946
+ switch (state.phase) {
947
+ case "signed_out": return slots.email({
948
+ state,
949
+ requestCode: auth.requestCode
950
+ });
951
+ case "otp_pending": return slots.otp({
952
+ state,
953
+ verifyCode: auth.verifyCode,
954
+ back: () => action(send, { _tag: "Reset" })
955
+ });
956
+ case "restoring":
957
+ case "otp_sending":
958
+ case "otp_verifying":
959
+ case "signing_out": return slots.pending({ state });
960
+ case "faulted": return slots.failure({
961
+ state,
962
+ recover: () => action(send, state.resume === null ? { _tag: "Reset" } : {
963
+ _tag: "ResumeOtpEntry",
964
+ now: Date.now()
965
+ }),
966
+ back: () => action(send, { _tag: "Reset" })
967
+ });
968
+ case "authenticated": return slots.success({
969
+ state,
970
+ destination
971
+ });
972
+ default: return absurd(state);
973
+ }
974
+ }
975
+ function ready(destination) {
976
+ return destination?.to === "dashboardPersonal" || destination?.to === "dashboardOrganization";
977
+ }
978
+ //#endregion
979
+ //#region src/access/onboarding-controller.tsx
980
+ function CapxulOnboardingController(props) {
981
+ const state = useCapxulIdentity();
982
+ const destination = useCapxulDestination();
983
+ const auth = useCapxulAuth();
984
+ const resumeSubmittedOrganization = useResumeSubmittedOrganization();
985
+ const resumedSubmission = useRef(null);
986
+ const [failedRecovery, setFailedRecovery] = useState(null);
987
+ const [organizationActions, setOrganizationActions] = useState(0);
988
+ async function withOrganizationProgress(action) {
989
+ setOrganizationActions((count) => count + 1);
990
+ try {
991
+ return await action();
992
+ } finally {
993
+ setOrganizationActions((count) => count - 1);
994
+ }
995
+ }
996
+ const createOrganization = (submission, options) => withOrganizationProgress(() => auth.createOrganization(submission, options));
997
+ const submission = props.submittedOrganization;
998
+ const accountAt = state.phase === "authenticated" ? state.account.at : null;
999
+ useEffect(() => {
1000
+ if (props.intent !== "organization" || submission === null || props.recoveryOptions === void 0 || accountAt !== "unknown" && accountAt !== "counterfactual" || resumedSubmission.current === submission) return;
1001
+ resumedSubmission.current = submission;
1002
+ setFailedRecovery(null);
1003
+ resumeSubmittedOrganization(submission, props.recoveryOptions()).then((result) => {
1004
+ if (!result.ok) setFailedRecovery(submission);
1005
+ });
1006
+ }, [
1007
+ accountAt,
1008
+ props.intent,
1009
+ props.recoveryOptions,
1010
+ resumeSubmittedOrganization,
1011
+ submission
1012
+ ]);
1013
+ if (state.phase !== "authenticated") return null;
1014
+ const { slots, navigation } = props;
1015
+ if (props.intent === null) return slots.intent({
1016
+ state,
1017
+ select: props.onIntent,
1018
+ back: navigation.selectorBack
1019
+ });
1020
+ if (props.intent === "personal" && !state.profileComplete) return slots.profile({
1021
+ intent: "personal",
1022
+ state,
1023
+ completePersonal: auth.completePersonal,
1024
+ cancel: navigation.profileCancel
1025
+ });
1026
+ if (props.intent === "organization" && props.organizationProfile === null && submission === null) return slots.profile({
1027
+ intent: "organization",
1028
+ state,
1029
+ continueOrganization: props.onOrganizationProfile,
1030
+ cancel: navigation.profileCancel
1031
+ });
1032
+ if (props.intent === "organization" && props.organizationProfile !== null && submission === null) return organizationForm(props, state, createOrganization, null);
1033
+ if (submission !== null && failedRecovery === submission && state.account.at !== "claimed") return organizationForm(props, state, createOrganization, submission);
1034
+ if (submission !== null && state.account.at !== "claimed") {
1035
+ if (state.account.at === "failed") {
1036
+ const retry = state.account.retryable ? (options) => createOrganization(submission, options) : void 0;
1037
+ return slots.accountFailure({
1038
+ state,
1039
+ account: state.account,
1040
+ ...retry === void 0 ? {} : { retry }
1041
+ });
1042
+ }
1043
+ return slots.accountProgress({
1044
+ state,
1045
+ account: state.account
1046
+ });
1047
+ }
1048
+ if (submission !== null && state.account.at === "claimed") {
1049
+ const org = state.account.org;
1050
+ if (org === null) return organizationForm(props, state, createOrganization, submission);
1051
+ if (org.observation === "error" || org.setup?.status === "failed" || org.setup?.status === "needsAttention" || org.setup?.status === "awaitingAuthorization" && organizationActions === 0) {
1052
+ const retry = org.orgId === null ? (options) => createOrganization(submission, options) : (options) => withOrganizationProgress(() => auth.retry(options));
1053
+ return slots.organizationFailure({
1054
+ state,
1055
+ org,
1056
+ retry
1057
+ });
1058
+ }
1059
+ if (org.setup?.status !== "ready" || org.observation !== "connected") return slots.organizationProgress({
1060
+ state,
1061
+ org
1062
+ });
1063
+ }
1064
+ if (state.account.at === "failed") return slots.accountFailure({
1065
+ state,
1066
+ account: state.account,
1067
+ ...state.account.retryable ? { retry: auth.retry } : {}
1068
+ });
1069
+ if (state.account.at !== "claimed") return slots.accountProgress({
1070
+ state,
1071
+ account: state.account
1072
+ });
1073
+ return ready(destination) ? slots.ready({
1074
+ state,
1075
+ destination
1076
+ }) : null;
1077
+ }
1078
+ function organizationForm(props, state, createOrganization, submitted) {
1079
+ const profileDetails = submitted?.profileDetails ?? props.organizationProfile;
1080
+ if (profileDetails === null) return null;
1081
+ return props.slots.organization({
1082
+ state,
1083
+ profileDetails,
1084
+ submitted,
1085
+ pinnedHandle: submitted?.organization.handle ?? null,
1086
+ submit: (organization, options) => {
1087
+ if (submitted !== null) return createOrganization(submitted, options);
1088
+ const normalized = normalizeOrganization(organization);
1089
+ if (normalized === null) return createOrganization({
1090
+ profileDetails,
1091
+ organization
1092
+ }, options);
1093
+ const next = {
1094
+ profileDetails,
1095
+ organization: normalized
1096
+ };
1097
+ props.onSubmittedOrganization(next);
1098
+ return createOrganization(next, options);
1099
+ },
1100
+ back: () => {
1101
+ props.onOrganizationProfile(null);
1102
+ props.navigation.organizationBack();
1103
+ },
1104
+ cancel: props.navigation.organizationCancel
1105
+ });
1106
+ }
1107
+ //#endregion
1108
+ export { orgKeySegment as _, entered as a, useCapxulIdentity as c, useCapxulTransitions as d, toInvocationControls as f, capxulKeys as g, actingAccount as h, CapxulProvider as i, useCapxulIdentityOrNull as l, accountKey as m, CapxulAuthenticationController as n, useCapxulAuth as o, toInvocationOptions as p, absurd as r, useCapxulDestination as s, CapxulOnboardingController as t, useCapxulSend as u, useCapxulClientOrNull as v, useCapxul as y };