@capxul/sdk-react 4.20.0-beta.4 → 4.20.0-beta.40
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 +25 -7
- package/dist/index.d.mts +620 -569
- package/dist/index.mjs +3079 -2614
- package/dist/{controllers-DXmEHrnw.mjs → onboarding-controller-BCrkmLxQ.mjs} +292 -159
- package/dist/{controllers-BWmKRFy6.d.mts → onboarding-controller-DrJiYWfY.d.mts} +11 -19
- package/dist/testing/index.d.mts +5 -5
- package/dist/testing/index.mjs +1 -1
- package/package.json +40 -9
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { CapxulError, createCapxulClient, isCapxulError, resolveIdentityDestination, toCountryCode, toHandle } from "@capxul/sdk";
|
|
3
|
+
import { CapxulError, createCapxulClient, isCapxulError, resolveIdentityDestination, toCountryCode, toHandle, toOrgId } from "@capxul/sdk";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
-
//#region src/
|
|
5
|
+
//#region src/platform/bootstrap-context.tsx
|
|
6
6
|
const CapxulBootstrapContext = createContext(null);
|
|
7
7
|
function CapxulBootstrapProvider({ value, children }) {
|
|
8
8
|
return /* @__PURE__ */ jsx(CapxulBootstrapContext.Provider, {
|
|
@@ -16,7 +16,7 @@ function useCapxul() {
|
|
|
16
16
|
return state;
|
|
17
17
|
}
|
|
18
18
|
//#endregion
|
|
19
|
-
//#region src/
|
|
19
|
+
//#region src/platform/client-context.tsx
|
|
20
20
|
const MISSING_CAPXUL_CLIENT_PROVIDER = Symbol("MISSING_CAPXUL_CLIENT_PROVIDER");
|
|
21
21
|
const CapxulClientContext = createContext(MISSING_CAPXUL_CLIENT_PROVIDER);
|
|
22
22
|
function CapxulClientProvider({ client, children }) {
|
|
@@ -36,7 +36,14 @@ function useCapxulClientOrNull() {
|
|
|
36
36
|
return client;
|
|
37
37
|
}
|
|
38
38
|
//#endregion
|
|
39
|
-
//#region src/
|
|
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
|
+
}
|
|
40
47
|
/**
|
|
41
48
|
* The account a read is answered FOR, as a key segment (#2007 R19).
|
|
42
49
|
*
|
|
@@ -144,7 +151,7 @@ const capxulKeys = {
|
|
|
144
151
|
"terms"
|
|
145
152
|
],
|
|
146
153
|
activity: ["capxul", "activity"],
|
|
147
|
-
activityDetail: (reference, actor
|
|
154
|
+
activityDetail: (reference, actor) => [
|
|
148
155
|
"capxul",
|
|
149
156
|
"activity",
|
|
150
157
|
actor ?? "self",
|
|
@@ -170,7 +177,7 @@ const capxulKeys = {
|
|
|
170
177
|
]
|
|
171
178
|
};
|
|
172
179
|
//#endregion
|
|
173
|
-
//#region src/
|
|
180
|
+
//#region src/platform/invocation-controls.ts
|
|
174
181
|
/**
|
|
175
182
|
* Keep only the five public option fields, and only when they hold the right
|
|
176
183
|
* kind of value. A named action verb takes its attempt as an optional FIRST
|
|
@@ -209,72 +216,23 @@ function toInvocationControls(options) {
|
|
|
209
216
|
};
|
|
210
217
|
}
|
|
211
218
|
//#endregion
|
|
212
|
-
//#region src/identity.
|
|
213
|
-
const MISSING_IDENTITY_PROVIDER = Symbol("MISSING_IDENTITY_PROVIDER");
|
|
214
|
-
const IdentityContext = createContext(MISSING_IDENTITY_PROVIDER);
|
|
219
|
+
//#region src/platform/identity-lanes.ts
|
|
215
220
|
const failure = (result) => result.ok ? null : {
|
|
216
221
|
ok: false,
|
|
217
222
|
reason: result.refused
|
|
218
223
|
};
|
|
219
|
-
async function guarded(runtime, verb, options, run) {
|
|
220
|
-
const invocation = toInvocationControls(options);
|
|
221
|
-
try {
|
|
222
|
-
return await (runtime.runFacade?.(verb, invocation, run) ?? run(invocation));
|
|
223
|
-
} catch {
|
|
224
|
-
return {
|
|
225
|
-
ok: false,
|
|
226
|
-
reason: "UNKNOWN"
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
function organizationFailure(reason, state) {
|
|
231
|
-
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?.at === "failed" ? state.account.org.failure : void 0;
|
|
232
|
-
if (stateFailure?.code === reason) return {
|
|
233
|
-
ok: false,
|
|
234
|
-
reason,
|
|
235
|
-
error: stateFailure.error ?? new CapxulError(stateFailure.code === "WORK_DIED" ? "UNKNOWN" : stateFailure.code, stateFailure.message, {
|
|
236
|
-
...stateFailure.mode === void 0 ? {} : { details: { failure_mode: stateFailure.mode } },
|
|
237
|
-
layer: "identity"
|
|
238
|
-
})
|
|
239
|
-
};
|
|
240
|
-
return {
|
|
241
|
-
ok: false,
|
|
242
|
-
reason,
|
|
243
|
-
error: new CapxulError(reason === "WORK_DIED" ? "UNKNOWN" : reason, reason === "INVALID_INPUT" ? "Organization details are invalid." : "Organization creation failed.", { layer: "identity" })
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
function normalizeOrganization(organization) {
|
|
247
|
-
const name = typeof organization.name === "string" ? organization.name.trim() : "";
|
|
248
|
-
const handle = typeof organization.handle === "string" ? organization.handle.trim().toLowerCase() : "";
|
|
249
|
-
if (name.length === 0) return null;
|
|
250
|
-
if (organization.bio !== void 0 && typeof organization.bio !== "string") return null;
|
|
251
|
-
if (organization.size !== void 0 && typeof organization.size !== "string") return null;
|
|
252
|
-
if (organization.logoStorageId !== void 0 && (typeof organization.logoStorageId !== "string" || organization.logoStorageId.length === 0)) return null;
|
|
253
|
-
try {
|
|
254
|
-
return {
|
|
255
|
-
name,
|
|
256
|
-
handle: toHandle(handle),
|
|
257
|
-
country: toCountryCode(organization.country),
|
|
258
|
-
...organization.bio === void 0 ? {} : { bio: organization.bio },
|
|
259
|
-
...organization.size === void 0 ? {} : { size: organization.size },
|
|
260
|
-
...organization.logoStorageId === void 0 ? {} : { logoStorageId: organization.logoStorageId }
|
|
261
|
-
};
|
|
262
|
-
} catch {
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
/** The Organization id when its lane is already running or finished, else null. */
|
|
267
|
-
const runningOrganization = (state) => {
|
|
268
|
-
if (state.phase !== "authenticated" || state.account.at !== "claimed") return null;
|
|
269
|
-
const org = state.account.org;
|
|
270
|
-
return org !== null && (org.at === "loading" || org.at === "settingUp" || org.at === "ready") ? org.orgId : null;
|
|
271
|
-
};
|
|
272
224
|
/** A superseded session read waits at most this long for the winning read. */
|
|
273
225
|
const SESSION_SETTLE_LIMIT_MS = 3e4;
|
|
274
226
|
const accountInFlight = (state) => state.phase === "authenticated" && (state.account.at === "deriving" || state.account.at === "claiming");
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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) => {
|
|
278
236
|
let done = false;
|
|
279
237
|
let stop = null;
|
|
280
238
|
let timer = null;
|
|
@@ -310,20 +268,29 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
310
268
|
stop = unsubscribe;
|
|
311
269
|
timer = setTimeout(() => finish({
|
|
312
270
|
ok: false,
|
|
313
|
-
reason: "UNKNOWN"
|
|
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
|
+
})
|
|
314
277
|
}), SESSION_SETTLE_LIMIT_MS);
|
|
315
278
|
});
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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) => {
|
|
327
294
|
let done = false;
|
|
328
295
|
let stop = null;
|
|
329
296
|
const onAbort = () => finish({
|
|
@@ -352,44 +319,110 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
352
319
|
stop = unsubscribe;
|
|
353
320
|
if (!accountInFlight(runtime.snapshot())) finish({ ok: true });
|
|
354
321
|
});
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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();
|
|
368
346
|
if (state.phase !== "authenticated") return {
|
|
369
347
|
ok: false,
|
|
370
348
|
reason: "WRONG_STATE"
|
|
371
349
|
};
|
|
372
350
|
if (state.account.at === "claimed") return { ok: true };
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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 {
|
|
389
417
|
ok: false,
|
|
390
|
-
reason:
|
|
418
|
+
reason: error.code,
|
|
419
|
+
error
|
|
391
420
|
};
|
|
392
|
-
}
|
|
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;
|
|
393
426
|
const completeProfile = async (profile, invocation) => {
|
|
394
427
|
const result = await runtime.completeProfile(profile, invocation);
|
|
395
428
|
return result.ok ? { ok: true } : result;
|
|
@@ -400,14 +433,14 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
400
433
|
ok: false,
|
|
401
434
|
reason: "INVALID_INPUT"
|
|
402
435
|
};
|
|
403
|
-
const refreshed = await
|
|
436
|
+
const refreshed = await readSession(runtime, invocation);
|
|
404
437
|
if (!refreshed.ok) return refreshed;
|
|
405
438
|
const current = runtime.snapshot();
|
|
406
439
|
if (current.phase !== "authenticated" || !current.profileComplete) {
|
|
407
440
|
const completed = await runtime.completeProfile(submission.profileDetails, invocation);
|
|
408
441
|
if (!completed.ok) return completed;
|
|
409
442
|
}
|
|
410
|
-
const claimed = await reachClaimed(invocation);
|
|
443
|
+
const claimed = await reachClaimed(runtime, invocation);
|
|
411
444
|
return claimed.ok ? {
|
|
412
445
|
ok: true,
|
|
413
446
|
organization
|
|
@@ -477,9 +510,9 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
477
510
|
ok: false,
|
|
478
511
|
reason: completed.reason
|
|
479
512
|
};
|
|
480
|
-
const refreshed = await
|
|
513
|
+
const refreshed = await readSession(runtime, invocation);
|
|
481
514
|
if (!refreshed.ok) return refreshed;
|
|
482
|
-
return
|
|
515
|
+
return failure(await runtime.send({ _tag: "EnsureAccount" }, invocation)) ?? { ok: true };
|
|
483
516
|
}),
|
|
484
517
|
createOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
|
|
485
518
|
const current = runtime.snapshot();
|
|
@@ -488,20 +521,6 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
488
521
|
ok: true,
|
|
489
522
|
orgId: running
|
|
490
523
|
};
|
|
491
|
-
if (current.phase === "authenticated" && current.account.at === "claimed") {
|
|
492
|
-
const org = current.account.org;
|
|
493
|
-
if (org?.at === "failed" && org.orgId !== null) {
|
|
494
|
-
if (!org.retryable) return {
|
|
495
|
-
ok: false,
|
|
496
|
-
reason: org.failure.code
|
|
497
|
-
};
|
|
498
|
-
const retried = await runtime.send({ _tag: "RetryOrganization" }, invocation);
|
|
499
|
-
return failure(retried) ?? {
|
|
500
|
-
ok: true,
|
|
501
|
-
orgId: org.orgId
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
524
|
const prepared = await prepareOrganization(submission, invocation);
|
|
506
525
|
if (!prepared.ok) return prepared;
|
|
507
526
|
const opened = runningOrganization(runtime.snapshot());
|
|
@@ -509,20 +528,24 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
509
528
|
ok: true,
|
|
510
529
|
orgId: opened
|
|
511
530
|
};
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
draft: prepared.organization
|
|
515
|
-
}, invocation);
|
|
516
|
-
const refused = failure(created);
|
|
517
|
-
if (refused !== null) return refused;
|
|
518
|
-
const state = created.state;
|
|
519
|
-
const org = state.phase === "authenticated" && state.account.at === "claimed" ? state.account.org : null;
|
|
520
|
-
return org?.at === "ready" && org.orgId !== null ? {
|
|
521
|
-
ok: true,
|
|
522
|
-
orgId: org.orgId
|
|
523
|
-
} : {
|
|
531
|
+
const begun = await client.onboarding.beginOrganization(prepared.organization, invocation);
|
|
532
|
+
if (!begun.ok) return {
|
|
524
533
|
ok: false,
|
|
525
|
-
reason:
|
|
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)
|
|
526
549
|
};
|
|
527
550
|
}).then((result) => {
|
|
528
551
|
if (result.ok) return result;
|
|
@@ -535,8 +558,21 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
535
558
|
}),
|
|
536
559
|
retry: (options) => guarded(runtime, "retry", options, async (invocation) => {
|
|
537
560
|
const state = runtime.snapshot();
|
|
538
|
-
const
|
|
539
|
-
|
|
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);
|
|
540
576
|
return failure(result) ?? { ok: true };
|
|
541
577
|
}),
|
|
542
578
|
resumeSubmittedOrganization: (submission, options) => guarded(runtime, "createOrganization", options, async (invocation) => {
|
|
@@ -548,6 +584,10 @@ function createAuth(client, clearAuthenticatedQueries) {
|
|
|
548
584
|
})
|
|
549
585
|
};
|
|
550
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);
|
|
551
591
|
function CapxulIdentityProvider({ client, children }) {
|
|
552
592
|
const queryClient = useQueryClient();
|
|
553
593
|
const runtime = client?._internal.identity ?? null;
|
|
@@ -565,7 +605,7 @@ function CapxulIdentityProvider({ client, children }) {
|
|
|
565
605
|
if (!active) return;
|
|
566
606
|
state = runtime.snapshot();
|
|
567
607
|
}
|
|
568
|
-
if (state.phase === "authenticated" && state.profileComplete && state.account.at === "unknown") await runtime.send({ _tag: "EnsureAccount" }, { signal: controller.signal });
|
|
608
|
+
if (client._internal.accountSetup !== "explicit" && state.phase === "authenticated" && state.profileComplete && state.account.at === "unknown") await runtime.send({ _tag: "EnsureAccount" }, { signal: controller.signal });
|
|
569
609
|
}).catch(() => void 0);
|
|
570
610
|
return () => {
|
|
571
611
|
active = false;
|
|
@@ -580,7 +620,7 @@ function CapxulIdentityProvider({ client, children }) {
|
|
|
580
620
|
const account = actingAccount(state);
|
|
581
621
|
const accountChanged = previousAccount !== void 0 && account !== void 0 && account !== previousAccount;
|
|
582
622
|
previousAccount = account;
|
|
583
|
-
if (record.outcome === "applied" && (record.from !== record.to || accountChanged) && (record.to === "authenticated:claimed" || record.to === "authenticated:claimed:
|
|
623
|
+
if (record.outcome === "applied" && (record.from !== record.to || accountChanged) && (record.to === "authenticated:claimed" || record.to === "authenticated:claimed:organization")) queryClient.invalidateQueries({
|
|
584
624
|
queryKey: capxulKeys.root,
|
|
585
625
|
...accountChanged ? { refetchType: "none" } : {}
|
|
586
626
|
});
|
|
@@ -665,7 +705,7 @@ function useCapxulTransitions(listener) {
|
|
|
665
705
|
}
|
|
666
706
|
const entered = (record, target) => record.outcome === "applied" && record.from !== target && record.to === target;
|
|
667
707
|
//#endregion
|
|
668
|
-
//#region src/provider.tsx
|
|
708
|
+
//#region src/platform/provider.tsx
|
|
669
709
|
/**
|
|
670
710
|
* Transient failure codes worth a retry — network blips, rate limits, and
|
|
671
711
|
* upstream provider/unknown hiccups that a later attempt may clear. Everything
|
|
@@ -720,6 +760,7 @@ function clearClientScopedQueries(queryClient, ownsQueryClient) {
|
|
|
720
760
|
}
|
|
721
761
|
function CapxulProvider(props) {
|
|
722
762
|
const { publishableKey, client: injectedClient, requirement, signer, observability, bootstrapBaseUrl, queryClient, children } = props;
|
|
763
|
+
const accountSetup = "accountSetup" in props ? props.accountSetup : void 0;
|
|
723
764
|
const [resolvedQueryClient] = useState(() => queryClient ?? makeDefaultQueryClient());
|
|
724
765
|
const [ownsQueryClient] = useState(() => queryClient === void 0);
|
|
725
766
|
const [attempt, setAttempt] = useState(0);
|
|
@@ -731,13 +772,15 @@ function CapxulProvider(props) {
|
|
|
731
772
|
...requirement === void 0 ? {} : { requirement },
|
|
732
773
|
...signer === void 0 ? {} : { signer },
|
|
733
774
|
...observability === void 0 ? {} : { observability },
|
|
734
|
-
...bootstrapBaseUrl === void 0 ? {} : { bootstrapBaseUrl }
|
|
775
|
+
...bootstrapBaseUrl === void 0 ? {} : { bootstrapBaseUrl },
|
|
776
|
+
...accountSetup === void 0 ? {} : { accountSetup }
|
|
735
777
|
}, [
|
|
736
778
|
publishableKey,
|
|
737
779
|
requirement,
|
|
738
780
|
signer,
|
|
739
781
|
observability,
|
|
740
782
|
bootstrapBaseUrl,
|
|
783
|
+
accountSetup,
|
|
741
784
|
attempt
|
|
742
785
|
]);
|
|
743
786
|
const [ownedBootstrap, setOwnedBootstrap] = useState(null);
|
|
@@ -814,7 +857,84 @@ function CapxulProvider(props) {
|
|
|
814
857
|
});
|
|
815
858
|
}
|
|
816
859
|
//#endregion
|
|
817
|
-
//#region src/
|
|
860
|
+
//#region ../protocol/src/errors/failure-modes.ts
|
|
861
|
+
/**
|
|
862
|
+
* Why an operation failed — the one flat vocabulary. The first five are the
|
|
863
|
+
* auth and signer diagnoses that predate this file. The next eleven name the
|
|
864
|
+
* chain path. `refused` and `defect` name a provider refusal and a Capxul
|
|
865
|
+
* fault on any path (ADR-0039 D2). `FailureMode` is derived from this list so
|
|
866
|
+
* a runtime membership check (the SDK's `$exception` projection, the
|
|
867
|
+
* observability schema) and the type can never drift apart. Every word has a
|
|
868
|
+
* failure class in `classes.ts`.
|
|
869
|
+
*/
|
|
870
|
+
const FAILURE_MODES = [
|
|
871
|
+
"auth-origin-mismatch",
|
|
872
|
+
"stale-openfort-cache",
|
|
873
|
+
"app-env-allowlist",
|
|
874
|
+
"no-secure-context",
|
|
875
|
+
"signer-not-ready",
|
|
876
|
+
"rate-limited",
|
|
877
|
+
"timeout",
|
|
878
|
+
"upstream-down",
|
|
879
|
+
"revert",
|
|
880
|
+
"insufficient-balance",
|
|
881
|
+
"insufficient-allowance",
|
|
882
|
+
"rejected",
|
|
883
|
+
"unauthorized",
|
|
884
|
+
"forbidden",
|
|
885
|
+
"bad-request",
|
|
886
|
+
"method-unsupported",
|
|
887
|
+
"refused",
|
|
888
|
+
"defect",
|
|
889
|
+
"unknown"
|
|
890
|
+
];
|
|
891
|
+
function isFailureMode(value) {
|
|
892
|
+
return typeof value === "string" && FAILURE_MODES.includes(value);
|
|
893
|
+
}
|
|
894
|
+
//#endregion
|
|
895
|
+
//#region ../protocol/src/errors/model.ts
|
|
896
|
+
/**
|
|
897
|
+
* `Code` narrows to the literal a factory passes, so `Errors.invalidInput(...)`
|
|
898
|
+
* is a `CapxulError<"INVALID_INPUT">`. An operation's error channel is then the
|
|
899
|
+
* union of the codes it can actually produce.
|
|
900
|
+
*/
|
|
901
|
+
var CapxulError$1 = class extends Error {
|
|
902
|
+
code;
|
|
903
|
+
details;
|
|
904
|
+
correlationId;
|
|
905
|
+
layer;
|
|
906
|
+
/** The one-word cause (`FailureMode`). `chain.mode` when a chain cause is set. */
|
|
907
|
+
mode;
|
|
908
|
+
/** The structured chain-path cause. Never rewritten by a later layer. */
|
|
909
|
+
chain;
|
|
910
|
+
constructor(code, message, options = {}) {
|
|
911
|
+
super(message, "cause" in options ? { cause: options.cause } : void 0);
|
|
912
|
+
this.name = "CapxulError";
|
|
913
|
+
this.code = code;
|
|
914
|
+
if (options.details !== void 0) this.details = options.details;
|
|
915
|
+
if (options.correlationId !== void 0) this.correlationId = options.correlationId;
|
|
916
|
+
if (options.layer !== void 0) this.layer = options.layer;
|
|
917
|
+
const legacy = options.details?.failure_mode;
|
|
918
|
+
const mode = options.chain?.mode ?? (isFailureMode(options.mode) ? options.mode : void 0) ?? (isFailureMode(legacy) ? legacy : void 0);
|
|
919
|
+
if (mode !== void 0) this.mode = mode;
|
|
920
|
+
if (options.chain !== void 0) this.chain = options.chain;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region ../protocol/src/primitives/absurd.ts
|
|
925
|
+
/**
|
|
926
|
+
* The default branch of an exhaustive switch: `default: return absurd(value)`.
|
|
927
|
+
* The compiler proves it unreachable; if a value the types rule out arrives at
|
|
928
|
+
* run time anyway, it fails as a named defect instead of falling through.
|
|
929
|
+
*/
|
|
930
|
+
function absurd(value) {
|
|
931
|
+
throw new CapxulError$1("UNKNOWN", "An exhaustive switch received a value its type rules out.", { details: {
|
|
932
|
+
defect: "unreachable_case",
|
|
933
|
+
value: String(value)
|
|
934
|
+
} });
|
|
935
|
+
}
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/access/controllers.tsx
|
|
818
938
|
const action = async (send, event) => {
|
|
819
939
|
const result = await send(event);
|
|
820
940
|
return result.ok ? { ok: true } : {
|
|
@@ -853,11 +973,14 @@ function CapxulAuthenticationController({ slots }) {
|
|
|
853
973
|
state,
|
|
854
974
|
destination
|
|
855
975
|
});
|
|
976
|
+
default: return absurd(state);
|
|
856
977
|
}
|
|
857
978
|
}
|
|
858
979
|
function ready(destination) {
|
|
859
980
|
return destination?.to === "dashboardPersonal" || destination?.to === "dashboardOrganization";
|
|
860
981
|
}
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/access/onboarding-controller.tsx
|
|
861
984
|
function CapxulOnboardingController(props) {
|
|
862
985
|
const state = useCapxulIdentity();
|
|
863
986
|
const destination = useCapxulDestination();
|
|
@@ -865,6 +988,16 @@ function CapxulOnboardingController(props) {
|
|
|
865
988
|
const resumeSubmittedOrganization = useResumeSubmittedOrganization();
|
|
866
989
|
const resumedSubmission = useRef(null);
|
|
867
990
|
const [failedRecovery, setFailedRecovery] = useState(null);
|
|
991
|
+
const [organizationActions, setOrganizationActions] = useState(0);
|
|
992
|
+
async function withOrganizationProgress(action) {
|
|
993
|
+
setOrganizationActions((count) => count + 1);
|
|
994
|
+
try {
|
|
995
|
+
return await action();
|
|
996
|
+
} finally {
|
|
997
|
+
setOrganizationActions((count) => count - 1);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const createOrganization = (submission, options) => withOrganizationProgress(() => auth.createOrganization(submission, options));
|
|
868
1001
|
const submission = props.submittedOrganization;
|
|
869
1002
|
const accountAt = state.phase === "authenticated" ? state.account.at : null;
|
|
870
1003
|
useEffect(() => {
|
|
@@ -900,11 +1033,11 @@ function CapxulOnboardingController(props) {
|
|
|
900
1033
|
continueOrganization: props.onOrganizationProfile,
|
|
901
1034
|
cancel: navigation.profileCancel
|
|
902
1035
|
});
|
|
903
|
-
if (props.intent === "organization" && props.organizationProfile !== null && submission === null) return organizationForm(props, state,
|
|
904
|
-
if (submission !== null && failedRecovery === submission && state.account.at !== "claimed") return organizationForm(props, state,
|
|
1036
|
+
if (props.intent === "organization" && props.organizationProfile !== null && submission === null) return organizationForm(props, state, createOrganization, null);
|
|
1037
|
+
if (submission !== null && failedRecovery === submission && state.account.at !== "claimed") return organizationForm(props, state, createOrganization, submission);
|
|
905
1038
|
if (submission !== null && state.account.at !== "claimed") {
|
|
906
1039
|
if (state.account.at === "failed") {
|
|
907
|
-
const retry = state.account.retryable ? (options) =>
|
|
1040
|
+
const retry = state.account.retryable ? (options) => createOrganization(submission, options) : void 0;
|
|
908
1041
|
return slots.accountFailure({
|
|
909
1042
|
state,
|
|
910
1043
|
account: state.account,
|
|
@@ -918,16 +1051,16 @@ function CapxulOnboardingController(props) {
|
|
|
918
1051
|
}
|
|
919
1052
|
if (submission !== null && state.account.at === "claimed") {
|
|
920
1053
|
const org = state.account.org;
|
|
921
|
-
if (org === null) return organizationForm(props, state,
|
|
922
|
-
if (org.
|
|
923
|
-
const retry = org.
|
|
1054
|
+
if (org === null) return organizationForm(props, state, createOrganization, submission);
|
|
1055
|
+
if (org.observation === "error" || org.setup?.status === "failed" || org.setup?.status === "needsAttention" || org.setup?.status === "awaitingAuthorization" && organizationActions === 0) {
|
|
1056
|
+
const retry = org.orgId === null ? (options) => createOrganization(submission, options) : (options) => withOrganizationProgress(() => auth.retry(options));
|
|
924
1057
|
return slots.organizationFailure({
|
|
925
1058
|
state,
|
|
926
1059
|
org,
|
|
927
|
-
|
|
1060
|
+
retry
|
|
928
1061
|
});
|
|
929
1062
|
}
|
|
930
|
-
if (org.
|
|
1063
|
+
if (org.setup?.status !== "ready" || org.observation !== "connected") return slots.organizationProgress({
|
|
931
1064
|
state,
|
|
932
1065
|
org
|
|
933
1066
|
});
|
|
@@ -946,7 +1079,7 @@ function CapxulOnboardingController(props) {
|
|
|
946
1079
|
destination
|
|
947
1080
|
}) : null;
|
|
948
1081
|
}
|
|
949
|
-
function organizationForm(props, state,
|
|
1082
|
+
function organizationForm(props, state, createOrganization, submitted) {
|
|
950
1083
|
const profileDetails = submitted?.profileDetails ?? props.organizationProfile;
|
|
951
1084
|
if (profileDetails === null) return null;
|
|
952
1085
|
return props.slots.organization({
|
|
@@ -955,9 +1088,9 @@ function organizationForm(props, state, auth, submitted) {
|
|
|
955
1088
|
submitted,
|
|
956
1089
|
pinnedHandle: submitted?.organization.handle ?? null,
|
|
957
1090
|
submit: (organization, options) => {
|
|
958
|
-
if (submitted !== null) return
|
|
1091
|
+
if (submitted !== null) return createOrganization(submitted, options);
|
|
959
1092
|
const normalized = normalizeOrganization(organization);
|
|
960
|
-
if (normalized === null) return
|
|
1093
|
+
if (normalized === null) return createOrganization({
|
|
961
1094
|
profileDetails,
|
|
962
1095
|
organization
|
|
963
1096
|
}, options);
|
|
@@ -966,7 +1099,7 @@ function organizationForm(props, state, auth, submitted) {
|
|
|
966
1099
|
organization: normalized
|
|
967
1100
|
};
|
|
968
1101
|
props.onSubmittedOrganization(next);
|
|
969
|
-
return
|
|
1102
|
+
return createOrganization(next, options);
|
|
970
1103
|
},
|
|
971
1104
|
back: () => {
|
|
972
1105
|
props.onOrganizationProfile(null);
|
|
@@ -976,4 +1109,4 @@ function organizationForm(props, state, auth, submitted) {
|
|
|
976
1109
|
});
|
|
977
1110
|
}
|
|
978
1111
|
//#endregion
|
|
979
|
-
export {
|
|
1112
|
+
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 };
|