@ai-matrx/auth 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +11 -0
- package/dist/claims-DXdbe3cW.d.cts +138 -0
- package/dist/claims-DXdbe3cW.d.ts +138 -0
- package/dist/index.cjs +135 -1
- package/dist/index.d.cts +4 -51
- package/dist/index.d.ts +4 -51
- package/dist/index.js +138 -1
- package/dist/matrx-oauth.cjs +458 -0
- package/dist/matrx-oauth.d.cts +181 -0
- package/dist/matrx-oauth.d.ts +181 -0
- package/dist/matrx-oauth.js +437 -0
- package/dist/next.cjs +133 -1
- package/dist/next.d.cts +77 -1
- package/dist/next.d.ts +77 -1
- package/dist/next.js +136 -1
- package/dist/node.cjs +139 -2
- package/dist/node.d.cts +117 -1
- package/dist/node.d.ts +117 -1
- package/dist/node.js +136 -1
- package/dist/organizations.cjs +358 -10
- package/dist/organizations.d.cts +359 -3
- package/dist/organizations.d.ts +359 -3
- package/dist/organizations.js +358 -10
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/storage-Dqnye4KF.d.cts +51 -0
- package/dist/storage-Dqnye4KF.d.ts +51 -0
- package/package.json +12 -2
package/dist/organizations.js
CHANGED
|
@@ -181,6 +181,209 @@ function onSessionChange(client, cb) {
|
|
|
181
181
|
return () => data.subscription.unsubscribe();
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// src/organization-gate.ts
|
|
185
|
+
var ORGANIZATION_RESOLUTION_TIMEOUT_MS = 1e4;
|
|
186
|
+
function createOrganizationBootstrapGate(defaultTimeoutMs = ORGANIZATION_RESOLUTION_TIMEOUT_MS) {
|
|
187
|
+
let settled = false;
|
|
188
|
+
let announce = null;
|
|
189
|
+
let resolution = new Promise((resolve) => {
|
|
190
|
+
announce = resolve;
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
isResolved: () => settled,
|
|
194
|
+
markResolved() {
|
|
195
|
+
if (settled) return;
|
|
196
|
+
settled = true;
|
|
197
|
+
announce?.();
|
|
198
|
+
},
|
|
199
|
+
async whenResolved(timeoutMs = defaultTimeoutMs) {
|
|
200
|
+
if (settled) return;
|
|
201
|
+
let timer;
|
|
202
|
+
await Promise.race([
|
|
203
|
+
resolution,
|
|
204
|
+
new Promise((resolve) => {
|
|
205
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
206
|
+
})
|
|
207
|
+
]);
|
|
208
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
209
|
+
},
|
|
210
|
+
reset() {
|
|
211
|
+
announce?.();
|
|
212
|
+
settled = false;
|
|
213
|
+
resolution = new Promise((resolve) => {
|
|
214
|
+
announce = resolve;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
var NOT_A_MEMBER_CODES = /* @__PURE__ */ new Set(["organization_forbidden", "organization_not_member"]);
|
|
220
|
+
function isNotAMemberRefusal(body) {
|
|
221
|
+
if (!body || typeof body !== "object") return false;
|
|
222
|
+
const record = body;
|
|
223
|
+
if (typeof record.code === "string" && NOT_A_MEMBER_CODES.has(record.code)) return true;
|
|
224
|
+
const detail = record.detail;
|
|
225
|
+
return !!detail && typeof detail === "object" && typeof detail.code === "string" && NOT_A_MEMBER_CODES.has(detail.code);
|
|
226
|
+
}
|
|
227
|
+
var NOT_A_MEMBER_RECHECK_GAP_MS = 3e4;
|
|
228
|
+
function createNotAMemberRefusalNotice(recheck, gapMs = NOT_A_MEMBER_RECHECK_GAP_MS) {
|
|
229
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
230
|
+
return {
|
|
231
|
+
notice(body, now = Date.now()) {
|
|
232
|
+
if (!isNotAMemberRefusal(body)) return false;
|
|
233
|
+
if (now - last < gapMs) return false;
|
|
234
|
+
last = now;
|
|
235
|
+
recheck();
|
|
236
|
+
return true;
|
|
237
|
+
},
|
|
238
|
+
reset() {
|
|
239
|
+
last = Number.NEGATIVE_INFINITY;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function resolveChildOrganizationId(parent, explicit, label = "child record", onDisagree = (m) => console.warn(m)) {
|
|
244
|
+
const parentOrg = typeof parent === "string" ? parent : parent?.organization_id ?? null;
|
|
245
|
+
if (parentOrg) {
|
|
246
|
+
if (explicit && explicit !== parentOrg) {
|
|
247
|
+
onDisagree(`[child org] ${label} filed in its parent's organization ${parentOrg}, not ${explicit} \u2014 a child belongs with its parent.`);
|
|
248
|
+
}
|
|
249
|
+
return parentOrg;
|
|
250
|
+
}
|
|
251
|
+
return explicit ?? null;
|
|
252
|
+
}
|
|
253
|
+
function resolveAdminSeat(input) {
|
|
254
|
+
if (!input || input.laneOpen !== true || !input.organizationId) return null;
|
|
255
|
+
return { id: input.organizationId, name: input.organizationName ?? null };
|
|
256
|
+
}
|
|
257
|
+
function adminSeatReplacesUserContext(input, personOrganizationId) {
|
|
258
|
+
const seat = resolveAdminSeat(input);
|
|
259
|
+
return seat !== null && seat.id !== (personOrganizationId ?? null);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/organization-link.ts
|
|
263
|
+
import { isRfc4122Uuid } from "@ai-matrx/kit/uuid";
|
|
264
|
+
var LINK_ORGANIZATION_QUERY_KEY = "org";
|
|
265
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,99}$/i;
|
|
266
|
+
function classifyLinkOrganizationValue(raw) {
|
|
267
|
+
const value = raw.trim();
|
|
268
|
+
if (isRfc4122Uuid(value)) return { kind: "named", organizationId: value };
|
|
269
|
+
if (SLUG.test(value)) return { kind: "addressed", slug: value.toLowerCase() };
|
|
270
|
+
return { kind: "malformed", raw: value };
|
|
271
|
+
}
|
|
272
|
+
function readLinkOrganizationParam(source) {
|
|
273
|
+
if (source == null) return { kind: "absent" };
|
|
274
|
+
let params;
|
|
275
|
+
if (source instanceof URLSearchParams) {
|
|
276
|
+
params = source;
|
|
277
|
+
} else {
|
|
278
|
+
try {
|
|
279
|
+
const q = source.includes("?") ? source.slice(source.indexOf("?")) : source;
|
|
280
|
+
params = new URLSearchParams(q);
|
|
281
|
+
} catch {
|
|
282
|
+
return { kind: "absent" };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!params.has(LINK_ORGANIZATION_QUERY_KEY)) return { kind: "absent" };
|
|
286
|
+
return classifyLinkOrganizationValue(params.get(LINK_ORGANIZATION_QUERY_KEY) ?? "");
|
|
287
|
+
}
|
|
288
|
+
function linkOrganizationNotAMemberMessage(signedInAs) {
|
|
289
|
+
const account = signedInAs ? `You are signed in as ${signedInAs}.` : "Check which account you are signed in as.";
|
|
290
|
+
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.`;
|
|
291
|
+
}
|
|
292
|
+
function linkOrganizationMalformedMessage(raw) {
|
|
293
|
+
const quoted = raw.length > 0 ? ` ("${raw}")` : "";
|
|
294
|
+
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.`;
|
|
295
|
+
}
|
|
296
|
+
function linkOrganizationSwitchAnnouncement(toName, fromName) {
|
|
297
|
+
const from = fromName ? ` You were working in ${fromName}.` : "";
|
|
298
|
+
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.`;
|
|
299
|
+
}
|
|
300
|
+
function linkOrganizationOfferMessage(toName, fromName) {
|
|
301
|
+
const where = fromName ? `, and you are working in ${fromName}` : "";
|
|
302
|
+
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.`;
|
|
303
|
+
}
|
|
304
|
+
function decideLinkOrganization(input) {
|
|
305
|
+
const { param, memberships, currentOrganizationId } = input;
|
|
306
|
+
const switchWhenALinkAsks = input.switchWhenALinkAsks ?? true;
|
|
307
|
+
const say = {
|
|
308
|
+
notAMember: linkOrganizationNotAMemberMessage,
|
|
309
|
+
malformed: linkOrganizationMalformedMessage,
|
|
310
|
+
switched: linkOrganizationSwitchAnnouncement,
|
|
311
|
+
offer: linkOrganizationOfferMessage,
|
|
312
|
+
offerAction: (name) => `Switch to ${name}`,
|
|
313
|
+
...input.messages
|
|
314
|
+
};
|
|
315
|
+
if (param.kind === "absent") return { kind: "no-link" };
|
|
316
|
+
if (param.kind === "malformed") {
|
|
317
|
+
return {
|
|
318
|
+
kind: "refused",
|
|
319
|
+
reason: "malformed",
|
|
320
|
+
message: say.malformed(param.raw)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const match = param.kind === "named" ? memberships.find((o) => o.id === param.organizationId) : memberships.find((o) => o.slug?.trim().toLowerCase() === param.slug);
|
|
324
|
+
if (!match && param.kind === "named" && input.admittedByAShare) {
|
|
325
|
+
return { kind: "admitted", organizationId: param.organizationId };
|
|
326
|
+
}
|
|
327
|
+
if (!match) {
|
|
328
|
+
return {
|
|
329
|
+
kind: "refused",
|
|
330
|
+
reason: "not-a-member",
|
|
331
|
+
message: say.notAMember(input.signedInAs)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (currentOrganizationId === match.id) {
|
|
335
|
+
return { kind: "already-current", organizationId: match.id };
|
|
336
|
+
}
|
|
337
|
+
const fromName = currentOrganizationId ? input.currentOrganizationName ?? memberships.find((o) => o.id === currentOrganizationId)?.name ?? null : null;
|
|
338
|
+
if (currentOrganizationId && !switchWhenALinkAsks) {
|
|
339
|
+
return {
|
|
340
|
+
kind: "offered",
|
|
341
|
+
organizationId: match.id,
|
|
342
|
+
organizationName: match.name,
|
|
343
|
+
message: say.offer(match.name, fromName),
|
|
344
|
+
actionLabel: say.offerAction(match.name)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
kind: "honoured",
|
|
349
|
+
organizationId: match.id,
|
|
350
|
+
organizationName: match.name,
|
|
351
|
+
// Nothing to announce on a cold arrival: they were working nowhere, so
|
|
352
|
+
// they were not moved. A move, however, is always announced.
|
|
353
|
+
announcement: currentOrganizationId ? say.switched(match.name, fromName) : null
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async function resolveOrganizationWithLink(input) {
|
|
357
|
+
const ladder = await resolveActiveOrganizationLazily({
|
|
358
|
+
memberships: input.memberships,
|
|
359
|
+
held: input.held ?? null,
|
|
360
|
+
readAccount: input.readAccount,
|
|
361
|
+
...input.first ? { first: input.first } : {}
|
|
362
|
+
});
|
|
363
|
+
if (!ladder) return null;
|
|
364
|
+
const param = typeof input.link === "string" ? input.link.includes("=") ? readLinkOrganizationParam(input.link) : classifyLinkOrganizationValue(input.link) : readLinkOrganizationParam(input.link ?? null);
|
|
365
|
+
const decisionInput = {
|
|
366
|
+
param,
|
|
367
|
+
memberships: input.memberships.map((o) => ({ id: o.id, name: o.name, slug: o.slug ?? null })),
|
|
368
|
+
currentOrganizationId: ladder.organization.id,
|
|
369
|
+
currentOrganizationName: ladder.organization.name,
|
|
370
|
+
...input.switchWhenALinkAsks === void 0 ? {} : { switchWhenALinkAsks: input.switchWhenALinkAsks },
|
|
371
|
+
...input.signedInAs === void 0 ? {} : { signedInAs: input.signedInAs },
|
|
372
|
+
...input.messages ? { messages: input.messages } : {}
|
|
373
|
+
};
|
|
374
|
+
let link = decideLinkOrganization(decisionInput);
|
|
375
|
+
if (link.kind === "refused" && link.reason === "not-a-member" && param.kind === "named" && input.admittedByAShare) {
|
|
376
|
+
if (await input.admittedByAShare(param.organizationId)) {
|
|
377
|
+
link = decideLinkOrganization({ ...decisionInput, admittedByAShare: true });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (link.kind === "honoured") {
|
|
381
|
+
const organization = input.memberships.find((m) => m.id === link.organizationId);
|
|
382
|
+
if (organization) return { organization, rung: "link", link };
|
|
383
|
+
}
|
|
384
|
+
return { organization: ladder.organization, rung: ladder.rung, link };
|
|
385
|
+
}
|
|
386
|
+
|
|
184
387
|
// src/active-organization.ts
|
|
185
388
|
var NoActiveOrganizationError = class extends Error {
|
|
186
389
|
constructor(reason, detail) {
|
|
@@ -258,6 +461,7 @@ function createActiveOrganization(options) {
|
|
|
258
461
|
const publish = (next) => {
|
|
259
462
|
current = Object.freeze(next);
|
|
260
463
|
if (current.status !== "resolving" && current.status !== "idle") {
|
|
464
|
+
options.bootstrapGate?.markResolved();
|
|
261
465
|
const ready = waiters;
|
|
262
466
|
waiters = [];
|
|
263
467
|
for (const w of ready) w();
|
|
@@ -275,19 +479,38 @@ function createActiveOrganization(options) {
|
|
|
275
479
|
}
|
|
276
480
|
const memberships = await readMemberships(client, person.id);
|
|
277
481
|
if (gen !== generation) return current;
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
482
|
+
const linkOptions = options.link;
|
|
483
|
+
let resolved;
|
|
484
|
+
let linkDecision = null;
|
|
485
|
+
if (linkOptions) {
|
|
486
|
+
const r = await resolveOrganizationWithLink({
|
|
487
|
+
memberships,
|
|
488
|
+
held: await held.read(person.id),
|
|
489
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id),
|
|
490
|
+
link: linkOptions.source() ?? null,
|
|
491
|
+
switchWhenALinkAsks: linkOptions.switchWhenALinkAsks?.(),
|
|
492
|
+
signedInAs: linkOptions.signedInAs?.(),
|
|
493
|
+
messages: linkOptions.messages,
|
|
494
|
+
admittedByAShare: linkOptions.admittedByAShare
|
|
495
|
+
});
|
|
496
|
+
resolved = r;
|
|
497
|
+
linkDecision = r?.link ?? null;
|
|
498
|
+
} else {
|
|
499
|
+
const linked = options.linked?.() ?? null;
|
|
500
|
+
const heldId = (linked && memberships.some((m) => m.id === linked) ? linked : null) ?? await held.read(person.id);
|
|
501
|
+
resolved = await resolveActiveOrganizationLazily({
|
|
502
|
+
memberships,
|
|
503
|
+
held: heldId,
|
|
504
|
+
readAccount: () => readAccountOrganizationChoices(client, person.id)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
285
507
|
if (gen !== generation) return current;
|
|
286
508
|
if (!resolved) {
|
|
287
509
|
publish({ ...IDLE, status: "none", userId: person.id, error: "no-memberships" });
|
|
288
510
|
return current;
|
|
289
511
|
}
|
|
290
512
|
await held.write(person.id, resolved.organization.id);
|
|
513
|
+
if (linkDecision && linkDecision.kind !== "no-link") options.link?.onDecision?.(linkDecision);
|
|
291
514
|
publish({
|
|
292
515
|
status: "ready",
|
|
293
516
|
userId: person.id,
|
|
@@ -323,6 +546,11 @@ function createActiveOrganization(options) {
|
|
|
323
546
|
const userId = session.user?.id ?? null;
|
|
324
547
|
if (event === "SIGNED_IN" && userId !== current.userId) void load();
|
|
325
548
|
});
|
|
549
|
+
const seatFor = (snap) => {
|
|
550
|
+
const seat = resolveAdminSeat(options.adminSeat?.());
|
|
551
|
+
return seat && snap.memberships.some((m) => m.id === seat.id) ? seat.id : null;
|
|
552
|
+
};
|
|
553
|
+
let lastNotAMemberRecheck = Number.NEGATIVE_INFINITY;
|
|
326
554
|
const settled = async () => {
|
|
327
555
|
if (current.status === "idle") return load();
|
|
328
556
|
if (current.status === "resolving") {
|
|
@@ -339,11 +567,11 @@ function createActiveOrganization(options) {
|
|
|
339
567
|
};
|
|
340
568
|
},
|
|
341
569
|
load,
|
|
342
|
-
current: () => current.status === "ready" ? current.organizationId : null,
|
|
570
|
+
current: () => current.status === "ready" ? seatFor(current) ?? current.organizationId : null,
|
|
343
571
|
async ensureOrgId(recordOrganizationId) {
|
|
344
572
|
if (recordOrganizationId) return recordOrganizationId;
|
|
345
573
|
const snap = await settled();
|
|
346
|
-
if (snap.status === "ready" && snap.organizationId) return snap.organizationId;
|
|
574
|
+
if (snap.status === "ready" && snap.organizationId) return seatFor(snap) ?? snap.organizationId;
|
|
347
575
|
if (snap.status === "error") throw new NoActiveOrganizationError("unreadable", snap.error ?? void 0);
|
|
348
576
|
throw new NoActiveOrganizationError(snap.error === "signed-out" ? "signed-out" : "no-memberships");
|
|
349
577
|
},
|
|
@@ -358,7 +586,16 @@ function createActiveOrganization(options) {
|
|
|
358
586
|
await writeLastActiveOrganization(client, organizationId);
|
|
359
587
|
},
|
|
360
588
|
async handleRefusal(status, body) {
|
|
361
|
-
|
|
589
|
+
let kind = classifyOrganizationRefusal(status, body);
|
|
590
|
+
if (kind === null && isNotAMemberRefusal(body)) {
|
|
591
|
+
const now = Date.now();
|
|
592
|
+
if (now - lastNotAMemberRecheck >= (options.refusalRecheckGapMs ?? NOT_A_MEMBER_RECHECK_GAP_MS)) {
|
|
593
|
+
lastNotAMemberRecheck = now;
|
|
594
|
+
kind = "lapsed";
|
|
595
|
+
} else {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
362
599
|
if (kind === "lapsed") {
|
|
363
600
|
if (current.userId) await held.clear();
|
|
364
601
|
await load();
|
|
@@ -378,21 +615,132 @@ function createActiveOrganization(options) {
|
|
|
378
615
|
if (options.autoLoad ?? true) void load();
|
|
379
616
|
return api;
|
|
380
617
|
}
|
|
618
|
+
|
|
619
|
+
// src/organization-required.ts
|
|
620
|
+
var ORGANIZATION_REQUIRED_CODE = "organization_required";
|
|
621
|
+
var ORGANIZATION_REQUIRED_WIRE_CODE = ORGANIZATION_REQUIRED_CODE;
|
|
622
|
+
var ORGANIZATION_CONTEXT_REQUIRED_CODE = "organization_context_required";
|
|
623
|
+
var MEMBERSHIPS_URL = "/auth/organizations";
|
|
624
|
+
function isOrganizationRequiredError(err) {
|
|
625
|
+
if (!(err instanceof Error)) return false;
|
|
626
|
+
const code = err.code;
|
|
627
|
+
return code === ORGANIZATION_CONTEXT_REQUIRED_CODE || code === ORGANIZATION_REQUIRED_CODE;
|
|
628
|
+
}
|
|
629
|
+
function isOrganizationRequiredEnvelope(body) {
|
|
630
|
+
return typeof body === "object" && body !== null && body.code === ORGANIZATION_REQUIRED_CODE;
|
|
631
|
+
}
|
|
632
|
+
function extractOrganizationHoldMemberships(err) {
|
|
633
|
+
if (!err || typeof err !== "object") return null;
|
|
634
|
+
if (err.code !== ORGANIZATION_REQUIRED_CODE) return null;
|
|
635
|
+
const container = err;
|
|
636
|
+
return readOrganizationsField(container.details) ?? readOrganizationsField(container.serverDetail);
|
|
637
|
+
}
|
|
638
|
+
function readOrganizationsField(details) {
|
|
639
|
+
if (!details || typeof details !== "object") return null;
|
|
640
|
+
const organizations = details.organizations;
|
|
641
|
+
if (!Array.isArray(organizations)) return null;
|
|
642
|
+
return organizations.filter(
|
|
643
|
+
(org) => !!org && typeof org === "object" && typeof org.id === "string" && typeof org.name === "string"
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
var OrganizationRequiredServerError = class extends Error {
|
|
647
|
+
name = "OrganizationRequiredServerError";
|
|
648
|
+
code = ORGANIZATION_REQUIRED_CODE;
|
|
649
|
+
/** `null` only when the list could not be read ("could not ask"), distinct from `[]`. */
|
|
650
|
+
memberships;
|
|
651
|
+
constructor(message, memberships = null) {
|
|
652
|
+
super(message);
|
|
653
|
+
this.memberships = memberships;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
function isOrganizationRequiredServerError(error) {
|
|
657
|
+
return error instanceof OrganizationRequiredServerError || error instanceof Error && error.name === "OrganizationRequiredServerError";
|
|
658
|
+
}
|
|
659
|
+
async function readCallerMemberships(client) {
|
|
660
|
+
try {
|
|
661
|
+
const { data: claims } = await client.auth.getClaims();
|
|
662
|
+
const userId = claims?.claims?.sub;
|
|
663
|
+
if (typeof userId !== "string" || !userId) return null;
|
|
664
|
+
const query = client.schema("iam").from("memberships").select("organization_id, organizations:organization_id(id,name,abbreviation)");
|
|
665
|
+
const { data, error } = await query.eq("user_id", userId).eq("container_type", "organization").eq("status", "active").is("deleted_at", null);
|
|
666
|
+
if (error || !Array.isArray(data)) return null;
|
|
667
|
+
const rows = data;
|
|
668
|
+
return rows.map((row) => ({
|
|
669
|
+
id: row.organizations?.id ?? row.organization_id,
|
|
670
|
+
name: row.organizations?.name ?? "Untitled organization",
|
|
671
|
+
...row.organizations?.abbreviation ? { abbreviation: row.organizations.abbreviation } : {}
|
|
672
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function organizationRequired(client, message) {
|
|
678
|
+
throw new OrganizationRequiredServerError(message, await readCallerMemberships(client));
|
|
679
|
+
}
|
|
680
|
+
function organizationRequiredEnvelope(error, setOn = "request") {
|
|
681
|
+
return {
|
|
682
|
+
error: ORGANIZATION_REQUIRED_CODE,
|
|
683
|
+
code: ORGANIZATION_REQUIRED_CODE,
|
|
684
|
+
message: error.message,
|
|
685
|
+
user_message: "Choose the organization you're working in, then try again.",
|
|
686
|
+
details: {
|
|
687
|
+
hold: ORGANIZATION_REQUIRED_CODE,
|
|
688
|
+
can_choose: true,
|
|
689
|
+
set_on: setOn,
|
|
690
|
+
remedy: "Choose the organization you're working in and send it with the request (the X-Organization-Id header), then try again.",
|
|
691
|
+
organizations: error.memberships,
|
|
692
|
+
memberships_url: MEMBERSHIPS_URL
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function organizationRequiredResponse(error) {
|
|
697
|
+
return Response.json(organizationRequiredEnvelope(error), { status: 400 });
|
|
698
|
+
}
|
|
381
699
|
export {
|
|
700
|
+
LINK_ORGANIZATION_QUERY_KEY,
|
|
701
|
+
MEMBERSHIPS_URL,
|
|
702
|
+
NOT_A_MEMBER_RECHECK_GAP_MS,
|
|
382
703
|
NoActiveOrganizationError,
|
|
383
704
|
NotAMemberError,
|
|
705
|
+
ORGANIZATION_CONTEXT_REQUIRED_CODE,
|
|
384
706
|
ORGANIZATION_ENTITY_TYPE,
|
|
707
|
+
ORGANIZATION_REQUIRED_CODE,
|
|
708
|
+
ORGANIZATION_REQUIRED_WIRE_CODE,
|
|
709
|
+
ORGANIZATION_RESOLUTION_TIMEOUT_MS,
|
|
710
|
+
OrganizationRequiredServerError,
|
|
711
|
+
adminSeatReplacesUserContext,
|
|
712
|
+
classifyLinkOrganizationValue,
|
|
385
713
|
classifyOrganizationRefusal,
|
|
386
714
|
createActiveOrganization,
|
|
715
|
+
createNotAMemberRefusalNotice,
|
|
716
|
+
createOrganizationBootstrapGate,
|
|
717
|
+
decideLinkOrganization,
|
|
718
|
+
extractOrganizationHoldMemberships,
|
|
719
|
+
isNotAMemberRefusal,
|
|
720
|
+
isOrganizationRequiredEnvelope,
|
|
721
|
+
isOrganizationRequiredError,
|
|
722
|
+
isOrganizationRequiredServerError,
|
|
723
|
+
linkOrganizationMalformedMessage,
|
|
724
|
+
linkOrganizationNotAMemberMessage,
|
|
725
|
+
linkOrganizationOfferMessage,
|
|
726
|
+
linkOrganizationSwitchAnnouncement,
|
|
387
727
|
memoryHeldOrganization,
|
|
388
728
|
oldestMembership,
|
|
389
729
|
orderOrganizations,
|
|
390
730
|
organizationMatches,
|
|
731
|
+
organizationRequired,
|
|
732
|
+
organizationRequiredEnvelope,
|
|
733
|
+
organizationRequiredResponse,
|
|
391
734
|
readAccountOrganizationChoices,
|
|
735
|
+
readCallerMemberships,
|
|
736
|
+
readLinkOrganizationParam,
|
|
392
737
|
readMemberships,
|
|
393
738
|
readOrganizationFavourites,
|
|
394
739
|
resolveActiveOrganization,
|
|
395
740
|
resolveActiveOrganizationLazily,
|
|
741
|
+
resolveAdminSeat,
|
|
742
|
+
resolveChildOrganizationId,
|
|
743
|
+
resolveOrganizationWithLink,
|
|
396
744
|
setOrganizationFavourite,
|
|
397
745
|
storageHeldOrganization,
|
|
398
746
|
toAccountOrganizationChoices,
|
package/dist/react.d.cts
CHANGED
|
@@ -96,7 +96,7 @@ interface ActiveOrganizationSnapshot {
|
|
|
96
96
|
readonly organization: OrganizationMembership | null;
|
|
97
97
|
readonly memberships: readonly OrganizationMembership[];
|
|
98
98
|
/** Which ladder rung answered (or "switched" after a person's click). */
|
|
99
|
-
readonly rung: OrganizationRung | "switched" | null;
|
|
99
|
+
readonly rung: OrganizationRung | "switched" | "link" | null;
|
|
100
100
|
readonly error: string | null;
|
|
101
101
|
}
|
|
102
102
|
interface ActiveOrganization {
|
package/dist/react.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ interface ActiveOrganizationSnapshot {
|
|
|
96
96
|
readonly organization: OrganizationMembership | null;
|
|
97
97
|
readonly memberships: readonly OrganizationMembership[];
|
|
98
98
|
/** Which ladder rung answered (or "switched" after a person's click). */
|
|
99
|
-
readonly rung: OrganizationRung | "switched" | null;
|
|
99
|
+
readonly rung: OrganizationRung | "switched" | "link" | null;
|
|
100
100
|
readonly error: string | null;
|
|
101
101
|
}
|
|
102
102
|
interface ActiveOrganization {
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
declare class StorageUnavailableError extends Error {
|
|
24
|
+
readonly storage: string;
|
|
25
|
+
constructor(storage: string, reason: string);
|
|
26
|
+
}
|
|
27
|
+
/** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
|
|
28
|
+
declare function memoryStorage(): SessionStorage & {
|
|
29
|
+
readonly size: () => number;
|
|
30
|
+
};
|
|
31
|
+
/** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
|
|
32
|
+
declare function browserStorage(kind?: "local" | "session"): SessionStorage;
|
|
33
|
+
/** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
|
|
34
|
+
interface ChromeStorageAreaLike {
|
|
35
|
+
get(keys: string): Promise<Record<string, unknown>>;
|
|
36
|
+
set(items: Record<string, unknown>): Promise<void>;
|
|
37
|
+
remove(keys: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
|
|
41
|
+
* panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
|
|
42
|
+
*/
|
|
43
|
+
declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
|
|
44
|
+
/** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
|
|
45
|
+
declare function keyValueStorage(store: {
|
|
46
|
+
get(key: string): Promise<string | null | undefined>;
|
|
47
|
+
set(key: string, value: string): Promise<void>;
|
|
48
|
+
remove(key: string): Promise<void>;
|
|
49
|
+
}): SessionStorage;
|
|
50
|
+
|
|
51
|
+
export { type ChromeStorageAreaLike as C, type SessionStorage as S, StorageUnavailableError as a, browserStorage as b, extensionStorage as e, keyValueStorage as k, memoryStorage as m };
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
declare class StorageUnavailableError extends Error {
|
|
24
|
+
readonly storage: string;
|
|
25
|
+
constructor(storage: string, reason: string);
|
|
26
|
+
}
|
|
27
|
+
/** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
|
|
28
|
+
declare function memoryStorage(): SessionStorage & {
|
|
29
|
+
readonly size: () => number;
|
|
30
|
+
};
|
|
31
|
+
/** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
|
|
32
|
+
declare function browserStorage(kind?: "local" | "session"): SessionStorage;
|
|
33
|
+
/** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
|
|
34
|
+
interface ChromeStorageAreaLike {
|
|
35
|
+
get(keys: string): Promise<Record<string, unknown>>;
|
|
36
|
+
set(items: Record<string, unknown>): Promise<void>;
|
|
37
|
+
remove(keys: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
|
|
41
|
+
* panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
|
|
42
|
+
*/
|
|
43
|
+
declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
|
|
44
|
+
/** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
|
|
45
|
+
declare function keyValueStorage(store: {
|
|
46
|
+
get(key: string): Promise<string | null | undefined>;
|
|
47
|
+
set(key: string, value: string): Promise<void>;
|
|
48
|
+
remove(key: string): Promise<void>;
|
|
49
|
+
}): SessionStorage;
|
|
50
|
+
|
|
51
|
+
export { type ChromeStorageAreaLike as C, type SessionStorage as S, StorageUnavailableError as a, browserStorage as b, extensionStorage as e, keyValueStorage as k, memoryStorage as m };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/auth",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "AI Matrx sign-in, session and organizations for every app: sign in/out, session storage and refresh per platform (browser, Next.js, extension, desktop, Node), the current person, their organizations and favourites, the one active organization (held requests, never a list filter), and the credentials @ai-matrx/data and @ai-matrx/api read.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -122,6 +122,16 @@
|
|
|
122
122
|
"default": "./dist/testing.cjs"
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
|
+
"./matrx-oauth": {
|
|
126
|
+
"import": {
|
|
127
|
+
"types": "./dist/matrx-oauth.d.ts",
|
|
128
|
+
"default": "./dist/matrx-oauth.js"
|
|
129
|
+
},
|
|
130
|
+
"require": {
|
|
131
|
+
"types": "./dist/matrx-oauth.d.cts",
|
|
132
|
+
"default": "./dist/matrx-oauth.cjs"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
125
135
|
"./package.json": "./package.json"
|
|
126
136
|
},
|
|
127
137
|
"devDependencies": {
|
|
@@ -191,5 +201,5 @@
|
|
|
191
201
|
"test": "vitest run",
|
|
192
202
|
"verify:tarball": "node ./scripts/verify-tarball.mjs"
|
|
193
203
|
},
|
|
194
|
-
"gitHead": "
|
|
204
|
+
"gitHead": "51416dab018cb60548d2baeed471c4b27a0cb44b"
|
|
195
205
|
}
|