@ai-matrx/auth 0.0.0 → 0.1.0
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 +25 -0
- package/LICENSE +21 -0
- package/README.md +31 -1
- package/dist/auth-cookie-DWx6Q3xo.d.cts +168 -0
- package/dist/auth-cookie-DWx6Q3xo.d.ts +168 -0
- package/dist/cookies.cjs +233 -0
- package/dist/cookies.d.cts +109 -0
- package/dist/cookies.d.ts +109 -0
- package/dist/cookies.js +210 -0
- package/dist/credentials.cjs +68 -0
- package/dist/credentials.d.cts +82 -0
- package/dist/credentials.d.ts +82 -0
- package/dist/credentials.js +47 -0
- package/dist/index.cjs +189 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +166 -0
- package/dist/next.cjs +655 -0
- package/dist/next.d.cts +389 -0
- package/dist/next.d.ts +389 -0
- package/dist/next.js +637 -0
- package/dist/node.cjs +61 -0
- package/dist/node.d.cts +33 -0
- package/dist/node.d.ts +33 -0
- package/dist/node.js +36 -0
- package/dist/organizations.cjs +389 -0
- package/dist/organizations.d.cts +226 -0
- package/dist/organizations.d.ts +226 -0
- package/dist/organizations.js +366 -0
- package/dist/react.cjs +206 -0
- package/dist/react.d.cts +163 -0
- package/dist/react.d.ts +163 -0
- package/dist/react.js +186 -0
- package/dist/session-4rdIyV28.d.cts +102 -0
- package/dist/session-4rdIyV28.d.ts +102 -0
- package/dist/session-identity.cjs +125 -0
- package/dist/session-identity.d.cts +100 -0
- package/dist/session-identity.d.ts +100 -0
- package/dist/session-identity.js +104 -0
- package/dist/testing.cjs +67 -0
- package/dist/testing.d.cts +38 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +46 -0
- package/package.json +186 -6
package/dist/node.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session storage per platform — where a Supabase session lives between runs.
|
|
3
|
+
*
|
|
4
|
+
* supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
|
|
5
|
+
* every Matrx client needs, written once:
|
|
6
|
+
*
|
|
7
|
+
* browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
|
|
8
|
+
* Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
|
|
9
|
+
* extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
|
|
10
|
+
* service worker; `localStorage` does not exist in a service worker)
|
|
11
|
+
* desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
|
|
12
|
+
* Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
|
|
13
|
+
*
|
|
14
|
+
* None of these is ever silently swapped for another: a storage the platform does not have throws
|
|
15
|
+
* a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
|
|
16
|
+
*/
|
|
17
|
+
/** The storage contract supabase-js reads (sync or async methods). */
|
|
18
|
+
interface SessionStorage {
|
|
19
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
20
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
21
|
+
removeItem(key: string): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @ai-matrx/auth/node — session storage for Node scripts and CLIs: the session survives between
|
|
26
|
+
* runs in one JSON file (mode 0600), so a script signs in once and refreshes after.
|
|
27
|
+
*
|
|
28
|
+
* const supabase = createAuthClient({ url, key, storage: fileStorage(".matrx-session.json") });
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
declare function fileStorage(path: string): SessionStorage;
|
|
32
|
+
|
|
33
|
+
export { fileStorage };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session storage per platform — where a Supabase session lives between runs.
|
|
3
|
+
*
|
|
4
|
+
* supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
|
|
5
|
+
* every Matrx client needs, written once:
|
|
6
|
+
*
|
|
7
|
+
* browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
|
|
8
|
+
* Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
|
|
9
|
+
* extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
|
|
10
|
+
* service worker; `localStorage` does not exist in a service worker)
|
|
11
|
+
* desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
|
|
12
|
+
* Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
|
|
13
|
+
*
|
|
14
|
+
* None of these is ever silently swapped for another: a storage the platform does not have throws
|
|
15
|
+
* a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
|
|
16
|
+
*/
|
|
17
|
+
/** The storage contract supabase-js reads (sync or async methods). */
|
|
18
|
+
interface SessionStorage {
|
|
19
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
20
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
21
|
+
removeItem(key: string): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @ai-matrx/auth/node — session storage for Node scripts and CLIs: the session survives between
|
|
26
|
+
* runs in one JSON file (mode 0600), so a script signs in once and refreshes after.
|
|
27
|
+
*
|
|
28
|
+
* const supabase = createAuthClient({ url, key, storage: fileStorage(".matrx-session.json") });
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
declare function fileStorage(path: string): SessionStorage;
|
|
32
|
+
|
|
33
|
+
export { fileStorage };
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/node.ts
|
|
2
|
+
function builtin(name) {
|
|
3
|
+
const get = globalThis.process?.getBuiltinModule;
|
|
4
|
+
if (!get) throw new Error(`fileStorage needs Node >= 20.16 (process.getBuiltinModule) to reach node:${name}`);
|
|
5
|
+
return get(`node:${name}`);
|
|
6
|
+
}
|
|
7
|
+
function fileStorage(path) {
|
|
8
|
+
const { mkdirSync, readFileSync, writeFileSync, chmodSync } = builtin("fs");
|
|
9
|
+
const { dirname } = builtin("path");
|
|
10
|
+
const readAll = () => {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (e.code === "ENOENT") return {};
|
|
16
|
+
throw new Error(`fileStorage: the session file ${path} could not be read: ${e.message}`);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const writeAll = (items) => {
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
21
|
+
writeFileSync(path, JSON.stringify(items), { mode: 384 });
|
|
22
|
+
chmodSync(path, 384);
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
getItem: (key) => readAll()[key] ?? null,
|
|
26
|
+
setItem: (key, value) => writeAll({ ...readAll(), [key]: value }),
|
|
27
|
+
removeItem: (key) => {
|
|
28
|
+
const items = readAll();
|
|
29
|
+
delete items[key];
|
|
30
|
+
writeAll(items);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
fileStorage
|
|
36
|
+
};
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/organizations.ts
|
|
21
|
+
var organizations_exports = {};
|
|
22
|
+
__export(organizations_exports, {
|
|
23
|
+
NoActiveOrganizationError: () => NoActiveOrganizationError,
|
|
24
|
+
NotAMemberError: () => NotAMemberError,
|
|
25
|
+
ORGANIZATION_ENTITY_TYPE: () => ORGANIZATION_ENTITY_TYPE,
|
|
26
|
+
classifyOrganizationRefusal: () => classifyOrganizationRefusal,
|
|
27
|
+
createActiveOrganization: () => createActiveOrganization,
|
|
28
|
+
memoryHeldOrganization: () => memoryHeldOrganization,
|
|
29
|
+
oldestMembership: () => oldestMembership,
|
|
30
|
+
orderOrganizations: () => orderOrganizations,
|
|
31
|
+
organizationMatches: () => organizationMatches,
|
|
32
|
+
readAccountOrganizationChoices: () => readAccountOrganizationChoices,
|
|
33
|
+
readMemberships: () => readMemberships,
|
|
34
|
+
readOrganizationFavourites: () => readOrganizationFavourites,
|
|
35
|
+
resolveActiveOrganization: () => resolveActiveOrganization,
|
|
36
|
+
resolveActiveOrganizationLazily: () => resolveActiveOrganizationLazily,
|
|
37
|
+
setOrganizationFavourite: () => setOrganizationFavourite,
|
|
38
|
+
storageHeldOrganization: () => storageHeldOrganization,
|
|
39
|
+
toAccountOrganizationChoices: () => toAccountOrganizationChoices,
|
|
40
|
+
writeLastActiveOrganization: () => writeLastActiveOrganization,
|
|
41
|
+
writeStartupOrganization: () => writeStartupOrganization
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(organizations_exports);
|
|
44
|
+
|
|
45
|
+
// src/organization-ladder.ts
|
|
46
|
+
function resolveActiveOrganization(input) {
|
|
47
|
+
const { memberships } = input;
|
|
48
|
+
if (memberships.length === 0) return null;
|
|
49
|
+
const member = (id) => id ? memberships.find((m) => m.id === id) ?? null : null;
|
|
50
|
+
const held = member(input.held);
|
|
51
|
+
if (held) return { organization: held, rung: "held" };
|
|
52
|
+
const last = member(input.account?.lastActiveOrganizationId);
|
|
53
|
+
if (last) return { organization: last, rung: "last-active" };
|
|
54
|
+
const startup = member(input.account?.startupOrganizationId);
|
|
55
|
+
if (startup) return { organization: startup, rung: "startup" };
|
|
56
|
+
const first = input.first && member(input.first.id) || oldestMembership(memberships);
|
|
57
|
+
return first ? { organization: first, rung: "first" } : null;
|
|
58
|
+
}
|
|
59
|
+
async function resolveActiveOrganizationLazily(input) {
|
|
60
|
+
const { memberships } = input;
|
|
61
|
+
if (memberships.length === 0) return null;
|
|
62
|
+
const quick = resolveActiveOrganization({ memberships, held: input.held ?? null, account: null, first: null });
|
|
63
|
+
if (quick?.rung === "held") return quick;
|
|
64
|
+
const account = await input.readAccount();
|
|
65
|
+
const named = resolveActiveOrganization({ memberships, held: null, account, first: null });
|
|
66
|
+
if (named && named.rung !== "first") return named;
|
|
67
|
+
const first = input.first ? await input.first() : null;
|
|
68
|
+
return resolveActiveOrganization({ memberships, held: null, account: null, first });
|
|
69
|
+
}
|
|
70
|
+
function oldestMembership(memberships) {
|
|
71
|
+
let best = null;
|
|
72
|
+
for (const m of memberships) {
|
|
73
|
+
if (!best) best = m;
|
|
74
|
+
else if (m.joinedAt && (!best.joinedAt || m.joinedAt < best.joinedAt)) best = m;
|
|
75
|
+
}
|
|
76
|
+
return best;
|
|
77
|
+
}
|
|
78
|
+
function orderOrganizations(organizations, favouriteIds) {
|
|
79
|
+
const favourites = new Set(favouriteIds);
|
|
80
|
+
const rank = (o) => favourites.has(o.id) ? 0 : o.isOwn ? 1 : 2;
|
|
81
|
+
return [...organizations].sort((a, b) => rank(a) - rank(b));
|
|
82
|
+
}
|
|
83
|
+
function organizationMatches(org, query) {
|
|
84
|
+
const q = query.trim().toLowerCase();
|
|
85
|
+
if (!q) return true;
|
|
86
|
+
if (org.name.toLowerCase().includes(q)) return true;
|
|
87
|
+
const abbreviation = org.abbreviation?.trim().toLowerCase();
|
|
88
|
+
return !!abbreviation && abbreviation.includes(q);
|
|
89
|
+
}
|
|
90
|
+
var MEMBERSHIP_PAGE = 500;
|
|
91
|
+
var ID_CHUNK = 100;
|
|
92
|
+
async function readMemberships(db, userId) {
|
|
93
|
+
const memberships = [];
|
|
94
|
+
for (let from = 0; ; from += MEMBERSHIP_PAGE) {
|
|
95
|
+
const { data, error } = await db.rpc("mbr_for_user", { p_container_type: "organization" }).order("id", { ascending: true }).range(from, from + MEMBERSHIP_PAGE - 1);
|
|
96
|
+
if (error) throw new Error(`Your organizations could not be read: ${error.message}`);
|
|
97
|
+
const page = Array.isArray(data) ? data : [];
|
|
98
|
+
memberships.push(...page);
|
|
99
|
+
if (page.length < MEMBERSHIP_PAGE) break;
|
|
100
|
+
}
|
|
101
|
+
const active = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const m of memberships) {
|
|
103
|
+
if (m.status !== "active") continue;
|
|
104
|
+
const seen = active.get(m.container_id);
|
|
105
|
+
if (!seen || m.created_at < seen.created_at) active.set(m.container_id, m);
|
|
106
|
+
}
|
|
107
|
+
const ids = [...active.keys()];
|
|
108
|
+
if (ids.length === 0) return [];
|
|
109
|
+
const rows = [];
|
|
110
|
+
for (let i = 0; i < ids.length; i += ID_CHUNK) {
|
|
111
|
+
const { data, error } = await db.schema("iam").from("organizations").select("id, name, slug, abbreviation, logo_url, created_by, settings, archived_at").in("id", ids.slice(i, i + ID_CHUNK));
|
|
112
|
+
if (error) throw new Error(`Your organizations could not be read: ${error.message}`);
|
|
113
|
+
rows.push(...data ?? []);
|
|
114
|
+
}
|
|
115
|
+
return rows.filter((r) => !r.archived_at).map((r) => {
|
|
116
|
+
const m = active.get(r.id);
|
|
117
|
+
const settings = r.settings && typeof r.settings === "object" ? r.settings : {};
|
|
118
|
+
return {
|
|
119
|
+
id: r.id,
|
|
120
|
+
name: r.name?.trim() || "Untitled organization",
|
|
121
|
+
slug: r.slug ?? null,
|
|
122
|
+
abbreviation: r.abbreviation?.trim() || null,
|
|
123
|
+
logoUrl: r.logo_url?.trim() || null,
|
|
124
|
+
role: m?.role ?? "member",
|
|
125
|
+
isOwn: !!r.created_by && r.created_by === userId,
|
|
126
|
+
isTestFixture: settings["test_fixture"] === true,
|
|
127
|
+
joinedAt: m?.created_at ?? ""
|
|
128
|
+
};
|
|
129
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
130
|
+
}
|
|
131
|
+
function toAccountOrganizationChoices(row) {
|
|
132
|
+
return {
|
|
133
|
+
lastActiveOrganizationId: row?.last_active_organization_id ?? null,
|
|
134
|
+
startupOrganizationId: row?.startup_organization_id ?? null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function readAccountOrganizationChoices(db, userId) {
|
|
138
|
+
const { data, error } = await db.schema("users").from("user_preferences").select("last_active_organization_id, startup_organization_id").eq("user_id", userId).maybeSingle();
|
|
139
|
+
if (error) throw new Error(`The account's organization read failed: ${error.message}`);
|
|
140
|
+
return toAccountOrganizationChoices(data);
|
|
141
|
+
}
|
|
142
|
+
async function writeLastActiveOrganization(db, organizationId) {
|
|
143
|
+
const { error } = await db.schema("users").rpc("set_last_active_organization", { p_organization_id: organizationId });
|
|
144
|
+
if (error) throw new Error(error.message);
|
|
145
|
+
}
|
|
146
|
+
async function writeStartupOrganization(db, organizationId) {
|
|
147
|
+
const { error } = await db.schema("users").rpc("set_startup_organization", { p_organization_id: organizationId });
|
|
148
|
+
if (error) throw new Error(error.message);
|
|
149
|
+
}
|
|
150
|
+
var ORGANIZATION_ENTITY_TYPE = "organization";
|
|
151
|
+
async function readOrganizationFavourites(db) {
|
|
152
|
+
const { data, error } = await db.rpc("ues_list", { p_kind: "favorite" });
|
|
153
|
+
if (error) throw new Error(`Your favourite organizations could not be read: ${error.message}`);
|
|
154
|
+
return (Array.isArray(data) ? data : []).filter((r) => r.entity_type === ORGANIZATION_ENTITY_TYPE && r.is_favorite).map((r) => r.entity_id);
|
|
155
|
+
}
|
|
156
|
+
async function setOrganizationFavourite(db, organizationId, favourite) {
|
|
157
|
+
const { error } = await db.rpc("ues_set", { p_entity_type: ORGANIZATION_ENTITY_TYPE, p_entity_id: organizationId, p_is_favorite: favourite });
|
|
158
|
+
if (error) throw new Error(error.message);
|
|
159
|
+
}
|
|
160
|
+
function classifyOrganizationRefusal(status, body) {
|
|
161
|
+
const detail = body && typeof body === "object" ? body["detail"] ?? body : null;
|
|
162
|
+
const admission = detail && typeof detail === "object" ? detail["organization_admission"] : null;
|
|
163
|
+
if (status === 400 && (admission === "denied" || admission === "unknown" || admission === "archived")) return "lapsed";
|
|
164
|
+
if (status === 503 && admission === "unverifiable") return "retry";
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/session.ts
|
|
169
|
+
var SignInError = class extends Error {
|
|
170
|
+
constructor(door, message, status, code) {
|
|
171
|
+
super(`${door}: ${message}`);
|
|
172
|
+
this.door = door;
|
|
173
|
+
this.status = status;
|
|
174
|
+
this.code = code;
|
|
175
|
+
this.name = "SignInError";
|
|
176
|
+
}
|
|
177
|
+
door;
|
|
178
|
+
status;
|
|
179
|
+
code;
|
|
180
|
+
};
|
|
181
|
+
function check(door, data, error) {
|
|
182
|
+
if (error) throw new SignInError(door, error.message, error.status, error.code);
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
async function getPerson(client) {
|
|
186
|
+
const { data, error } = await client.auth.getUser();
|
|
187
|
+
if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
|
|
188
|
+
return check("getPerson", data, error).user;
|
|
189
|
+
}
|
|
190
|
+
function onSessionChange(client, cb) {
|
|
191
|
+
const { data } = client.auth.onAuthStateChange(cb);
|
|
192
|
+
return () => data.subscription.unsubscribe();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/active-organization.ts
|
|
196
|
+
var NoActiveOrganizationError = class extends Error {
|
|
197
|
+
constructor(reason, detail) {
|
|
198
|
+
super(
|
|
199
|
+
reason === "signed-out" ? "No organization: nobody is signed in." : reason === "no-memberships" ? "No organization: this account is not an active member of any organization." : `No organization: your organizations could not be checked${detail ? ` (${detail})` : ""}.`
|
|
200
|
+
);
|
|
201
|
+
this.reason = reason;
|
|
202
|
+
this.name = "NoActiveOrganizationError";
|
|
203
|
+
}
|
|
204
|
+
reason;
|
|
205
|
+
};
|
|
206
|
+
var NotAMemberError = class extends Error {
|
|
207
|
+
constructor(organizationId) {
|
|
208
|
+
super(`You are not an active member of organization ${organizationId}.`);
|
|
209
|
+
this.organizationId = organizationId;
|
|
210
|
+
this.name = "NotAMemberError";
|
|
211
|
+
}
|
|
212
|
+
organizationId;
|
|
213
|
+
};
|
|
214
|
+
var HELD_KEY = "ai-matrx.auth.active-organization";
|
|
215
|
+
function storageHeldOrganization(storage, key = HELD_KEY) {
|
|
216
|
+
return {
|
|
217
|
+
read(userId) {
|
|
218
|
+
try {
|
|
219
|
+
const raw = storage.getItem(key);
|
|
220
|
+
if (!raw) return null;
|
|
221
|
+
const parsed = JSON.parse(raw);
|
|
222
|
+
return parsed.userId === userId && typeof parsed.organizationId === "string" ? parsed.organizationId : null;
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
write(userId, organizationId) {
|
|
228
|
+
storage.setItem(key, JSON.stringify({ userId, organizationId }));
|
|
229
|
+
},
|
|
230
|
+
clear() {
|
|
231
|
+
storage.removeItem(key);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function memoryHeldOrganization() {
|
|
236
|
+
let held = null;
|
|
237
|
+
return {
|
|
238
|
+
read: (userId) => held && held.userId === userId ? held.organizationId : null,
|
|
239
|
+
write: (userId, organizationId) => {
|
|
240
|
+
held = { userId, organizationId };
|
|
241
|
+
},
|
|
242
|
+
clear: () => {
|
|
243
|
+
held = null;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function defaultHeld() {
|
|
248
|
+
const session = globalThis.sessionStorage;
|
|
249
|
+
return session ? storageHeldOrganization(session) : memoryHeldOrganization();
|
|
250
|
+
}
|
|
251
|
+
var IDLE = Object.freeze({
|
|
252
|
+
status: "idle",
|
|
253
|
+
userId: null,
|
|
254
|
+
organizationId: null,
|
|
255
|
+
organization: null,
|
|
256
|
+
memberships: Object.freeze([]),
|
|
257
|
+
rung: null,
|
|
258
|
+
error: null
|
|
259
|
+
});
|
|
260
|
+
function createActiveOrganization(options) {
|
|
261
|
+
const { client } = options;
|
|
262
|
+
const held = options.held ?? defaultHeld();
|
|
263
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
264
|
+
let current = IDLE;
|
|
265
|
+
let inflight = null;
|
|
266
|
+
let generation = 0;
|
|
267
|
+
let waiters = [];
|
|
268
|
+
let disposed = false;
|
|
269
|
+
const publish = (next) => {
|
|
270
|
+
current = Object.freeze(next);
|
|
271
|
+
if (current.status !== "resolving" && current.status !== "idle") {
|
|
272
|
+
const ready = waiters;
|
|
273
|
+
waiters = [];
|
|
274
|
+
for (const w of ready) w();
|
|
275
|
+
}
|
|
276
|
+
for (const l of [...listeners]) l();
|
|
277
|
+
};
|
|
278
|
+
const run = async (gen) => {
|
|
279
|
+
publish({ ...current, status: "resolving", error: null });
|
|
280
|
+
try {
|
|
281
|
+
const person = await getPerson(client);
|
|
282
|
+
if (gen !== generation) return current;
|
|
283
|
+
if (!person) {
|
|
284
|
+
publish({ ...IDLE, status: "none", error: "signed-out" });
|
|
285
|
+
return current;
|
|
286
|
+
}
|
|
287
|
+
const memberships = await readMemberships(client, person.id);
|
|
288
|
+
if (gen !== generation) return current;
|
|
289
|
+
const linked = options.linked?.() ?? null;
|
|
290
|
+
const heldId = (linked && memberships.some((m) => m.id === linked) ? linked : null) ?? await held.read(person.id);
|
|
291
|
+
const resolved = await resolveActiveOrganizationLazily({
|
|
292
|
+
memberships,
|
|
293
|
+
held: heldId,
|
|
294
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id)
|
|
295
|
+
});
|
|
296
|
+
if (gen !== generation) return current;
|
|
297
|
+
if (!resolved) {
|
|
298
|
+
publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
|
|
299
|
+
return current;
|
|
300
|
+
}
|
|
301
|
+
await held.write(person.id, resolved.organization.id);
|
|
302
|
+
publish({
|
|
303
|
+
status: "ready",
|
|
304
|
+
userId: person.id,
|
|
305
|
+
organizationId: resolved.organization.id,
|
|
306
|
+
organization: resolved.organization,
|
|
307
|
+
memberships,
|
|
308
|
+
rung: resolved.rung,
|
|
309
|
+
error: null
|
|
310
|
+
});
|
|
311
|
+
return current;
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (gen !== generation) return current;
|
|
314
|
+
publish({ ...current, status: "error", error: e?.message ?? String(e) });
|
|
315
|
+
return current;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const load = () => {
|
|
319
|
+
if (disposed) return Promise.resolve(current);
|
|
320
|
+
generation += 1;
|
|
321
|
+
inflight = run(generation).finally(() => {
|
|
322
|
+
inflight = null;
|
|
323
|
+
});
|
|
324
|
+
return inflight;
|
|
325
|
+
};
|
|
326
|
+
const stopSession = onSessionChange(client, (event, session) => {
|
|
327
|
+
if (event === "SIGNED_OUT" || !session) {
|
|
328
|
+
generation += 1;
|
|
329
|
+
void held.clear();
|
|
330
|
+
publish({ ...IDLE, status: "none", error: "signed-out" });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const userId = session.user?.id ?? null;
|
|
334
|
+
if (event === "SIGNED_IN" && userId !== current.userId) void load();
|
|
335
|
+
});
|
|
336
|
+
const settled = async () => {
|
|
337
|
+
if (current.status === "idle") return load();
|
|
338
|
+
if (current.status === "resolving") {
|
|
339
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
340
|
+
}
|
|
341
|
+
return current;
|
|
342
|
+
};
|
|
343
|
+
const api = {
|
|
344
|
+
snapshot: () => current,
|
|
345
|
+
subscribe(listener) {
|
|
346
|
+
listeners.add(listener);
|
|
347
|
+
return () => {
|
|
348
|
+
listeners.delete(listener);
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
load,
|
|
352
|
+
current: () => current.status === "ready" ? current.organizationId : null,
|
|
353
|
+
async ensureOrgId(recordOrganizationId) {
|
|
354
|
+
if (recordOrganizationId) return recordOrganizationId;
|
|
355
|
+
const snap = await settled();
|
|
356
|
+
if (snap.status === "ready" && snap.organizationId) return snap.organizationId;
|
|
357
|
+
if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
|
|
358
|
+
throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
|
|
359
|
+
},
|
|
360
|
+
async switchTo(organizationId) {
|
|
361
|
+
const snap = await settled();
|
|
362
|
+
const organization = snap.memberships.find((m) => m.id === organizationId);
|
|
363
|
+
if (!organization || !snap.userId) throw new NotAMemberError(organizationId);
|
|
364
|
+
generation += 1;
|
|
365
|
+
await held.write(snap.userId, organizationId);
|
|
366
|
+
publish({ ...snap, status: "ready", organizationId, organization, rung: "switched", error: null });
|
|
367
|
+
await writeLastActiveOrganization(client, organizationId);
|
|
368
|
+
},
|
|
369
|
+
async handleRefusal(status, body) {
|
|
370
|
+
const kind = classifyOrganizationRefusal(status, body);
|
|
371
|
+
if (kind === "lapsed") {
|
|
372
|
+
if (current.userId) await held.clear();
|
|
373
|
+
await load();
|
|
374
|
+
}
|
|
375
|
+
return kind;
|
|
376
|
+
},
|
|
377
|
+
dispose() {
|
|
378
|
+
disposed = true;
|
|
379
|
+
generation += 1;
|
|
380
|
+
stopSession();
|
|
381
|
+
listeners.clear();
|
|
382
|
+
const pending = waiters;
|
|
383
|
+
waiters = [];
|
|
384
|
+
for (const w of pending) w();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
if (options.autoLoad ?? true) void load();
|
|
388
|
+
return api;
|
|
389
|
+
}
|