@ai-matrx/auth 0.2.0 → 0.2.2
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 +47 -0
- package/dist/active-organization-Dy4UtiyR.d.cts +594 -0
- package/dist/active-organization-Dy4UtiyR.d.ts +594 -0
- package/dist/guest.cjs +213 -0
- package/dist/guest.d.cts +189 -0
- package/dist/guest.d.ts +189 -0
- package/dist/guest.js +192 -0
- package/dist/index.cjs +514 -112
- package/dist/index.d.cts +161 -5
- package/dist/index.d.ts +161 -5
- package/dist/index.js +515 -113
- package/dist/matrx-oauth-_Ji8X5Wm.d.cts +264 -0
- package/dist/matrx-oauth-_Ji8X5Wm.d.ts +264 -0
- package/dist/matrx-oauth.cjs +77 -10
- package/dist/matrx-oauth.d.cts +1 -181
- package/dist/matrx-oauth.d.ts +1 -181
- package/dist/matrx-oauth.js +77 -10
- package/dist/organizations.cjs +269 -13
- package/dist/organizations.d.cts +3 -225
- package/dist/organizations.d.ts +3 -225
- package/dist/organizations.js +269 -13
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/package.json +12 -2
- package/dist/session-DdQiStp6.d.cts +0 -112
- package/dist/session-DdQiStp6.d.ts +0 -112
- package/dist/storage-Dqnye4KF.d.cts +0 -51
- package/dist/storage-Dqnye4KF.d.ts +0 -51
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,
|
|
@@ -102,7 +119,8 @@ function organizationMatches(org, query) {
|
|
|
102
119
|
}
|
|
103
120
|
var MEMBERSHIP_PAGE = 500;
|
|
104
121
|
var ID_CHUNK = 100;
|
|
105
|
-
async function readMemberships(db, userId) {
|
|
122
|
+
async function readMemberships(db, userId, options = {}) {
|
|
123
|
+
const filter = options.archived ?? "active";
|
|
106
124
|
const memberships = [];
|
|
107
125
|
for (let from = 0; ; from += MEMBERSHIP_PAGE) {
|
|
108
126
|
const { data, error } = await db.rpc("mbr_for_user", { p_container_type: "organization" }).order("id", { ascending: true }).range(from, from + MEMBERSHIP_PAGE - 1);
|
|
@@ -125,7 +143,7 @@ async function readMemberships(db, userId) {
|
|
|
125
143
|
if (error) throw new Error(`Your organizations could not be read: ${error.message}`);
|
|
126
144
|
rows.push(...data ?? []);
|
|
127
145
|
}
|
|
128
|
-
return rows.filter((r) => !r.archived_at).map((r) => {
|
|
146
|
+
return rows.filter((r) => filter === "all" ? true : filter === "archived" ? !!r.archived_at : !r.archived_at).map((r) => {
|
|
129
147
|
const m = active.get(r.id);
|
|
130
148
|
const settings = r.settings && typeof r.settings === "object" ? r.settings : {};
|
|
131
149
|
return {
|
|
@@ -137,7 +155,8 @@ async function readMemberships(db, userId) {
|
|
|
137
155
|
role: m?.role ?? "member",
|
|
138
156
|
isOwn: !!r.created_by && r.created_by === userId,
|
|
139
157
|
isTestFixture: settings["test_fixture"] === true,
|
|
140
|
-
joinedAt: m?.created_at ?? ""
|
|
158
|
+
joinedAt: m?.created_at ?? "",
|
|
159
|
+
archivedAt: r.archived_at ?? null
|
|
141
160
|
};
|
|
142
161
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
143
162
|
}
|
|
@@ -238,6 +257,209 @@ function onSessionChange(client, cb) {
|
|
|
238
257
|
return () => data.subscription.unsubscribe();
|
|
239
258
|
}
|
|
240
259
|
|
|
260
|
+
// src/organization-gate.ts
|
|
261
|
+
var ORGANIZATION_RESOLUTION_TIMEOUT_MS = 1e4;
|
|
262
|
+
function createOrganizationBootstrapGate(defaultTimeoutMs = ORGANIZATION_RESOLUTION_TIMEOUT_MS) {
|
|
263
|
+
let settled = false;
|
|
264
|
+
let announce = null;
|
|
265
|
+
let resolution = new Promise((resolve) => {
|
|
266
|
+
announce = resolve;
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
isResolved: () => settled,
|
|
270
|
+
markResolved() {
|
|
271
|
+
if (settled) return;
|
|
272
|
+
settled = true;
|
|
273
|
+
announce?.();
|
|
274
|
+
},
|
|
275
|
+
async whenResolved(timeoutMs = defaultTimeoutMs) {
|
|
276
|
+
if (settled) return;
|
|
277
|
+
let timer;
|
|
278
|
+
await Promise.race([
|
|
279
|
+
resolution,
|
|
280
|
+
new Promise((resolve) => {
|
|
281
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
282
|
+
})
|
|
283
|
+
]);
|
|
284
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
285
|
+
},
|
|
286
|
+
reset() {
|
|
287
|
+
announce?.();
|
|
288
|
+
settled = false;
|
|
289
|
+
resolution = new Promise((resolve) => {
|
|
290
|
+
announce = resolve;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
var NOT_A_MEMBER_CODES = /* @__PURE__ */ new Set(["organization_forbidden", "organization_not_member"]);
|
|
296
|
+
function isNotAMemberRefusal(body) {
|
|
297
|
+
if (!body || typeof body !== "object") return false;
|
|
298
|
+
const record = body;
|
|
299
|
+
if (typeof record.code === "string" && NOT_A_MEMBER_CODES.has(record.code)) return true;
|
|
300
|
+
const detail = record.detail;
|
|
301
|
+
return !!detail && typeof detail === "object" && typeof detail.code === "string" && NOT_A_MEMBER_CODES.has(detail.code);
|
|
302
|
+
}
|
|
303
|
+
var NOT_A_MEMBER_RECHECK_GAP_MS = 3e4;
|
|
304
|
+
function createNotAMemberRefusalNotice(recheck, gapMs = NOT_A_MEMBER_RECHECK_GAP_MS) {
|
|
305
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
306
|
+
return {
|
|
307
|
+
notice(body, now = Date.now()) {
|
|
308
|
+
if (!isNotAMemberRefusal(body)) return false;
|
|
309
|
+
if (now - last < gapMs) return false;
|
|
310
|
+
last = now;
|
|
311
|
+
recheck();
|
|
312
|
+
return true;
|
|
313
|
+
},
|
|
314
|
+
reset() {
|
|
315
|
+
last = Number.NEGATIVE_INFINITY;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function resolveChildOrganizationId(parent, explicit, label = "child record", onDisagree = (m) => console.warn(m)) {
|
|
320
|
+
const parentOrg = typeof parent === "string" ? parent : parent?.organization_id ?? null;
|
|
321
|
+
if (parentOrg) {
|
|
322
|
+
if (explicit && explicit !== parentOrg) {
|
|
323
|
+
onDisagree(`[child org] ${label} filed in its parent's organization ${parentOrg}, not ${explicit} \u2014 a child belongs with its parent.`);
|
|
324
|
+
}
|
|
325
|
+
return parentOrg;
|
|
326
|
+
}
|
|
327
|
+
return explicit ?? null;
|
|
328
|
+
}
|
|
329
|
+
function resolveAdminSeat(input) {
|
|
330
|
+
if (!input || input.laneOpen !== true || !input.organizationId) return null;
|
|
331
|
+
return { id: input.organizationId, name: input.organizationName ?? null };
|
|
332
|
+
}
|
|
333
|
+
function adminSeatReplacesUserContext(input, personOrganizationId) {
|
|
334
|
+
const seat = resolveAdminSeat(input);
|
|
335
|
+
return seat !== null && seat.id !== (personOrganizationId ?? null);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/organization-link.ts
|
|
339
|
+
var import_uuid = require("@ai-matrx/kit/uuid");
|
|
340
|
+
var LINK_ORGANIZATION_QUERY_KEY = "org";
|
|
341
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,99}$/i;
|
|
342
|
+
function classifyLinkOrganizationValue(raw) {
|
|
343
|
+
const value = raw.trim();
|
|
344
|
+
if ((0, import_uuid.isRfc4122Uuid)(value)) return { kind: "named", organizationId: value };
|
|
345
|
+
if (SLUG.test(value)) return { kind: "addressed", slug: value.toLowerCase() };
|
|
346
|
+
return { kind: "malformed", raw: value };
|
|
347
|
+
}
|
|
348
|
+
function readLinkOrganizationParam(source) {
|
|
349
|
+
if (source == null) return { kind: "absent" };
|
|
350
|
+
let params;
|
|
351
|
+
if (source instanceof URLSearchParams) {
|
|
352
|
+
params = source;
|
|
353
|
+
} else {
|
|
354
|
+
try {
|
|
355
|
+
const q = source.includes("?") ? source.slice(source.indexOf("?")) : source;
|
|
356
|
+
params = new URLSearchParams(q);
|
|
357
|
+
} catch {
|
|
358
|
+
return { kind: "absent" };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!params.has(LINK_ORGANIZATION_QUERY_KEY)) return { kind: "absent" };
|
|
362
|
+
return classifyLinkOrganizationValue(params.get(LINK_ORGANIZATION_QUERY_KEY) ?? "");
|
|
363
|
+
}
|
|
364
|
+
function linkOrganizationNotAMemberMessage(signedInAs) {
|
|
365
|
+
const account = signedInAs ? `You are signed in as ${signedInAs}.` : "Check which account you are signed in as.";
|
|
366
|
+
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.`;
|
|
367
|
+
}
|
|
368
|
+
function linkOrganizationMalformedMessage(raw) {
|
|
369
|
+
const quoted = raw.length > 0 ? ` ("${raw}")` : "";
|
|
370
|
+
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.`;
|
|
371
|
+
}
|
|
372
|
+
function linkOrganizationSwitchAnnouncement(toName, fromName) {
|
|
373
|
+
const from = fromName ? ` You were working in ${fromName}.` : "";
|
|
374
|
+
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.`;
|
|
375
|
+
}
|
|
376
|
+
function linkOrganizationOfferMessage(toName, fromName) {
|
|
377
|
+
const where = fromName ? `, and you are working in ${fromName}` : "";
|
|
378
|
+
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.`;
|
|
379
|
+
}
|
|
380
|
+
function decideLinkOrganization(input) {
|
|
381
|
+
const { param, memberships, currentOrganizationId } = input;
|
|
382
|
+
const switchWhenALinkAsks = input.switchWhenALinkAsks ?? true;
|
|
383
|
+
const say = {
|
|
384
|
+
notAMember: linkOrganizationNotAMemberMessage,
|
|
385
|
+
malformed: linkOrganizationMalformedMessage,
|
|
386
|
+
switched: linkOrganizationSwitchAnnouncement,
|
|
387
|
+
offer: linkOrganizationOfferMessage,
|
|
388
|
+
offerAction: (name) => `Switch to ${name}`,
|
|
389
|
+
...input.messages
|
|
390
|
+
};
|
|
391
|
+
if (param.kind === "absent") return { kind: "no-link" };
|
|
392
|
+
if (param.kind === "malformed") {
|
|
393
|
+
return {
|
|
394
|
+
kind: "refused",
|
|
395
|
+
reason: "malformed",
|
|
396
|
+
message: say.malformed(param.raw)
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const match = param.kind === "named" ? memberships.find((o) => o.id === param.organizationId) : memberships.find((o) => o.slug?.trim().toLowerCase() === param.slug);
|
|
400
|
+
if (!match && param.kind === "named" && input.admittedByAShare) {
|
|
401
|
+
return { kind: "admitted", organizationId: param.organizationId };
|
|
402
|
+
}
|
|
403
|
+
if (!match) {
|
|
404
|
+
return {
|
|
405
|
+
kind: "refused",
|
|
406
|
+
reason: "not-a-member",
|
|
407
|
+
message: say.notAMember(input.signedInAs)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (currentOrganizationId === match.id) {
|
|
411
|
+
return { kind: "already-current", organizationId: match.id };
|
|
412
|
+
}
|
|
413
|
+
const fromName = currentOrganizationId ? input.currentOrganizationName ?? memberships.find((o) => o.id === currentOrganizationId)?.name ?? null : null;
|
|
414
|
+
if (currentOrganizationId && !switchWhenALinkAsks) {
|
|
415
|
+
return {
|
|
416
|
+
kind: "offered",
|
|
417
|
+
organizationId: match.id,
|
|
418
|
+
organizationName: match.name,
|
|
419
|
+
message: say.offer(match.name, fromName),
|
|
420
|
+
actionLabel: say.offerAction(match.name)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
kind: "honoured",
|
|
425
|
+
organizationId: match.id,
|
|
426
|
+
organizationName: match.name,
|
|
427
|
+
// Nothing to announce on a cold arrival: they were working nowhere, so
|
|
428
|
+
// they were not moved. A move, however, is always announced.
|
|
429
|
+
announcement: currentOrganizationId ? say.switched(match.name, fromName) : null
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
async function resolveOrganizationWithLink(input) {
|
|
433
|
+
const ladder = await resolveActiveOrganizationLazily({
|
|
434
|
+
memberships: input.memberships,
|
|
435
|
+
held: input.held ?? null,
|
|
436
|
+
readAccount: input.readAccount,
|
|
437
|
+
...input.first ? { first: input.first } : {}
|
|
438
|
+
});
|
|
439
|
+
if (!ladder) return null;
|
|
440
|
+
const param = typeof input.link === "string" ? input.link.includes("=") ? readLinkOrganizationParam(input.link) : classifyLinkOrganizationValue(input.link) : readLinkOrganizationParam(input.link ?? null);
|
|
441
|
+
const decisionInput = {
|
|
442
|
+
param,
|
|
443
|
+
memberships: input.memberships.map((o) => ({ id: o.id, name: o.name, slug: o.slug ?? null })),
|
|
444
|
+
currentOrganizationId: ladder.organization.id,
|
|
445
|
+
currentOrganizationName: ladder.organization.name,
|
|
446
|
+
...input.switchWhenALinkAsks === void 0 ? {} : { switchWhenALinkAsks: input.switchWhenALinkAsks },
|
|
447
|
+
...input.signedInAs === void 0 ? {} : { signedInAs: input.signedInAs },
|
|
448
|
+
...input.messages ? { messages: input.messages } : {}
|
|
449
|
+
};
|
|
450
|
+
let link = decideLinkOrganization(decisionInput);
|
|
451
|
+
if (link.kind === "refused" && link.reason === "not-a-member" && param.kind === "named" && input.admittedByAShare) {
|
|
452
|
+
if (await input.admittedByAShare(param.organizationId)) {
|
|
453
|
+
link = decideLinkOrganization({ ...decisionInput, admittedByAShare: true });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (link.kind === "honoured") {
|
|
457
|
+
const organization = input.memberships.find((m) => m.id === link.organizationId);
|
|
458
|
+
if (organization) return { organization, rung: "link", link };
|
|
459
|
+
}
|
|
460
|
+
return { organization: ladder.organization, rung: ladder.rung, link };
|
|
461
|
+
}
|
|
462
|
+
|
|
241
463
|
// src/active-organization.ts
|
|
242
464
|
var NoActiveOrganizationError = class extends Error {
|
|
243
465
|
constructor(reason, detail) {
|
|
@@ -315,6 +537,7 @@ function createActiveOrganization(options) {
|
|
|
315
537
|
const publish = (next) => {
|
|
316
538
|
current = Object.freeze(next);
|
|
317
539
|
if (current.status !== "resolving" && current.status !== "idle") {
|
|
540
|
+
options.bootstrapGate?.markResolved();
|
|
318
541
|
const ready = waiters;
|
|
319
542
|
waiters = [];
|
|
320
543
|
for (const w of ready) w();
|
|
@@ -332,19 +555,38 @@ function createActiveOrganization(options) {
|
|
|
332
555
|
}
|
|
333
556
|
const memberships = await readMemberships(client, person.id);
|
|
334
557
|
if (gen !== generation) return current;
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
558
|
+
const linkOptions = options.link;
|
|
559
|
+
let resolved;
|
|
560
|
+
let linkDecision = null;
|
|
561
|
+
if (linkOptions) {
|
|
562
|
+
const r = await resolveOrganizationWithLink({
|
|
563
|
+
memberships,
|
|
564
|
+
held: await held.read(person.id),
|
|
565
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id),
|
|
566
|
+
link: linkOptions.source() ?? null,
|
|
567
|
+
switchWhenALinkAsks: linkOptions.switchWhenALinkAsks?.(),
|
|
568
|
+
signedInAs: linkOptions.signedInAs?.(),
|
|
569
|
+
messages: linkOptions.messages,
|
|
570
|
+
admittedByAShare: linkOptions.admittedByAShare
|
|
571
|
+
});
|
|
572
|
+
resolved = r;
|
|
573
|
+
linkDecision = r?.link ?? null;
|
|
574
|
+
} else {
|
|
575
|
+
const linked = options.linked?.() ?? null;
|
|
576
|
+
const heldId = (linked && memberships.some((m) => m.id === linked) ? linked : null) ?? await held.read(person.id);
|
|
577
|
+
resolved = await resolveActiveOrganizationLazily({
|
|
578
|
+
memberships,
|
|
579
|
+
held: heldId,
|
|
580
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id)
|
|
581
|
+
});
|
|
582
|
+
}
|
|
342
583
|
if (gen !== generation) return current;
|
|
343
584
|
if (!resolved) {
|
|
344
585
|
publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
|
|
345
586
|
return current;
|
|
346
587
|
}
|
|
347
588
|
await held.write(person.id, resolved.organization.id);
|
|
589
|
+
if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
|
|
348
590
|
publish({
|
|
349
591
|
status: "ready",
|
|
350
592
|
userId: person.id,
|
|
@@ -380,6 +622,11 @@ function createActiveOrganization(options) {
|
|
|
380
622
|
const userId = session.user?.id ?? null;
|
|
381
623
|
if (event === "SIGNED_IN" && userId !== current.userId) void load();
|
|
382
624
|
});
|
|
625
|
+
const seatFor = (snap) => {
|
|
626
|
+
const seat = resolveAdminSeat(options.adminSeat?.());
|
|
627
|
+
return seat && snap.memberships.some((m) => m.id === seat.id) ? seat.id : null;
|
|
628
|
+
};
|
|
629
|
+
let lastNotAMemberRecheck = Number.NEGATIVE_INFINITY;
|
|
383
630
|
const settled = async () => {
|
|
384
631
|
if (current.status === "idle") return load();
|
|
385
632
|
if (current.status === "resolving") {
|
|
@@ -396,11 +643,11 @@ function createActiveOrganization(options) {
|
|
|
396
643
|
};
|
|
397
644
|
},
|
|
398
645
|
load,
|
|
399
|
-
current: () => current.status === "ready" ? current.organizationId : null,
|
|
646
|
+
current: () => current.status === "ready" ? seatFor(current) ?? current.organizationId : null,
|
|
400
647
|
async ensureOrgId(recordOrganizationId) {
|
|
401
648
|
if (recordOrganizationId) return recordOrganizationId;
|
|
402
649
|
const snap = await settled();
|
|
403
|
-
if (snap.status === "ready" && snap.organizationId) return snap.organizationId;
|
|
650
|
+
if (snap.status === "ready" && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
|
|
404
651
|
if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
|
|
405
652
|
throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
|
|
406
653
|
},
|
|
@@ -415,7 +662,16 @@ function createActiveOrganization(options) {
|
|
|
415
662
|
await writeLastActiveOrganization(client, organizationId);
|
|
416
663
|
},
|
|
417
664
|
async handleRefusal(status, body) {
|
|
418
|
-
|
|
665
|
+
let kind = classifyOrganizationRefusal(status, body);
|
|
666
|
+
if (kind === null && isNotAMemberRefusal(body)) {
|
|
667
|
+
const now = Date.now();
|
|
668
|
+
if (now - lastNotAMemberRecheck >= (options.refusalRecheckGapMs ?? NOT_A_MEMBER_RECHECK_GAP_MS)) {
|
|
669
|
+
lastNotAMemberRecheck = now;
|
|
670
|
+
kind = "lapsed";
|
|
671
|
+
} else {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
419
675
|
if (kind === "lapsed") {
|
|
420
676
|
if (current.userId) await held.clear();
|
|
421
677
|
await load();
|
package/dist/organizations.d.cts
CHANGED
|
@@ -1,227 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @ai-matrx/auth/organizations (moved from @ai-matrx/data/organizations, 2026-10-09) — THE active-organization ladder and the account's organization
|
|
6
|
-
* facts, for every Matrx client (the web, Matrx 2, the extension, Studio).
|
|
7
|
-
*
|
|
8
|
-
* 🚨 ONE ACTIVE ORGANIZATION, SET ONCE WHEN THE APP LOADS, NEVER NONE (Arman, 2026-10-07;
|
|
9
|
-
* organizations STATE rules 10–15). The ladder picks what the UI OPENS to; from then on every
|
|
10
|
-
* request carries the one active organization the person can see. Reads never filter by it.
|
|
11
|
-
*
|
|
12
|
-
* THE RUNGS, each kept only if it is a CURRENT membership:
|
|
13
|
-
* 1. HELD — what this tab (web) or this running app (desktop, in memory) already works in;
|
|
14
|
-
* 2. LAST ACTIVE — `users.user_preferences.last_active_organization_id` (follows the person
|
|
15
|
-
* across devices: STATE rule 12);
|
|
16
|
-
* 3. START-UP — `users.user_preferences.startup_organization_id` (the "Start-up organization"
|
|
17
|
-
* setting);
|
|
18
|
-
* 4. FIRST — the oldest active membership.
|
|
19
|
-
* Zero memberships → null. A failed read THROWS (the host shows "could not check" and a retry,
|
|
20
|
-
* never a guess). The ladder's own answer never writes anything (rule 14): only a person's click
|
|
21
|
-
* writes last active.
|
|
22
|
-
*
|
|
23
|
-
* Hosts with extra rungs keep them (the web's `?org=` link rung) and call the pure core here.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/** One organization the person is an active member of, as every picker draws it. */
|
|
27
|
-
interface OrganizationMembership {
|
|
28
|
-
readonly id: string;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly slug: string | null;
|
|
31
|
-
/** The database-validated 2–3 letters, when the organization has them. */
|
|
32
|
-
readonly abbreviation: string | null;
|
|
33
|
-
readonly logoUrl: string | null;
|
|
34
|
-
/** owner | admin | member (the membership's role). */
|
|
35
|
-
readonly role: string;
|
|
36
|
-
/** The person created it (drawn under the favourites, before the rest). */
|
|
37
|
-
readonly isOwn: boolean;
|
|
38
|
-
/** Classified in `iam.organizations.settings.test_fixture` — never guessed from a name. */
|
|
39
|
-
readonly isTestFixture: boolean;
|
|
40
|
-
/** When the membership began (ISO): the oldest is rung 4. */
|
|
41
|
-
readonly joinedAt: string;
|
|
42
|
-
}
|
|
43
|
-
interface AccountOrganizationChoices {
|
|
44
|
-
readonly lastActiveOrganizationId: string | null;
|
|
45
|
-
readonly startupOrganizationId: string | null;
|
|
46
|
-
}
|
|
47
|
-
type OrganizationRung = "held" | "last-active" | "startup" | "first";
|
|
48
|
-
interface ResolvedOrganization<T extends {
|
|
49
|
-
id: string;
|
|
50
|
-
}> {
|
|
51
|
-
readonly organization: T;
|
|
52
|
-
/** Which rung answered (for logs and tests; never shown as a choice). */
|
|
53
|
-
readonly rung: OrganizationRung;
|
|
54
|
-
}
|
|
55
|
-
interface LadderInput<T extends {
|
|
56
|
-
id: string;
|
|
57
|
-
}> {
|
|
58
|
-
readonly memberships: readonly T[];
|
|
59
|
-
readonly held?: string | null;
|
|
60
|
-
readonly account?: AccountOrganizationChoices | null;
|
|
61
|
-
/** Rung 4: the oldest active membership. Omitted: the oldest by `joinedAt`, else the first given. */
|
|
62
|
-
readonly first?: T | null;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* THE LADDER. Pure: no read, no write. Null only when there are no memberships.
|
|
66
|
-
*/
|
|
67
|
-
declare function resolveActiveOrganization<T extends {
|
|
68
|
-
id: string;
|
|
69
|
-
joinedAt?: string;
|
|
70
|
-
}>(input: LadderInput<T>): ResolvedOrganization<T> | null;
|
|
71
|
-
/**
|
|
72
|
-
* The ladder with its reads deferred: the account's choices are read only when no held
|
|
73
|
-
* organization answers, and the first membership only when nothing else does. A failed read
|
|
74
|
-
* throws through.
|
|
75
|
-
*/
|
|
76
|
-
declare function resolveActiveOrganizationLazily<T extends {
|
|
77
|
-
id: string;
|
|
78
|
-
joinedAt?: string;
|
|
79
|
-
}>(input: {
|
|
80
|
-
readonly memberships: readonly T[];
|
|
81
|
-
readonly held?: string | null;
|
|
82
|
-
readonly readAccount: () => Promise<AccountOrganizationChoices>;
|
|
83
|
-
readonly first?: () => Promise<T | null>;
|
|
84
|
-
}): Promise<ResolvedOrganization<T> | null>;
|
|
85
|
-
/** The oldest membership by `joinedAt` (ties and missing dates keep the given order). */
|
|
86
|
-
declare function oldestMembership<T extends {
|
|
87
|
-
joinedAt?: string;
|
|
88
|
-
}>(memberships: readonly T[]): T | null;
|
|
89
|
-
/**
|
|
90
|
-
* THE ORDER every organization list uses (the shared OrganizationPicker's): favourites, then the
|
|
91
|
-
* person's own organizations, then the rest; inside each band the given order is kept.
|
|
92
|
-
*/
|
|
93
|
-
declare function orderOrganizations<T extends {
|
|
94
|
-
id: string;
|
|
95
|
-
isOwn?: boolean;
|
|
96
|
-
}>(organizations: readonly T[], favouriteIds: readonly string[]): T[];
|
|
97
|
-
/** Search over the two things a person types: the name and the abbreviation. */
|
|
98
|
-
declare function organizationMatches(org: {
|
|
99
|
-
name: string;
|
|
100
|
-
abbreviation?: string | null;
|
|
101
|
-
}, query: string): boolean;
|
|
102
|
-
/** Any supabase-js client (typed or not). */
|
|
103
|
-
type OrganizationsDb = SupabaseClient<any, any, any>;
|
|
104
|
-
/**
|
|
105
|
-
* Every organization the signed-in person is an ACTIVE member of (archived organizations left
|
|
106
|
-
* out), through the canonical membership door (`mbr_for_user`, paged) and the organizations'
|
|
107
|
-
* own rows. Throws when either read fails; [] when there are none. Ordered by name.
|
|
108
|
-
*/
|
|
109
|
-
declare function readMemberships(db: OrganizationsDb, userId: string): Promise<OrganizationMembership[]>;
|
|
110
|
-
interface ChoicesRow {
|
|
111
|
-
last_active_organization_id: string | null;
|
|
112
|
-
startup_organization_id: string | null;
|
|
113
|
-
}
|
|
114
|
-
/** The two columns as the ladder's input. A person with no preferences row has neither. */
|
|
115
|
-
declare function toAccountOrganizationChoices(row: Partial<ChoicesRow> | null | undefined): AccountOrganizationChoices;
|
|
116
|
-
/**
|
|
117
|
-
* The account's last active and start-up organizations. 🚨 ONLY THE LADDER READS THESE: they
|
|
118
|
-
* choose what the shell opens to and nothing else. Throws when the read fails.
|
|
119
|
-
*/
|
|
120
|
-
declare function readAccountOrganizationChoices(db: OrganizationsDb, userId: string): Promise<AccountOrganizationChoices>;
|
|
121
|
-
/** Save where the person now works, for their next load on any device. Only a click calls this. Throws on refusal. */
|
|
122
|
-
declare function writeLastActiveOrganization(db: OrganizationsDb, organizationId: string): Promise<void>;
|
|
123
|
-
/** Set (or clear, with null) the Start-up organization setting. Throws on refusal. */
|
|
124
|
-
declare function writeStartupOrganization(db: OrganizationsDb, organizationId: string | null): Promise<void>;
|
|
125
|
-
/** The favourites store's entity type (`platform.user_entity_state`). */
|
|
126
|
-
declare const ORGANIZATION_ENTITY_TYPE = "organization";
|
|
127
|
-
/**
|
|
128
|
-
* The person's favourite organizations: the platform's per-person flag store
|
|
129
|
-
* (`public.ues_list`, kind favorite), the same store agent and model favourites use. Throws on
|
|
130
|
-
* failure.
|
|
131
|
-
*/
|
|
132
|
-
declare function readOrganizationFavourites(db: OrganizationsDb): Promise<string[]>;
|
|
133
|
-
/** Star (true) or unstar (false) one organization. `ues_set` refuses one the person cannot open. Throws on refusal. */
|
|
134
|
-
declare function setOrganizationFavourite(db: OrganizationsDb, organizationId: string, favourite: boolean): Promise<void>;
|
|
135
|
-
/**
|
|
136
|
-
* What the AI Matrx server's refusal of a request's organization means for the ladder
|
|
137
|
-
* (`matrx_connect` AuthMiddleware._organization_admission_words via `organization_free.py`):
|
|
138
|
-
* - 400 with `detail.organization_admission` "denied" (not a member), "unknown" (no such
|
|
139
|
-
* organization) or "archived" → the organization is gone for this person: re-run the ladder;
|
|
140
|
-
* - 503 with "unverifiable" (the lookup failed; Retry-After 5) → keep it and retry;
|
|
141
|
-
* - anything else → not an organization refusal.
|
|
142
|
-
*/
|
|
143
|
-
type OrganizationRefusal = "lapsed" | "retry" | null;
|
|
144
|
-
declare function classifyOrganizationRefusal(status: number, body: unknown): OrganizationRefusal;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* THE ACTIVE ORGANIZATION — one per tab / running app, set once when the app loads, never none
|
|
148
|
-
* while the person has a membership (Arman, 2026-10-07; organizations STATE rules 10–15).
|
|
149
|
-
*
|
|
150
|
-
* const active = createActiveOrganization({ client: supabase });
|
|
151
|
-
* await active.load(); // the ladder: held → last active → start-up → first
|
|
152
|
-
* const orgId = await active.ensureOrgId(); // HELD until the ladder answers; never a guess
|
|
153
|
-
* await active.switchTo(otherId); // a person's click: saves last active for next load
|
|
154
|
-
*
|
|
155
|
-
* HELD-REQUEST SEMANTICS. A write or server call that needs the organization asks
|
|
156
|
-
* `ensureOrgId()`: while the ladder is still resolving the call WAITS (it is held, never sent
|
|
157
|
-
* without an organization and never sent with a guessed one); once resolved it gets the one
|
|
158
|
-
* active organization; with zero memberships or a failed read it is REFUSED with a named error.
|
|
159
|
-
* Acting on a record passes that record's own organization (`ensureOrgId(record.organization_id)`),
|
|
160
|
-
* which wins and never switches the active one.
|
|
161
|
-
*
|
|
162
|
-
* 🚨 NEVER A LIST FILTER. Reads show everything the person can see across all their
|
|
163
|
-
* organizations; nothing here narrows a read. The active organization is only where new things
|
|
164
|
-
* are saved and which organization a server call runs in.
|
|
165
|
-
*
|
|
166
|
-
* The held id is identity-keyed: a different person signing in on the same device never inherits
|
|
167
|
-
* the last person's organization, and a sign-out resets the store.
|
|
168
|
-
*/
|
|
169
|
-
|
|
170
|
-
/** Where this tab / app remembers its organization between reloads (identity-keyed). */
|
|
171
|
-
interface HeldOrganizationStore {
|
|
172
|
-
read(userId: string): string | null | Promise<string | null>;
|
|
173
|
-
write(userId: string, organizationId: string): void | Promise<void>;
|
|
174
|
-
clear(): void | Promise<void>;
|
|
175
|
-
}
|
|
176
|
-
type ActiveOrganizationStatus = "idle" | "resolving" | "ready" | "none" | "error";
|
|
177
|
-
interface ActiveOrganizationSnapshot {
|
|
178
|
-
readonly status: ActiveOrganizationStatus;
|
|
179
|
-
readonly userId: string | null;
|
|
180
|
-
readonly organizationId: string | null;
|
|
181
|
-
readonly organization: OrganizationMembership | null;
|
|
182
|
-
readonly memberships: readonly OrganizationMembership[];
|
|
183
|
-
/** Which ladder rung answered (or "switched" after a person's click). */
|
|
184
|
-
readonly rung: OrganizationRung | "switched" | null;
|
|
185
|
-
readonly error: string | null;
|
|
186
|
-
}
|
|
187
|
-
declare class NoActiveOrganizationError extends Error {
|
|
188
|
-
readonly reason: "signed-out" | "no-memberships" | "unreadable";
|
|
189
|
-
constructor(reason: "signed-out" | "no-memberships" | "unreadable", detail?: string);
|
|
190
|
-
}
|
|
191
|
-
declare class NotAMemberError extends Error {
|
|
192
|
-
readonly organizationId: string;
|
|
193
|
-
constructor(organizationId: string);
|
|
194
|
-
}
|
|
195
|
-
interface ActiveOrganization {
|
|
196
|
-
snapshot(): ActiveOrganizationSnapshot;
|
|
197
|
-
subscribe(listener: () => void): () => void;
|
|
198
|
-
/** Run the ladder (again). Resolves with the settled snapshot; never throws. */
|
|
199
|
-
load(): Promise<ActiveOrganizationSnapshot>;
|
|
200
|
-
/** The active organization id now, or null while resolving / with none. Sync (api's getter). */
|
|
201
|
-
current(): string | null;
|
|
202
|
-
/** THE HELD REQUEST: the record's own organization, else the active one once resolved. */
|
|
203
|
-
ensureOrgId(recordOrganizationId?: string | null): Promise<string>;
|
|
204
|
-
/** A person's click: switch, remember for this tab, save as last active for the next load. */
|
|
205
|
-
switchTo(organizationId: string): Promise<void>;
|
|
206
|
-
/** Feed a server refusal in: a lapsed organization re-runs the ladder. Returns the class. */
|
|
207
|
-
handleRefusal(status: number, body: unknown): Promise<"lapsed" | "retry" | null>;
|
|
208
|
-
dispose(): void;
|
|
209
|
-
}
|
|
210
|
-
interface CreateActiveOrganizationOptions {
|
|
211
|
-
/** The person's Supabase client (any supabase-js client). */
|
|
212
|
-
readonly client: OrganizationsDb & AuthClient;
|
|
213
|
-
/** Where this tab/app holds its choice. Default: browser `sessionStorage` (per tab), else memory. */
|
|
214
|
-
readonly held?: HeldOrganizationStore | undefined;
|
|
215
|
-
/** An extra first rung (the web's `?org=` link). Kept only if it is a current membership. */
|
|
216
|
-
readonly linked?: (() => string | null | undefined) | undefined;
|
|
217
|
-
/** Load as soon as it is created (default true). */
|
|
218
|
-
readonly autoLoad?: boolean | undefined;
|
|
219
|
-
}
|
|
220
|
-
/** 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;
|
|
222
|
-
/** Held in memory: the running app's own choice (desktop, scripts, tests). */
|
|
223
|
-
declare function memoryHeldOrganization(): HeldOrganizationStore;
|
|
224
|
-
declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
|
|
1
|
+
export { h as AccountOrganizationChoices, j as ActiveOrganization, k as ActiveOrganizationSnapshot, l as ActiveOrganizationStatus, m as AdminSeat, n as AdminSeatInput, p as ArchiveFilter, C as CreateActiveOrganizationOptions, H as HeldOrganizationStore, L as LINK_ORGANIZATION_QUERY_KEY, q as LadderInput, t as LinkOrganizationDecision, v as LinkOrganizationDecisionInput, w as LinkOrganizationMembership, x as LinkOrganizationMessages, y as LinkOrganizationParam, N as NOT_A_MEMBER_RECHECK_GAP_MS, z as NoActiveOrganizationError, B as NotAMemberError, D as NotAMemberRefusalNotice, O as ORGANIZATION_ENTITY_TYPE, E as ORGANIZATION_RESOLUTION_TIMEOUT_MS, F as OrganizationBootstrapGate, G as OrganizationMembership, I as OrganizationRefusal, J as OrganizationRung, K as OrganizationsDb, P as ParentOrganization, R as ReadMembershipsOptions, M as ResolveWithLinkInput, Q as ResolvedOrganization, T as ResolvedWithLink, V as adminSeatReplacesUserContext, W as classifyLinkOrganizationValue, X as classifyOrganizationRefusal, Y as createActiveOrganization, Z as createNotAMemberRefusalNotice, _ as createOrganizationBootstrapGate, $ as decideLinkOrganization, a0 as isNotAMemberRefusal, a1 as linkOrganizationMalformedMessage, a2 as linkOrganizationNotAMemberMessage, a3 as linkOrganizationOfferMessage, a4 as linkOrganizationSwitchAnnouncement, a5 as memoryHeldOrganization, a6 as oldestMembership, a7 as orderOrganizations, a8 as organizationMatches, a9 as readAccountOrganizationChoices, aa as readLinkOrganizationParam, ab as readMemberships, ac as readOrganizationFavourites, ad as resolveActiveOrganization, ae as resolveActiveOrganizationLazily, af as resolveAdminSeat, ag as resolveChildOrganizationId, ah as resolveOrganizationWithLink, ai as setOrganizationFavourite, aj as storageHeldOrganization, ak as toAccountOrganizationChoices, al as writeLastActiveOrganization, am as writeStartupOrganization } from './active-organization-Dy4UtiyR.cjs';
|
|
2
|
+
import '@supabase/supabase-js';
|
|
225
3
|
|
|
226
4
|
/**
|
|
227
5
|
* "This request needs an organization and names none" — the ONE recogniser, the server's throwable
|
|
@@ -329,4 +107,4 @@ declare function organizationRequiredEnvelope(error: OrganizationRequiredServerE
|
|
|
329
107
|
/** The standard `Response` (status 400) a route answers the refusal with — no framework needed. */
|
|
330
108
|
declare function organizationRequiredResponse(error: OrganizationRequiredServerError): Response;
|
|
331
109
|
|
|
332
|
-
export {
|
|
110
|
+
export { MEMBERSHIPS_URL, type MembershipReadClient, ORGANIZATION_CONTEXT_REQUIRED_CODE, ORGANIZATION_REQUIRED_CODE, ORGANIZATION_REQUIRED_WIRE_CODE, type OrganizationHoldDetails, type OrganizationHoldSetOn, type OrganizationMembershipSummary, type OrganizationRequiredEnvelope, OrganizationRequiredServerError, type OrganizationRequiredWireMembership, extractOrganizationHoldMemberships, isOrganizationRequiredEnvelope, isOrganizationRequiredError, isOrganizationRequiredServerError, organizationRequired, organizationRequiredEnvelope, organizationRequiredResponse, readCallerMemberships };
|