@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.
- package/README.md +131 -0
- package/dist/index.d.mts +1167 -0
- package/dist/index.mjs +3936 -0
- package/dist/onboarding-controller-DWuvVuC8.mjs +1108 -0
- package/dist/onboarding-controller-DrJiYWfY.d.mts +205 -0
- package/dist/testing/index.d.mts +19 -0
- package/dist/testing/index.mjs +22 -0
- package/package.json +94 -0
|
@@ -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 };
|