@ai-matrx/auth 0.2.0 → 0.2.1
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/CHANGELOG.md +15 -0
- package/dist/organizations.cjs +264 -10
- package/dist/organizations.d.cts +253 -3
- package/dist/organizations.d.ts +253 -3
- package/dist/organizations.js +264 -10
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/auth
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
### website organization pieces (round 2)
|
|
5
|
+
|
|
6
|
+
**The website's organization logic moves in (`/organizations`); the app keeps only store bindings.**
|
|
7
|
+
- `createActiveOrganization` — new options: `link` (the `?org=` link as a first-class rung, honoured only for a
|
|
8
|
+
current membership, `onDecision` receives refused/offered/announced/admitted for the host to say), `adminSeat`
|
|
9
|
+
(admin lane replaces `current()`/`ensureOrgId()` while open, only for a membership), `bootstrapGate`,
|
|
10
|
+
`refusalRecheckGapMs`; `handleRefusal` now also re-runs the ladder for the not-a-member codes
|
|
11
|
+
(`organization_forbidden` / `organization_not_member`), once per gap. `storageHeldOrganization(...).read` is sync.
|
|
12
|
+
- `organization-link` — `classifyLinkOrganizationValue`, `readLinkOrganizationParam`, `decideLinkOrganization`
|
|
13
|
+
(injectable `messages`), `resolveOrganizationWithLink` (the one ladder + the link, share admission injected).
|
|
14
|
+
- `organization-gate` — `createOrganizationBootstrapGate`, `isNotAMemberRefusal`, `createNotAMemberRefusalNotice`,
|
|
15
|
+
`resolveChildOrganizationId`, `resolveAdminSeat`, `adminSeatReplacesUserContext`.
|
|
16
|
+
Tests: active-organization (+5), organization-gate (9).
|
|
17
|
+
|
|
3
18
|
## 0.2.0
|
|
4
19
|
|
|
5
20
|
**The apps' own identity, server-door and sign-in copies move in (consolidation U1).**
|
package/dist/organizations.cjs
CHANGED
|
@@ -20,20 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/organizations.ts
|
|
21
21
|
var organizations_exports = {};
|
|
22
22
|
__export(organizations_exports, {
|
|
23
|
+
LINK_ORGANIZATION_QUERY_KEY: () => LINK_ORGANIZATION_QUERY_KEY,
|
|
23
24
|
MEMBERSHIPS_URL: () => MEMBERSHIPS_URL,
|
|
25
|
+
NOT_A_MEMBER_RECHECK_GAP_MS: () => NOT_A_MEMBER_RECHECK_GAP_MS,
|
|
24
26
|
NoActiveOrganizationError: () => NoActiveOrganizationError,
|
|
25
27
|
NotAMemberError: () => NotAMemberError,
|
|
26
28
|
ORGANIZATION_CONTEXT_REQUIRED_CODE: () => ORGANIZATION_CONTEXT_REQUIRED_CODE,
|
|
27
29
|
ORGANIZATION_ENTITY_TYPE: () => ORGANIZATION_ENTITY_TYPE,
|
|
28
30
|
ORGANIZATION_REQUIRED_CODE: () => ORGANIZATION_REQUIRED_CODE,
|
|
29
31
|
ORGANIZATION_REQUIRED_WIRE_CODE: () => ORGANIZATION_REQUIRED_WIRE_CODE,
|
|
32
|
+
ORGANIZATION_RESOLUTION_TIMEOUT_MS: () => ORGANIZATION_RESOLUTION_TIMEOUT_MS,
|
|
30
33
|
OrganizationRequiredServerError: () => OrganizationRequiredServerError,
|
|
34
|
+
adminSeatReplacesUserContext: () => adminSeatReplacesUserContext,
|
|
35
|
+
classifyLinkOrganizationValue: () => classifyLinkOrganizationValue,
|
|
31
36
|
classifyOrganizationRefusal: () => classifyOrganizationRefusal,
|
|
32
37
|
createActiveOrganization: () => createActiveOrganization,
|
|
38
|
+
createNotAMemberRefusalNotice: () => createNotAMemberRefusalNotice,
|
|
39
|
+
createOrganizationBootstrapGate: () => createOrganizationBootstrapGate,
|
|
40
|
+
decideLinkOrganization: () => decideLinkOrganization,
|
|
33
41
|
extractOrganizationHoldMemberships: () => extractOrganizationHoldMemberships,
|
|
42
|
+
isNotAMemberRefusal: () => isNotAMemberRefusal,
|
|
34
43
|
isOrganizationRequiredEnvelope: () => isOrganizationRequiredEnvelope,
|
|
35
44
|
isOrganizationRequiredError: () => isOrganizationRequiredError,
|
|
36
45
|
isOrganizationRequiredServerError: () => isOrganizationRequiredServerError,
|
|
46
|
+
linkOrganizationMalformedMessage: () => linkOrganizationMalformedMessage,
|
|
47
|
+
linkOrganizationNotAMemberMessage: () => linkOrganizationNotAMemberMessage,
|
|
48
|
+
linkOrganizationOfferMessage: () => linkOrganizationOfferMessage,
|
|
49
|
+
linkOrganizationSwitchAnnouncement: () => linkOrganizationSwitchAnnouncement,
|
|
37
50
|
memoryHeldOrganization: () => memoryHeldOrganization,
|
|
38
51
|
oldestMembership: () => oldestMembership,
|
|
39
52
|
orderOrganizations: () => orderOrganizations,
|
|
@@ -43,10 +56,14 @@ __export(organizations_exports, {
|
|
|
43
56
|
organizationRequiredResponse: () => organizationRequiredResponse,
|
|
44
57
|
readAccountOrganizationChoices: () => readAccountOrganizationChoices,
|
|
45
58
|
readCallerMemberships: () => readCallerMemberships,
|
|
59
|
+
readLinkOrganizationParam: () => readLinkOrganizationParam,
|
|
46
60
|
readMemberships: () => readMemberships,
|
|
47
61
|
readOrganizationFavourites: () => readOrganizationFavourites,
|
|
48
62
|
resolveActiveOrganization: () => resolveActiveOrganization,
|
|
49
63
|
resolveActiveOrganizationLazily: () => resolveActiveOrganizationLazily,
|
|
64
|
+
resolveAdminSeat: () => resolveAdminSeat,
|
|
65
|
+
resolveChildOrganizationId: () => resolveChildOrganizationId,
|
|
66
|
+
resolveOrganizationWithLink: () => resolveOrganizationWithLink,
|
|
50
67
|
setOrganizationFavourite: () => setOrganizationFavourite,
|
|
51
68
|
storageHeldOrganization: () => storageHeldOrganization,
|
|
52
69
|
toAccountOrganizationChoices: () => toAccountOrganizationChoices,
|
|
@@ -238,6 +255,209 @@ function onSessionChange(client, cb) {
|
|
|
238
255
|
return () => data.subscription.unsubscribe();
|
|
239
256
|
}
|
|
240
257
|
|
|
258
|
+
// src/organization-gate.ts
|
|
259
|
+
var ORGANIZATION_RESOLUTION_TIMEOUT_MS = 1e4;
|
|
260
|
+
function createOrganizationBootstrapGate(defaultTimeoutMs = ORGANIZATION_RESOLUTION_TIMEOUT_MS) {
|
|
261
|
+
let settled = false;
|
|
262
|
+
let announce = null;
|
|
263
|
+
let resolution = new Promise((resolve) => {
|
|
264
|
+
announce = resolve;
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
isResolved: () => settled,
|
|
268
|
+
markResolved() {
|
|
269
|
+
if (settled) return;
|
|
270
|
+
settled = true;
|
|
271
|
+
announce?.();
|
|
272
|
+
},
|
|
273
|
+
async whenResolved(timeoutMs = defaultTimeoutMs) {
|
|
274
|
+
if (settled) return;
|
|
275
|
+
let timer;
|
|
276
|
+
await Promise.race([
|
|
277
|
+
resolution,
|
|
278
|
+
new Promise((resolve) => {
|
|
279
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
280
|
+
})
|
|
281
|
+
]);
|
|
282
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
283
|
+
},
|
|
284
|
+
reset() {
|
|
285
|
+
announce?.();
|
|
286
|
+
settled = false;
|
|
287
|
+
resolution = new Promise((resolve) => {
|
|
288
|
+
announce = resolve;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
var NOT_A_MEMBER_CODES = /* @__PURE__ */ new Set(["organization_forbidden", "organization_not_member"]);
|
|
294
|
+
function isNotAMemberRefusal(body) {
|
|
295
|
+
if (!body || typeof body !== "object") return false;
|
|
296
|
+
const record = body;
|
|
297
|
+
if (typeof record.code === "string" && NOT_A_MEMBER_CODES.has(record.code)) return true;
|
|
298
|
+
const detail = record.detail;
|
|
299
|
+
return !!detail && typeof detail === "object" && typeof detail.code === "string" && NOT_A_MEMBER_CODES.has(detail.code);
|
|
300
|
+
}
|
|
301
|
+
var NOT_A_MEMBER_RECHECK_GAP_MS = 3e4;
|
|
302
|
+
function createNotAMemberRefusalNotice(recheck, gapMs = NOT_A_MEMBER_RECHECK_GAP_MS) {
|
|
303
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
304
|
+
return {
|
|
305
|
+
notice(body, now = Date.now()) {
|
|
306
|
+
if (!isNotAMemberRefusal(body)) return false;
|
|
307
|
+
if (now - last < gapMs) return false;
|
|
308
|
+
last = now;
|
|
309
|
+
recheck();
|
|
310
|
+
return true;
|
|
311
|
+
},
|
|
312
|
+
reset() {
|
|
313
|
+
last = Number.NEGATIVE_INFINITY;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function resolveChildOrganizationId(parent, explicit, label = "child record", onDisagree = (m) => console.warn(m)) {
|
|
318
|
+
const parentOrg = typeof parent === "string" ? parent : parent?.organization_id ?? null;
|
|
319
|
+
if (parentOrg) {
|
|
320
|
+
if (explicit && explicit !== parentOrg) {
|
|
321
|
+
onDisagree(`[child org] ${label} filed in its parent's organization ${parentOrg}, not ${explicit} \u2014 a child belongs with its parent.`);
|
|
322
|
+
}
|
|
323
|
+
return parentOrg;
|
|
324
|
+
}
|
|
325
|
+
return explicit ?? null;
|
|
326
|
+
}
|
|
327
|
+
function resolveAdminSeat(input) {
|
|
328
|
+
if (!input || input.laneOpen !== true || !input.organizationId) return null;
|
|
329
|
+
return { id: input.organizationId, name: input.organizationName ?? null };
|
|
330
|
+
}
|
|
331
|
+
function adminSeatReplacesUserContext(input, personOrganizationId) {
|
|
332
|
+
const seat = resolveAdminSeat(input);
|
|
333
|
+
return seat !== null && seat.id !== (personOrganizationId ?? null);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/organization-link.ts
|
|
337
|
+
var import_uuid = require("@ai-matrx/kit/uuid");
|
|
338
|
+
var LINK_ORGANIZATION_QUERY_KEY = "org";
|
|
339
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,99}$/i;
|
|
340
|
+
function classifyLinkOrganizationValue(raw) {
|
|
341
|
+
const value = raw.trim();
|
|
342
|
+
if ((0, import_uuid.isRfc4122Uuid)(value)) return { kind: "named", organizationId: value };
|
|
343
|
+
if (SLUG.test(value)) return { kind: "addressed", slug: value.toLowerCase() };
|
|
344
|
+
return { kind: "malformed", raw: value };
|
|
345
|
+
}
|
|
346
|
+
function readLinkOrganizationParam(source) {
|
|
347
|
+
if (source == null) return { kind: "absent" };
|
|
348
|
+
let params;
|
|
349
|
+
if (source instanceof URLSearchParams) {
|
|
350
|
+
params = source;
|
|
351
|
+
} else {
|
|
352
|
+
try {
|
|
353
|
+
const q = source.includes("?") ? source.slice(source.indexOf("?")) : source;
|
|
354
|
+
params = new URLSearchParams(q);
|
|
355
|
+
} catch {
|
|
356
|
+
return { kind: "absent" };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!params.has(LINK_ORGANIZATION_QUERY_KEY)) return { kind: "absent" };
|
|
360
|
+
return classifyLinkOrganizationValue(params.get(LINK_ORGANIZATION_QUERY_KEY) ?? "");
|
|
361
|
+
}
|
|
362
|
+
function linkOrganizationNotAMemberMessage(signedInAs) {
|
|
363
|
+
const account = signedInAs ? `You are signed in as ${signedInAs}.` : "Check which account you are signed in as.";
|
|
364
|
+
return `The link you followed is for an organization this account is not a member of, so nothing was opened and you were not moved. ${account} Sign in with the account the link was meant for, or ask whoever sent it to give this account access.`;
|
|
365
|
+
}
|
|
366
|
+
function linkOrganizationMalformedMessage(raw) {
|
|
367
|
+
const quoted = raw.length > 0 ? ` ("${raw}")` : "";
|
|
368
|
+
return `The link you followed names an organization we cannot read${quoted}, so nothing was switched and you were left where you were. Ask whoever sent it for the link again, or pick the organization you are working in from the avatar menu.`;
|
|
369
|
+
}
|
|
370
|
+
function linkOrganizationSwitchAnnouncement(toName, fromName) {
|
|
371
|
+
const from = fromName ? ` You were working in ${fromName}.` : "";
|
|
372
|
+
return `You are now working in ${toName} because the link you followed is filed under it.${from} Switch back any time from the avatar menu.`;
|
|
373
|
+
}
|
|
374
|
+
function linkOrganizationOfferMessage(toName, fromName) {
|
|
375
|
+
const where = fromName ? `, and you are working in ${fromName}` : "";
|
|
376
|
+
return `The link you followed is for ${toName}${where}. "Switch organization when a link asks" is turned off in your settings, so nothing changed \u2014 switch to ${toName} to open it.`;
|
|
377
|
+
}
|
|
378
|
+
function decideLinkOrganization(input) {
|
|
379
|
+
const { param, memberships, currentOrganizationId } = input;
|
|
380
|
+
const switchWhenALinkAsks = input.switchWhenALinkAsks ?? true;
|
|
381
|
+
const say = {
|
|
382
|
+
notAMember: linkOrganizationNotAMemberMessage,
|
|
383
|
+
malformed: linkOrganizationMalformedMessage,
|
|
384
|
+
switched: linkOrganizationSwitchAnnouncement,
|
|
385
|
+
offer: linkOrganizationOfferMessage,
|
|
386
|
+
offerAction: (name) => `Switch to ${name}`,
|
|
387
|
+
...input.messages
|
|
388
|
+
};
|
|
389
|
+
if (param.kind === "absent") return { kind: "no-link" };
|
|
390
|
+
if (param.kind === "malformed") {
|
|
391
|
+
return {
|
|
392
|
+
kind: "refused",
|
|
393
|
+
reason: "malformed",
|
|
394
|
+
message: say.malformed(param.raw)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const match = param.kind === "named" ? memberships.find((o) => o.id === param.organizationId) : memberships.find((o) => o.slug?.trim().toLowerCase() === param.slug);
|
|
398
|
+
if (!match && param.kind === "named" && input.admittedByAShare) {
|
|
399
|
+
return { kind: "admitted", organizationId: param.organizationId };
|
|
400
|
+
}
|
|
401
|
+
if (!match) {
|
|
402
|
+
return {
|
|
403
|
+
kind: "refused",
|
|
404
|
+
reason: "not-a-member",
|
|
405
|
+
message: say.notAMember(input.signedInAs)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (currentOrganizationId === match.id) {
|
|
409
|
+
return { kind: "already-current", organizationId: match.id };
|
|
410
|
+
}
|
|
411
|
+
const fromName = currentOrganizationId ? input.currentOrganizationName ?? memberships.find((o) => o.id === currentOrganizationId)?.name ?? null : null;
|
|
412
|
+
if (currentOrganizationId && !switchWhenALinkAsks) {
|
|
413
|
+
return {
|
|
414
|
+
kind: "offered",
|
|
415
|
+
organizationId: match.id,
|
|
416
|
+
organizationName: match.name,
|
|
417
|
+
message: say.offer(match.name, fromName),
|
|
418
|
+
actionLabel: say.offerAction(match.name)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
kind: "honoured",
|
|
423
|
+
organizationId: match.id,
|
|
424
|
+
organizationName: match.name,
|
|
425
|
+
// Nothing to announce on a cold arrival: they were working nowhere, so
|
|
426
|
+
// they were not moved. A move, however, is always announced.
|
|
427
|
+
announcement: currentOrganizationId ? say.switched(match.name, fromName) : null
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function resolveOrganizationWithLink(input) {
|
|
431
|
+
const ladder = await resolveActiveOrganizationLazily({
|
|
432
|
+
memberships: input.memberships,
|
|
433
|
+
held: input.held ?? null,
|
|
434
|
+
readAccount: input.readAccount,
|
|
435
|
+
...input.first ? { first: input.first } : {}
|
|
436
|
+
});
|
|
437
|
+
if (!ladder) return null;
|
|
438
|
+
const param = typeof input.link === "string" ? input.link.includes("=") ? readLinkOrganizationParam(input.link) : classifyLinkOrganizationValue(input.link) : readLinkOrganizationParam(input.link ?? null);
|
|
439
|
+
const decisionInput = {
|
|
440
|
+
param,
|
|
441
|
+
memberships: input.memberships.map((o) => ({ id: o.id, name: o.name, slug: o.slug ?? null })),
|
|
442
|
+
currentOrganizationId: ladder.organization.id,
|
|
443
|
+
currentOrganizationName: ladder.organization.name,
|
|
444
|
+
...input.switchWhenALinkAsks === void 0 ? {} : { switchWhenALinkAsks: input.switchWhenALinkAsks },
|
|
445
|
+
...input.signedInAs === void 0 ? {} : { signedInAs: input.signedInAs },
|
|
446
|
+
...input.messages ? { messages: input.messages } : {}
|
|
447
|
+
};
|
|
448
|
+
let link = decideLinkOrganization(decisionInput);
|
|
449
|
+
if (link.kind === "refused" && link.reason === "not-a-member" && param.kind === "named" && input.admittedByAShare) {
|
|
450
|
+
if (await input.admittedByAShare(param.organizationId)) {
|
|
451
|
+
link = decideLinkOrganization({ ...decisionInput, admittedByAShare: true });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (link.kind === "honoured") {
|
|
455
|
+
const organization = input.memberships.find((m) => m.id === link.organizationId);
|
|
456
|
+
if (organization) return { organization, rung: "link", link };
|
|
457
|
+
}
|
|
458
|
+
return { organization: ladder.organization, rung: ladder.rung, link };
|
|
459
|
+
}
|
|
460
|
+
|
|
241
461
|
// src/active-organization.ts
|
|
242
462
|
var NoActiveOrganizationError = class extends Error {
|
|
243
463
|
constructor(reason, detail) {
|
|
@@ -315,6 +535,7 @@ function createActiveOrganization(options) {
|
|
|
315
535
|
const publish = (next) => {
|
|
316
536
|
current = Object.freeze(next);
|
|
317
537
|
if (current.status !== "resolving" && current.status !== "idle") {
|
|
538
|
+
options.bootstrapGate?.markResolved();
|
|
318
539
|
const ready = waiters;
|
|
319
540
|
waiters = [];
|
|
320
541
|
for (const w of ready) w();
|
|
@@ -332,19 +553,38 @@ function createActiveOrganization(options) {
|
|
|
332
553
|
}
|
|
333
554
|
const memberships = await readMemberships(client, person.id);
|
|
334
555
|
if (gen !== generation) return current;
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
556
|
+
const linkOptions = options.link;
|
|
557
|
+
let resolved;
|
|
558
|
+
let linkDecision = null;
|
|
559
|
+
if (linkOptions) {
|
|
560
|
+
const r = await resolveOrganizationWithLink({
|
|
561
|
+
memberships,
|
|
562
|
+
held: await held.read(person.id),
|
|
563
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id),
|
|
564
|
+
link: linkOptions.source() ?? null,
|
|
565
|
+
switchWhenALinkAsks: linkOptions.switchWhenALinkAsks?.(),
|
|
566
|
+
signedInAs: linkOptions.signedInAs?.(),
|
|
567
|
+
messages: linkOptions.messages,
|
|
568
|
+
admittedByAShare: linkOptions.admittedByAShare
|
|
569
|
+
});
|
|
570
|
+
resolved = r;
|
|
571
|
+
linkDecision = r?.link ?? null;
|
|
572
|
+
} else {
|
|
573
|
+
const linked = options.linked?.() ?? null;
|
|
574
|
+
const heldId = (linked && memberships.some((m) => m.id === linked) ? linked : null) ?? await held.read(person.id);
|
|
575
|
+
resolved = await resolveActiveOrganizationLazily({
|
|
576
|
+
memberships,
|
|
577
|
+
held: heldId,
|
|
578
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id)
|
|
579
|
+
});
|
|
580
|
+
}
|
|
342
581
|
if (gen !== generation) return current;
|
|
343
582
|
if (!resolved) {
|
|
344
583
|
publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
|
|
345
584
|
return current;
|
|
346
585
|
}
|
|
347
586
|
await held.write(person.id, resolved.organization.id);
|
|
587
|
+
if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
|
|
348
588
|
publish({
|
|
349
589
|
status: "ready",
|
|
350
590
|
userId: person.id,
|
|
@@ -380,6 +620,11 @@ function createActiveOrganization(options) {
|
|
|
380
620
|
const userId = session.user?.id ?? null;
|
|
381
621
|
if (event === "SIGNED_IN" && userId !== current.userId) void load();
|
|
382
622
|
});
|
|
623
|
+
const seatFor = (snap) => {
|
|
624
|
+
const seat = resolveAdminSeat(options.adminSeat?.());
|
|
625
|
+
return seat && snap.memberships.some((m) => m.id === seat.id) ? seat.id : null;
|
|
626
|
+
};
|
|
627
|
+
let lastNotAMemberRecheck = Number.NEGATIVE_INFINITY;
|
|
383
628
|
const settled = async () => {
|
|
384
629
|
if (current.status === "idle") return load();
|
|
385
630
|
if (current.status === "resolving") {
|
|
@@ -396,11 +641,11 @@ function createActiveOrganization(options) {
|
|
|
396
641
|
};
|
|
397
642
|
},
|
|
398
643
|
load,
|
|
399
|
-
current: () => current.status === "ready" ? current.organizationId : null,
|
|
644
|
+
current: () => current.status === "ready" ? seatFor(current) ?? current.organizationId : null,
|
|
400
645
|
async ensureOrgId(recordOrganizationId) {
|
|
401
646
|
if (recordOrganizationId) return recordOrganizationId;
|
|
402
647
|
const snap = await settled();
|
|
403
|
-
if (snap.status === "ready" && snap.organizationId) return snap.organizationId;
|
|
648
|
+
if (snap.status === "ready" && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
|
|
404
649
|
if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
|
|
405
650
|
throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
|
|
406
651
|
},
|
|
@@ -415,7 +660,16 @@ function createActiveOrganization(options) {
|
|
|
415
660
|
await writeLastActiveOrganization(client, organizationId);
|
|
416
661
|
},
|
|
417
662
|
async handleRefusal(status, body) {
|
|
418
|
-
|
|
663
|
+
let kind = classifyOrganizationRefusal(status, body);
|
|
664
|
+
if (kind === null && isNotAMemberRefusal(body)) {
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
if (now - lastNotAMemberRecheck >= (options.refusalRecheckGapMs ?? NOT_A_MEMBER_RECHECK_GAP_MS)) {
|
|
667
|
+
lastNotAMemberRecheck = now;
|
|
668
|
+
kind = "lapsed";
|
|
669
|
+
} else {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
419
673
|
if (kind === "lapsed") {
|
|
420
674
|
if (current.userId) await held.clear();
|
|
421
675
|
await load();
|
package/dist/organizations.d.cts
CHANGED
|
@@ -143,6 +143,230 @@ declare function setOrganizationFavourite(db: OrganizationsDb, organizationId: s
|
|
|
143
143
|
type OrganizationRefusal = "lapsed" | "retry" | null;
|
|
144
144
|
declare function classifyOrganizationRefusal(status: number, body: unknown): OrganizationRefusal;
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Organization-question plumbing that is LOGIC, not store binding (moved from the website's
|
|
148
|
+
* `orgBootstrapGate`, `organizationRefusal`, `childOrganization`, `adminOrganization`).
|
|
149
|
+
* Pure: every host input is injected; no Redux, no Next, no window.
|
|
150
|
+
*/
|
|
151
|
+
/** How long a write may wait for the first answer before it is refused anyway. */
|
|
152
|
+
declare const ORGANIZATION_RESOLUTION_TIMEOUT_MS = 10000;
|
|
153
|
+
interface OrganizationBootstrapGate {
|
|
154
|
+
isResolved(): boolean;
|
|
155
|
+
/** The organization question has been answered (with an organization or without). Idempotent. */
|
|
156
|
+
markResolved(): void;
|
|
157
|
+
/** Wait for the answer or the timeout, whichever first. Always resolves; never rejects. */
|
|
158
|
+
whenResolved(timeoutMs?: number): Promise<void>;
|
|
159
|
+
/** Tests and in-place identity swaps only. */
|
|
160
|
+
reset(): void;
|
|
161
|
+
}
|
|
162
|
+
/** "Nobody has looked yet" is a race, not a fact: a held write waits on this, bounded. */
|
|
163
|
+
declare function createOrganizationBootstrapGate(defaultTimeoutMs?: number): OrganizationBootstrapGate;
|
|
164
|
+
/** True when an error body is the server's "you are not a member of the organization you carried". */
|
|
165
|
+
declare function isNotAMemberRefusal(body: unknown): boolean;
|
|
166
|
+
declare const NOT_A_MEMBER_RECHECK_GAP_MS = 30000;
|
|
167
|
+
interface NotAMemberRefusalNotice {
|
|
168
|
+
/** True when a recheck was started (at most one per gap, so a burst reads memberships once). */
|
|
169
|
+
notice(body: unknown, now?: number): boolean;
|
|
170
|
+
reset(): void;
|
|
171
|
+
}
|
|
172
|
+
declare function createNotAMemberRefusalNotice(recheck: () => void, gapMs?: number): NotAMemberRefusalNotice;
|
|
173
|
+
type ParentOrganization = string | {
|
|
174
|
+
organization_id?: string | null;
|
|
175
|
+
} | null | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* A record created as part of an existing parent lives in the PARENT's organization; the explicit
|
|
178
|
+
* one applies only when the parent names none. A disagreement is announced (`onDisagree`,
|
|
179
|
+
* default console.warn), never obeyed.
|
|
180
|
+
*/
|
|
181
|
+
declare function resolveChildOrganizationId(parent: ParentOrganization, explicit?: string | null, label?: string, onDisagree?: (message: string) => void): string | null;
|
|
182
|
+
/** What the host knows about the admin lane. */
|
|
183
|
+
interface AdminSeatInput {
|
|
184
|
+
readonly laneOpen: boolean;
|
|
185
|
+
readonly organizationId?: string | null;
|
|
186
|
+
readonly organizationName?: string | null;
|
|
187
|
+
}
|
|
188
|
+
interface AdminSeat {
|
|
189
|
+
readonly id: string;
|
|
190
|
+
readonly name: string | null;
|
|
191
|
+
}
|
|
192
|
+
/** The admin organization in force right now, or null (off the lane, or none set). */
|
|
193
|
+
declare function resolveAdminSeat(input: AdminSeatInput | null | undefined): AdminSeat | null;
|
|
194
|
+
/** True when the admin seat differs from the person's own active organization (scope/project/task never ride across). */
|
|
195
|
+
declare function adminSeatReplacesUserContext(input: AdminSeatInput | null | undefined, personOrganizationId: string | null | undefined): boolean;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The query key, platform-wide. The server stamps this on every deep link it
|
|
199
|
+
* emits; `features/admin/users/components/OrganizationsAdminClient.tsx` has
|
|
200
|
+
* read it since before this module existed. One spelling, in one place.
|
|
201
|
+
*/
|
|
202
|
+
declare const LINK_ORGANIZATION_QUERY_KEY = "org";
|
|
203
|
+
type LinkOrganizationParam =
|
|
204
|
+
/** No link said anything about an organization. The ladder is untouched. */
|
|
205
|
+
{
|
|
206
|
+
kind: "absent";
|
|
207
|
+
}
|
|
208
|
+
/** Something was said and it is neither a uuid nor an address. Refused in words, never used. */
|
|
209
|
+
| {
|
|
210
|
+
kind: "malformed";
|
|
211
|
+
raw: string;
|
|
212
|
+
}
|
|
213
|
+
/** A well-formed organization id. Still has to survive the membership check. */
|
|
214
|
+
| {
|
|
215
|
+
kind: "named";
|
|
216
|
+
organizationId: string;
|
|
217
|
+
}
|
|
218
|
+
/** An organization's address (slug). Survives only by matching a membership's slug. */
|
|
219
|
+
| {
|
|
220
|
+
kind: "addressed";
|
|
221
|
+
slug: string;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Classify one bare `org=` value. The ONE rule for what a value may be, shared
|
|
225
|
+
* by the URL reader below and the boot resolver's bare-value path.
|
|
226
|
+
*/
|
|
227
|
+
declare function classifyLinkOrganizationValue(raw: string): LinkOrganizationParam;
|
|
228
|
+
/**
|
|
229
|
+
* Read the key out of a query string, a `URLSearchParams`, or a full URL.
|
|
230
|
+
* Never throws: a URL this cannot parse is `absent`, because a crash on
|
|
231
|
+
* arrival is a worse answer than the ladder's own.
|
|
232
|
+
*/
|
|
233
|
+
declare function readLinkOrganizationParam(source: string | URLSearchParams | null | undefined): LinkOrganizationParam;
|
|
234
|
+
interface LinkOrganizationMembership {
|
|
235
|
+
id: string;
|
|
236
|
+
name: string;
|
|
237
|
+
/** The organization's address, so an `addressed` link can be matched. */
|
|
238
|
+
slug?: string | null;
|
|
239
|
+
}
|
|
240
|
+
interface LinkOrganizationDecisionInput {
|
|
241
|
+
param: LinkOrganizationParam;
|
|
242
|
+
/** The person's LIVE memberships. The only authority on what they may open. */
|
|
243
|
+
memberships: ReadonlyArray<LinkOrganizationMembership>;
|
|
244
|
+
/** The organization they are working in right now, or null. */
|
|
245
|
+
currentOrganizationId: string | null;
|
|
246
|
+
currentOrganizationName?: string | null;
|
|
247
|
+
/**
|
|
248
|
+
* The knob `userPreferences.organization.switchOnLinkPrompt` — "switch
|
|
249
|
+
* organization when a link asks". DEFAULT ON, so an omitted value is `true`.
|
|
250
|
+
*/
|
|
251
|
+
switchWhenALinkAsks?: boolean;
|
|
252
|
+
/** The account they are signed in as, for the refusal sentence. */
|
|
253
|
+
signedInAs?: string | null;
|
|
254
|
+
/**
|
|
255
|
+
* The link's organization is one this person is not a member of but IS let into
|
|
256
|
+
* by a share they accepted (`lib/organizations/linkOrganizationAdmission.ts`).
|
|
257
|
+
* A share admits and never makes a member, so the answer is `admitted`: nobody
|
|
258
|
+
* is moved, nothing is refused, and the page opens it as that organization's
|
|
259
|
+
* guest. Omitted, the answer is exactly what it was before (VERIFIER-15 H4).
|
|
260
|
+
*/
|
|
261
|
+
admittedByAShare?: boolean;
|
|
262
|
+
/** Replace any of the default English sentences (a localising host). */
|
|
263
|
+
messages?: Partial<LinkOrganizationMessages>;
|
|
264
|
+
}
|
|
265
|
+
interface LinkOrganizationMessages {
|
|
266
|
+
notAMember(signedInAs?: string | null): string;
|
|
267
|
+
malformed(raw: string): string;
|
|
268
|
+
switched(toName: string, fromName: string | null): string;
|
|
269
|
+
offer(toName: string, fromName: string | null): string;
|
|
270
|
+
offerAction(toName: string): string;
|
|
271
|
+
}
|
|
272
|
+
type LinkOrganizationDecision =
|
|
273
|
+
/** Nothing to do. No toast, no change. */
|
|
274
|
+
{
|
|
275
|
+
kind: "no-link";
|
|
276
|
+
}
|
|
277
|
+
/** The link names the organization they are already in. Silent no-op. */
|
|
278
|
+
| {
|
|
279
|
+
kind: "already-current";
|
|
280
|
+
organizationId: string;
|
|
281
|
+
}
|
|
282
|
+
/** Use this organization for this session. `announcement` non-null = they moved. */
|
|
283
|
+
| {
|
|
284
|
+
kind: "honoured";
|
|
285
|
+
organizationId: string;
|
|
286
|
+
organizationName: string;
|
|
287
|
+
announcement: string | null;
|
|
288
|
+
}
|
|
289
|
+
/** The knob is off and this would be a real switch. Offer it as a click. */
|
|
290
|
+
| {
|
|
291
|
+
kind: "offered";
|
|
292
|
+
organizationId: string;
|
|
293
|
+
organizationName: string;
|
|
294
|
+
message: string;
|
|
295
|
+
actionLabel: string;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Not a member, but let in by a share they accepted. Silent: nobody is moved
|
|
299
|
+
* (a share never makes a member) and nothing was refused (the page opens it).
|
|
300
|
+
*/
|
|
301
|
+
| {
|
|
302
|
+
kind: "admitted";
|
|
303
|
+
organizationId: string;
|
|
304
|
+
}
|
|
305
|
+
/** Malformed, or not theirs. Changes nothing; says so. */
|
|
306
|
+
| {
|
|
307
|
+
kind: "refused";
|
|
308
|
+
reason: "malformed" | "not-a-member";
|
|
309
|
+
message: string;
|
|
310
|
+
};
|
|
311
|
+
/** What a person reads when the link's organization is not theirs to open. */
|
|
312
|
+
declare function linkOrganizationNotAMemberMessage(signedInAs?: string | null): string;
|
|
313
|
+
/** What a person reads when `org=` is not an organization id at all. */
|
|
314
|
+
declare function linkOrganizationMalformedMessage(raw: string): string;
|
|
315
|
+
/** What a person reads when a link actually moved them. Law 4: never silent. */
|
|
316
|
+
declare function linkOrganizationSwitchAnnouncement(toName: string, fromName: string | null): string;
|
|
317
|
+
/** What a person reads with the knob turned off and a link asking to move them. */
|
|
318
|
+
declare function linkOrganizationOfferMessage(toName: string, fromName: string | null): string;
|
|
319
|
+
/**
|
|
320
|
+
* The decision. One function, every branch, no side effects.
|
|
321
|
+
*
|
|
322
|
+
* 🚨 WHY THE KNOB GOVERNS ONLY A REAL SWITCH. The knob protects the
|
|
323
|
+
* organization a person is WORKING IN from being yanked out from under them by
|
|
324
|
+
* a link someone else composed. With `currentOrganizationId === null` there is
|
|
325
|
+
* nothing to protect and nothing to switch away from: the alternative to
|
|
326
|
+
* honouring the link is the picker, which is the very dead end this module was
|
|
327
|
+
* built to end. So the knob turns a SWITCH into an offer; it never turns an
|
|
328
|
+
* arrival into a refusal. This is deliberate and it is not a hidden default —
|
|
329
|
+
* with no link present, a null selection stays null exactly as before.
|
|
330
|
+
*/
|
|
331
|
+
declare function decideLinkOrganization(input: LinkOrganizationDecisionInput): LinkOrganizationDecision;
|
|
332
|
+
interface ResolveWithLinkInput<T extends {
|
|
333
|
+
id: string;
|
|
334
|
+
name: string;
|
|
335
|
+
slug?: string | null;
|
|
336
|
+
joinedAt?: string;
|
|
337
|
+
}> {
|
|
338
|
+
readonly memberships: readonly T[];
|
|
339
|
+
/** This tab's held organization (rung 1; a refresh keeps it). */
|
|
340
|
+
readonly held?: string | null;
|
|
341
|
+
readonly readAccount: () => Promise<AccountOrganizationChoices>;
|
|
342
|
+
readonly first?: () => Promise<T | null>;
|
|
343
|
+
/** The raw `?org=` value, a query string, a URL or URLSearchParams. Omitted = no link. */
|
|
344
|
+
readonly link?: string | URLSearchParams | null;
|
|
345
|
+
readonly switchWhenALinkAsks?: boolean | undefined;
|
|
346
|
+
readonly signedInAs?: string | null | undefined;
|
|
347
|
+
readonly messages?: Partial<LinkOrganizationMessages> | undefined;
|
|
348
|
+
/** Asked only when the link already failed the membership check (a share admits, never joins). */
|
|
349
|
+
readonly admittedByAShare?: ((organizationId: string) => Promise<boolean>) | undefined;
|
|
350
|
+
}
|
|
351
|
+
interface ResolvedWithLink<T> {
|
|
352
|
+
readonly organization: T;
|
|
353
|
+
readonly rung: OrganizationRung | "link";
|
|
354
|
+
/** What the link meant, when it said anything — the host SAYS it (refused / announced / offered). */
|
|
355
|
+
readonly link: LinkOrganizationDecision;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The ONE load ladder with the link folded in: held → last active → start-up → first, then the
|
|
359
|
+
* link's own organization wins ONLY when it is a current membership. Every other link outcome
|
|
360
|
+
* (refused, offered, already-current, admitted) leaves the ladder's answer standing. Null when
|
|
361
|
+
* the person has no memberships. A failed read throws through.
|
|
362
|
+
*/
|
|
363
|
+
declare function resolveOrganizationWithLink<T extends {
|
|
364
|
+
id: string;
|
|
365
|
+
name: string;
|
|
366
|
+
slug?: string | null;
|
|
367
|
+
joinedAt?: string;
|
|
368
|
+
}>(input: ResolveWithLinkInput<T>): Promise<ResolvedWithLink<T> | null>;
|
|
369
|
+
|
|
146
370
|
/**
|
|
147
371
|
* THE ACTIVE ORGANIZATION — one per tab / running app, set once when the app loads, never none
|
|
148
372
|
* while the person has a membership (Arman, 2026-10-07; organizations STATE rules 10–15).
|
|
@@ -181,7 +405,7 @@ interface ActiveOrganizationSnapshot {
|
|
|
181
405
|
readonly organization: OrganizationMembership | null;
|
|
182
406
|
readonly memberships: readonly OrganizationMembership[];
|
|
183
407
|
/** Which ladder rung answered (or "switched" after a person's click). */
|
|
184
|
-
readonly rung: OrganizationRung | "switched" | null;
|
|
408
|
+
readonly rung: OrganizationRung | "switched" | "link" | null;
|
|
185
409
|
readonly error: string | null;
|
|
186
410
|
}
|
|
187
411
|
declare class NoActiveOrganizationError extends Error {
|
|
@@ -214,11 +438,37 @@ interface CreateActiveOrganizationOptions {
|
|
|
214
438
|
readonly held?: HeldOrganizationStore | undefined;
|
|
215
439
|
/** An extra first rung (the web's `?org=` link). Kept only if it is a current membership. */
|
|
216
440
|
readonly linked?: (() => string | null | undefined) | undefined;
|
|
441
|
+
/**
|
|
442
|
+
* The link's own organization (`?org=` uuid or address) as a first-class rung: honoured only when
|
|
443
|
+
* it is a current membership, otherwise the ladder stands and `onDecision` receives the refusal /
|
|
444
|
+
* offer / announcement for the host to SAY. Supersedes `linked` when both are given.
|
|
445
|
+
*/
|
|
446
|
+
readonly link?: {
|
|
447
|
+
readonly source: () => string | URLSearchParams | null | undefined;
|
|
448
|
+
readonly switchWhenALinkAsks?: (() => boolean | undefined) | undefined;
|
|
449
|
+
readonly signedInAs?: (() => string | null | undefined) | undefined;
|
|
450
|
+
readonly messages?: Partial<LinkOrganizationMessages> | undefined;
|
|
451
|
+
/** A share admits without making a member (asked only after a not-a-member verdict). */
|
|
452
|
+
readonly admittedByAShare?: ((organizationId: string) => Promise<boolean>) | undefined;
|
|
453
|
+
readonly onDecision?: ((decision: LinkOrganizationDecision) => void) | undefined;
|
|
454
|
+
} | undefined;
|
|
455
|
+
/**
|
|
456
|
+
* THE ADMIN SEAT: while the host's admin lane is open, the admin's own organization IS the active
|
|
457
|
+
* one for every reader (`current()`, `ensureOrgId()`); off the lane the person's own is untouched.
|
|
458
|
+
* Ignored unless the seat is a current membership. The snapshot always keeps the person's own.
|
|
459
|
+
*/
|
|
460
|
+
readonly adminSeat?: (() => AdminSeatInput | null | undefined) | undefined;
|
|
461
|
+
/** Marked resolved whenever the ladder settles (organization or none). */
|
|
462
|
+
readonly bootstrapGate?: OrganizationBootstrapGate | undefined;
|
|
463
|
+
/** Minimum gap between ladder re-runs caused by a not-a-member refusal. Default 30 s. */
|
|
464
|
+
readonly refusalRecheckGapMs?: number | undefined;
|
|
217
465
|
/** Load as soon as it is created (default true). */
|
|
218
466
|
readonly autoLoad?: boolean | undefined;
|
|
219
467
|
}
|
|
220
468
|
/** A held store over a `Storage` (browser `sessionStorage` = per tab), identity-keyed. */
|
|
221
|
-
declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): HeldOrganizationStore
|
|
469
|
+
declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">, key?: string): HeldOrganizationStore & {
|
|
470
|
+
read(userId: string): string | null;
|
|
471
|
+
};
|
|
222
472
|
/** Held in memory: the running app's own choice (desktop, scripts, tests). */
|
|
223
473
|
declare function memoryHeldOrganization(): HeldOrganizationStore;
|
|
224
474
|
declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
|
|
@@ -329,4 +579,4 @@ declare function organizationRequiredEnvelope(error: OrganizationRequiredServerE
|
|
|
329
579
|
/** The standard `Response` (status 400) a route answers the refusal with — no framework needed. */
|
|
330
580
|
declare function organizationRequiredResponse(error: OrganizationRequiredServerError): Response;
|
|
331
581
|
|
|
332
|
-
export { type AccountOrganizationChoices, type ActiveOrganization, type ActiveOrganizationSnapshot, type ActiveOrganizationStatus, type CreateActiveOrganizationOptions, type HeldOrganizationStore, type LadderInput, MEMBERSHIPS_URL, type MembershipReadClient, NoActiveOrganizationError, NotAMemberError, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_ENTITY_TYPE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembership, type OrganizationMembershipSummary, type OrganizationRefusal, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, type OrganizationRung, type OrganizationsDb, type ResolvedOrganization, classifyOrganizationRefusal, createActiveOrganization, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, memoryHeldOrganization, oldestMembership, orderOrganizations, organizationMatches, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readAccountOrganizationChoices, readCallerMemberships, readMemberships, readOrganizationFavourites, resolveActiveOrganization, resolveActiveOrganizationLazily, setOrganizationFavourite, storageHeldOrganization, toAccountOrganizationChoices, writeLastActiveOrganization, writeStartupOrganization };
|
|
582
|
+
export { type AccountOrganizationChoices, type ActiveOrganization, type ActiveOrganizationSnapshot, type ActiveOrganizationStatus, type AdminSeat, type AdminSeatInput, type CreateActiveOrganizationOptions, type HeldOrganizationStore, LINK_ORGANIZATION_QUERY_KEY, type LadderInput, type LinkOrganizationDecision, type LinkOrganizationDecisionInput, type LinkOrganizationMembership, type LinkOrganizationMessages, type LinkOrganizationParam, MEMBERSHIPS_URL, type MembershipReadClient, NOT_A_MEMBER_RECHECK_GAP_MS, NoActiveOrganizationError, NotAMemberError, type NotAMemberRefusalNotice, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_ENTITY_TYPE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, ORGANIZATION_RESOLUTION_TIMEOUT_MS, type OrganizationBootstrapGate, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembership, type OrganizationMembershipSummary, type OrganizationRefusal, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, type OrganizationRung, type OrganizationsDb, type ParentOrganization, type ResolveWithLinkInput, type ResolvedOrganization, type ResolvedWithLink, adminSeatReplacesUserContext, classifyLinkOrganizationValue, classifyOrganizationRefusal, createActiveOrganization, createNotAMemberRefusalNotice, createOrganizationBootstrapGate, decideLinkOrganization, extractOrganizationHoldMemberships, isNotAMemberRefusal, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, linkOrganizationMalformedMessage, linkOrganizationNotAMemberMessage, linkOrganizationOfferMessage, linkOrganizationSwitchAnnouncement, memoryHeldOrganization, oldestMembership, orderOrganizations, organizationMatches, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readAccountOrganizationChoices, readCallerMemberships, readLinkOrganizationParam, readMemberships, readOrganizationFavourites, resolveActiveOrganization, resolveActiveOrganizationLazily, resolveAdminSeat, resolveChildOrganizationId, resolveOrganizationWithLink, setOrganizationFavourite, storageHeldOrganization, toAccountOrganizationChoices, writeLastActiveOrganization, writeStartupOrganization };
|