@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.
@@ -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 linked = options.linked?.() ?? null;
336
- const heldId = (linked && memberships.some((m) => m.id === linked) ? linked : null) ?? await held.read(person.id);
337
- const resolved = await resolveActiveOrganizationLazily({
338
- memberships,
339
- held: heldId,
340
- readAccount: () => readAccountOrganizationChoices(client, person.id)
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
- const kind = classifyOrganizationRefusal(status, body);
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();
@@ -1,227 +1,5 @@
1
- import { SupabaseClient } from '@supabase/supabase-js';
2
- import { A as AuthClient } from './session-DdQiStp6.cjs';
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 { 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 };
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 };