@ai-matrx/auth 0.1.3 → 0.2.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.
@@ -1,5 +1,5 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
- import { A as AuthClient } from './session-DhRO4euL.cjs';
2
+ import { A as AuthClient } from './session-DdQiStp6.cjs';
3
3
 
4
4
  /**
5
5
  * @ai-matrx/auth/organizations (moved from @ai-matrx/data/organizations, 2026-10-09) — THE active-organization ladder and the account's organization
@@ -223,4 +223,110 @@ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "set
223
223
  declare function memoryHeldOrganization(): HeldOrganizationStore;
224
224
  declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
225
225
 
226
- export { type AccountOrganizationChoices, type ActiveOrganization, type ActiveOrganizationSnapshot, type ActiveOrganizationStatus, type CreateActiveOrganizationOptions, type HeldOrganizationStore, type LadderInput, NoActiveOrganizationError, NotAMemberError, ORGANIZATION_ENTITY_TYPE, type OrganizationMembership, type OrganizationRefusal, type OrganizationRung, type OrganizationsDb, type ResolvedOrganization, classifyOrganizationRefusal, createActiveOrganization, memoryHeldOrganization, oldestMembership, orderOrganizations, organizationMatches, readAccountOrganizationChoices, readMemberships, readOrganizationFavourites, resolveActiveOrganization, resolveActiveOrganizationLazily, setOrganizationFavourite, storageHeldOrganization, toAccountOrganizationChoices, writeLastActiveOrganization, writeStartupOrganization };
226
+ /**
227
+ * "This request needs an organization and names none" — the ONE recogniser, the server's throwable
228
+ * and the wire envelope (moved from the website's `lib/organizations/organizationRequired*.ts`,
229
+ * 2026-10-12). The envelope is byte-for-byte aidream's `matrx_connect.org_hold.organization_hold_detail`
230
+ * (`details.organizations`, status 400), so one recogniser covers both servers.
231
+ *
232
+ * Ruling (Arman, 2026-09-19): nothing but the picker may choose an organization. A request that
233
+ * needs one and has none is HELD: the person is shown their memberships, sets one, and it proceeds.
234
+ */
235
+ /** The wire code both servers answer a missing organization with. */
236
+ declare const ORGANIZATION_REQUIRED_CODE: "organization_required";
237
+ /** Same value, the name the website's callers already read. */
238
+ declare const ORGANIZATION_REQUIRED_WIRE_CODE: "organization_required";
239
+ /** The transport kernel's client-side code for the same refusal (`OrganizationContextError`). */
240
+ declare const ORGANIZATION_CONTEXT_REQUIRED_CODE: "organization_context_required";
241
+ /** aidream's `MEMBERSHIPS_URL` literal — informational parity with the canonical envelope. */
242
+ declare const MEMBERSHIPS_URL: "/auth/organizations";
243
+ /** One organization the refusal offers the person. */
244
+ interface OrganizationMembershipSummary {
245
+ id: string;
246
+ name: string;
247
+ abbreviation?: string;
248
+ }
249
+ type OrganizationRequiredWireMembership = OrganizationMembershipSummary;
250
+ type OrganizationHoldSetOn = "request" | "mcp_connection" | "coding_session";
251
+ /**
252
+ * True when `err` is the fail-closed "no organization selected" refusal — from a transport kernel
253
+ * (`code: "organization_context_required"`), a route handler or either server
254
+ * (`code: "organization_required"`). Matched by CODE, never by message, so it survives a
255
+ * serialization boundary and copy changes.
256
+ */
257
+ declare function isOrganizationRequiredError(err: unknown): boolean;
258
+ /** The parsed 400 body of an organization refusal (a plain `fetch` rather than a throwing transport). */
259
+ declare function isOrganizationRequiredEnvelope(body: unknown): body is {
260
+ code: typeof ORGANIZATION_REQUIRED_CODE;
261
+ message?: string;
262
+ user_message?: string;
263
+ details?: {
264
+ organizations?: OrganizationMembershipSummary[] | null;
265
+ memberships_url?: string;
266
+ set_on?: string;
267
+ remedy?: string;
268
+ };
269
+ };
270
+ /**
271
+ * What organizations does this refusal offer? Reads `.details.organizations` (either server's
272
+ * envelope or a `BackendApiError`) or `.serverDetail.organizations` (a normalized `ApiCallError`).
273
+ * `null` = absent or unreadable ("could not ask" — fall back to the picker's list); `[]` is the
274
+ * honest "you belong to nothing".
275
+ */
276
+ declare function extractOrganizationHoldMemberships(err: unknown): OrganizationMembershipSummary[] | null;
277
+ /** The refusal, as a throwable. Nothing recovers from it by choosing — choosing is the person's act. */
278
+ declare class OrganizationRequiredServerError extends Error {
279
+ name: "OrganizationRequiredServerError";
280
+ readonly code: "organization_required";
281
+ /** `null` only when the list could not be read ("could not ask"), distinct from `[]`. */
282
+ readonly memberships: OrganizationMembershipSummary[] | null;
283
+ constructor(message: string, memberships?: OrganizationMembershipSummary[] | null);
284
+ }
285
+ declare function isOrganizationRequiredServerError(error: unknown): error is OrganizationRequiredServerError;
286
+ /** The minimal client shape `readCallerMemberships` needs (any Supabase client). */
287
+ interface MembershipReadClient {
288
+ auth: {
289
+ getClaims(): Promise<{
290
+ data: {
291
+ claims?: {
292
+ sub?: unknown;
293
+ };
294
+ } | null;
295
+ error: unknown;
296
+ }>;
297
+ };
298
+ schema(name: string): {
299
+ from(table: string): {
300
+ select(columns: string): unknown;
301
+ };
302
+ };
303
+ }
304
+ /**
305
+ * The caller's own active organization memberships — the only ones a refusal may offer (reads
306
+ * `iam.memberships`, never every organization row security returns: for a platform admin that is
307
+ * every tenant). `null` on any failure, so "could not ask" is never "you belong to nothing".
308
+ */
309
+ declare function readCallerMemberships(client: MembershipReadClient): Promise<OrganizationMembershipSummary[] | null>;
310
+ /** Refuse, with the choices attached. */
311
+ declare function organizationRequired(client: MembershipReadClient, message: string): Promise<never>;
312
+ interface OrganizationHoldDetails {
313
+ hold: typeof ORGANIZATION_REQUIRED_CODE;
314
+ can_choose: true;
315
+ set_on: OrganizationHoldSetOn;
316
+ remedy: string;
317
+ organizations: OrganizationMembershipSummary[] | null;
318
+ memberships_url: string;
319
+ }
320
+ interface OrganizationRequiredEnvelope {
321
+ error: typeof ORGANIZATION_REQUIRED_CODE;
322
+ code: typeof ORGANIZATION_REQUIRED_CODE;
323
+ message: string;
324
+ user_message: string;
325
+ details: OrganizationHoldDetails;
326
+ }
327
+ /** The wire body (status 400) — aidream's `organization_hold_detail`, field for field. */
328
+ declare function organizationRequiredEnvelope(error: OrganizationRequiredServerError, setOn?: OrganizationHoldSetOn): OrganizationRequiredEnvelope;
329
+ /** The standard `Response` (status 400) a route answers the refusal with — no framework needed. */
330
+ declare function organizationRequiredResponse(error: OrganizationRequiredServerError): Response;
331
+
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 };
@@ -1,5 +1,5 @@
1
1
  import { SupabaseClient } from '@supabase/supabase-js';
2
- import { A as AuthClient } from './session-DhRO4euL.js';
2
+ import { A as AuthClient } from './session-DdQiStp6.js';
3
3
 
4
4
  /**
5
5
  * @ai-matrx/auth/organizations (moved from @ai-matrx/data/organizations, 2026-10-09) — THE active-organization ladder and the account's organization
@@ -223,4 +223,110 @@ declare function storageHeldOrganization(storage: Pick<Storage, "getItem" | "set
223
223
  declare function memoryHeldOrganization(): HeldOrganizationStore;
224
224
  declare function createActiveOrganization(options: CreateActiveOrganizationOptions): ActiveOrganization;
225
225
 
226
- export { type AccountOrganizationChoices, type ActiveOrganization, type ActiveOrganizationSnapshot, type ActiveOrganizationStatus, type CreateActiveOrganizationOptions, type HeldOrganizationStore, type LadderInput, NoActiveOrganizationError, NotAMemberError, ORGANIZATION_ENTITY_TYPE, type OrganizationMembership, type OrganizationRefusal, type OrganizationRung, type OrganizationsDb, type ResolvedOrganization, classifyOrganizationRefusal, createActiveOrganization, memoryHeldOrganization, oldestMembership, orderOrganizations, organizationMatches, readAccountOrganizationChoices, readMemberships, readOrganizationFavourites, resolveActiveOrganization, resolveActiveOrganizationLazily, setOrganizationFavourite, storageHeldOrganization, toAccountOrganizationChoices, writeLastActiveOrganization, writeStartupOrganization };
226
+ /**
227
+ * "This request needs an organization and names none" — the ONE recogniser, the server's throwable
228
+ * and the wire envelope (moved from the website's `lib/organizations/organizationRequired*.ts`,
229
+ * 2026-10-12). The envelope is byte-for-byte aidream's `matrx_connect.org_hold.organization_hold_detail`
230
+ * (`details.organizations`, status 400), so one recogniser covers both servers.
231
+ *
232
+ * Ruling (Arman, 2026-09-19): nothing but the picker may choose an organization. A request that
233
+ * needs one and has none is HELD: the person is shown their memberships, sets one, and it proceeds.
234
+ */
235
+ /** The wire code both servers answer a missing organization with. */
236
+ declare const ORGANIZATION_REQUIRED_CODE: "organization_required";
237
+ /** Same value, the name the website's callers already read. */
238
+ declare const ORGANIZATION_REQUIRED_WIRE_CODE: "organization_required";
239
+ /** The transport kernel's client-side code for the same refusal (`OrganizationContextError`). */
240
+ declare const ORGANIZATION_CONTEXT_REQUIRED_CODE: "organization_context_required";
241
+ /** aidream's `MEMBERSHIPS_URL` literal — informational parity with the canonical envelope. */
242
+ declare const MEMBERSHIPS_URL: "/auth/organizations";
243
+ /** One organization the refusal offers the person. */
244
+ interface OrganizationMembershipSummary {
245
+ id: string;
246
+ name: string;
247
+ abbreviation?: string;
248
+ }
249
+ type OrganizationRequiredWireMembership = OrganizationMembershipSummary;
250
+ type OrganizationHoldSetOn = "request" | "mcp_connection" | "coding_session";
251
+ /**
252
+ * True when `err` is the fail-closed "no organization selected" refusal — from a transport kernel
253
+ * (`code: "organization_context_required"`), a route handler or either server
254
+ * (`code: "organization_required"`). Matched by CODE, never by message, so it survives a
255
+ * serialization boundary and copy changes.
256
+ */
257
+ declare function isOrganizationRequiredError(err: unknown): boolean;
258
+ /** The parsed 400 body of an organization refusal (a plain `fetch` rather than a throwing transport). */
259
+ declare function isOrganizationRequiredEnvelope(body: unknown): body is {
260
+ code: typeof ORGANIZATION_REQUIRED_CODE;
261
+ message?: string;
262
+ user_message?: string;
263
+ details?: {
264
+ organizations?: OrganizationMembershipSummary[] | null;
265
+ memberships_url?: string;
266
+ set_on?: string;
267
+ remedy?: string;
268
+ };
269
+ };
270
+ /**
271
+ * What organizations does this refusal offer? Reads `.details.organizations` (either server's
272
+ * envelope or a `BackendApiError`) or `.serverDetail.organizations` (a normalized `ApiCallError`).
273
+ * `null` = absent or unreadable ("could not ask" — fall back to the picker's list); `[]` is the
274
+ * honest "you belong to nothing".
275
+ */
276
+ declare function extractOrganizationHoldMemberships(err: unknown): OrganizationMembershipSummary[] | null;
277
+ /** The refusal, as a throwable. Nothing recovers from it by choosing — choosing is the person's act. */
278
+ declare class OrganizationRequiredServerError extends Error {
279
+ name: "OrganizationRequiredServerError";
280
+ readonly code: "organization_required";
281
+ /** `null` only when the list could not be read ("could not ask"), distinct from `[]`. */
282
+ readonly memberships: OrganizationMembershipSummary[] | null;
283
+ constructor(message: string, memberships?: OrganizationMembershipSummary[] | null);
284
+ }
285
+ declare function isOrganizationRequiredServerError(error: unknown): error is OrganizationRequiredServerError;
286
+ /** The minimal client shape `readCallerMemberships` needs (any Supabase client). */
287
+ interface MembershipReadClient {
288
+ auth: {
289
+ getClaims(): Promise<{
290
+ data: {
291
+ claims?: {
292
+ sub?: unknown;
293
+ };
294
+ } | null;
295
+ error: unknown;
296
+ }>;
297
+ };
298
+ schema(name: string): {
299
+ from(table: string): {
300
+ select(columns: string): unknown;
301
+ };
302
+ };
303
+ }
304
+ /**
305
+ * The caller's own active organization memberships — the only ones a refusal may offer (reads
306
+ * `iam.memberships`, never every organization row security returns: for a platform admin that is
307
+ * every tenant). `null` on any failure, so "could not ask" is never "you belong to nothing".
308
+ */
309
+ declare function readCallerMemberships(client: MembershipReadClient): Promise<OrganizationMembershipSummary[] | null>;
310
+ /** Refuse, with the choices attached. */
311
+ declare function organizationRequired(client: MembershipReadClient, message: string): Promise<never>;
312
+ interface OrganizationHoldDetails {
313
+ hold: typeof ORGANIZATION_REQUIRED_CODE;
314
+ can_choose: true;
315
+ set_on: OrganizationHoldSetOn;
316
+ remedy: string;
317
+ organizations: OrganizationMembershipSummary[] | null;
318
+ memberships_url: string;
319
+ }
320
+ interface OrganizationRequiredEnvelope {
321
+ error: typeof ORGANIZATION_REQUIRED_CODE;
322
+ code: typeof ORGANIZATION_REQUIRED_CODE;
323
+ message: string;
324
+ user_message: string;
325
+ details: OrganizationHoldDetails;
326
+ }
327
+ /** The wire body (status 400) — aidream's `organization_hold_detail`, field for field. */
328
+ declare function organizationRequiredEnvelope(error: OrganizationRequiredServerError, setOn?: OrganizationHoldSetOn): OrganizationRequiredEnvelope;
329
+ /** The standard `Response` (status 400) a route answers the refusal with — no framework needed. */
330
+ declare function organizationRequiredResponse(error: OrganizationRequiredServerError): Response;
331
+
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 };
@@ -138,10 +138,43 @@ function check(door, data, error) {
138
138
  if (error) throw new SignInError(door, error.message, error.status, error.code);
139
139
  return data;
140
140
  }
141
+ var personCache = /* @__PURE__ */ new WeakMap();
142
+ function personEntry(client) {
143
+ let entry = personCache.get(client);
144
+ if (!entry) {
145
+ const fresh = { read: null, epoch: 0 };
146
+ entry = fresh;
147
+ personCache.set(client, fresh);
148
+ const subscribe = client.auth?.onAuthStateChange;
149
+ if (typeof subscribe === "function") {
150
+ subscribe.call(client.auth, async (event) => {
151
+ if (event === "SIGNED_IN" || event === "SIGNED_OUT" || event === "USER_UPDATED") invalidatePerson(client);
152
+ });
153
+ }
154
+ }
155
+ return entry;
156
+ }
157
+ function invalidatePerson(client) {
158
+ const entry = personCache.get(client);
159
+ if (!entry) return;
160
+ entry.epoch += 1;
161
+ entry.read = null;
162
+ }
141
163
  async function getPerson(client) {
142
- const { data, error } = await client.auth.getUser();
143
- if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
144
- return check("getPerson", data, error).user;
164
+ const entry = personEntry(client);
165
+ if (entry.read) return entry.read;
166
+ const epoch = entry.epoch;
167
+ const read = (async () => {
168
+ const { data, error } = await client.auth.getUser();
169
+ if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
170
+ return check("getPerson", data, error).user;
171
+ })();
172
+ entry.read = read;
173
+ const settle = (keep) => {
174
+ if (entry.epoch === epoch && entry.read === read && !keep) entry.read = null;
175
+ };
176
+ read.then((user) => settle(user !== null), () => settle(false));
177
+ return read;
145
178
  }
146
179
  function onSessionChange(client, cb) {
147
180
  const { data } = client.auth.onAuthStateChange(cb);
@@ -274,6 +307,7 @@ function createActiveOrganization(options) {
274
307
  const load = () => {
275
308
  if (disposed) return Promise.resolve(current);
276
309
  generation += 1;
310
+ invalidatePerson(client);
277
311
  inflight = run(generation).finally(() => {
278
312
  inflight = null;
279
313
  });
@@ -318,6 +352,7 @@ function createActiveOrganization(options) {
318
352
  const organization = snap.memberships.find((m) => m.id === organizationId);
319
353
  if (!organization || !snap.userId) throw new NotAMemberError(organizationId);
320
354
  generation += 1;
355
+ invalidatePerson(client);
321
356
  await held.write(snap.userId, organizationId);
322
357
  publish({ ...snap, status: "ready", organizationId, organization, rung: "switched", error: null });
323
358
  await writeLastActiveOrganization(client, organizationId);
@@ -343,17 +378,111 @@ function createActiveOrganization(options) {
343
378
  if (options.autoLoad ?? true) void load();
344
379
  return api;
345
380
  }
381
+
382
+ // src/organization-required.ts
383
+ var ORGANIZATION_REQUIRED_CODE = "organization_required";
384
+ var ORGANIZATION_REQUIRED_WIRE_CODE = ORGANIZATION_REQUIRED_CODE;
385
+ var ORGANIZATION_CONTEXT_REQUIRED_CODE = "organization_context_required";
386
+ var MEMBERSHIPS_URL = "/auth/organizations";
387
+ function isOrganizationRequiredError(err) {
388
+ if (!(err instanceof Error)) return false;
389
+ const code = err.code;
390
+ return code === ORGANIZATION_CONTEXT_REQUIRED_CODE || code === ORGANIZATION_REQUIRED_CODE;
391
+ }
392
+ function isOrganizationRequiredEnvelope(body) {
393
+ return typeof body === "object" && body !== null && body.code === ORGANIZATION_REQUIRED_CODE;
394
+ }
395
+ function extractOrganizationHoldMemberships(err) {
396
+ if (!err || typeof err !== "object") return null;
397
+ if (err.code !== ORGANIZATION_REQUIRED_CODE) return null;
398
+ const container = err;
399
+ return readOrganizationsField(container.details) ?? readOrganizationsField(container.serverDetail);
400
+ }
401
+ function readOrganizationsField(details) {
402
+ if (!details || typeof details !== "object") return null;
403
+ const organizations = details.organizations;
404
+ if (!Array.isArray(organizations)) return null;
405
+ return organizations.filter(
406
+ (org) => !!org && typeof org === "object" && typeof org.id === "string" && typeof org.name === "string"
407
+ );
408
+ }
409
+ var OrganizationRequiredServerError = class extends Error {
410
+ name = "OrganizationRequiredServerError";
411
+ code = ORGANIZATION_REQUIRED_CODE;
412
+ /** `null` only when the list could not be read ("could not ask"), distinct from `[]`. */
413
+ memberships;
414
+ constructor(message, memberships = null) {
415
+ super(message);
416
+ this.memberships = memberships;
417
+ }
418
+ };
419
+ function isOrganizationRequiredServerError(error) {
420
+ return error instanceof OrganizationRequiredServerError || error instanceof Error && error.name === "OrganizationRequiredServerError";
421
+ }
422
+ async function readCallerMemberships(client) {
423
+ try {
424
+ const { data: claims } = await client.auth.getClaims();
425
+ const userId = claims?.claims?.sub;
426
+ if (typeof userId !== "string" || !userId) return null;
427
+ const query = client.schema("iam").from("memberships").select("organization_id, organizations:organization_id(id,name,abbreviation)");
428
+ const { data, error } = await query.eq("user_id", userId).eq("container_type", "organization").eq("status", "active").is("deleted_at", null);
429
+ if (error || !Array.isArray(data)) return null;
430
+ const rows = data;
431
+ return rows.map((row) => ({
432
+ id: row.organizations?.id ?? row.organization_id,
433
+ name: row.organizations?.name ?? "Untitled organization",
434
+ ...row.organizations?.abbreviation ? { abbreviation: row.organizations.abbreviation } : {}
435
+ })).sort((a, b) => a.name.localeCompare(b.name));
436
+ } catch {
437
+ return null;
438
+ }
439
+ }
440
+ async function organizationRequired(client, message) {
441
+ throw new OrganizationRequiredServerError(message, await readCallerMemberships(client));
442
+ }
443
+ function organizationRequiredEnvelope(error, setOn = "request") {
444
+ return {
445
+ error: ORGANIZATION_REQUIRED_CODE,
446
+ code: ORGANIZATION_REQUIRED_CODE,
447
+ message: error.message,
448
+ user_message: "Choose the organization you're working in, then try again.",
449
+ details: {
450
+ hold: ORGANIZATION_REQUIRED_CODE,
451
+ can_choose: true,
452
+ set_on: setOn,
453
+ remedy: "Choose the organization you're working in and send it with the request (the X-Organization-Id header), then try again.",
454
+ organizations: error.memberships,
455
+ memberships_url: MEMBERSHIPS_URL
456
+ }
457
+ };
458
+ }
459
+ function organizationRequiredResponse(error) {
460
+ return Response.json(organizationRequiredEnvelope(error), { status: 400 });
461
+ }
346
462
  export {
463
+ MEMBERSHIPS_URL,
347
464
  NoActiveOrganizationError,
348
465
  NotAMemberError,
466
+ ORGANIZATION_CONTEXT_REQUIRED_CODE,
349
467
  ORGANIZATION_ENTITY_TYPE,
468
+ ORGANIZATION_REQUIRED_CODE,
469
+ ORGANIZATION_REQUIRED_WIRE_CODE,
470
+ OrganizationRequiredServerError,
350
471
  classifyOrganizationRefusal,
351
472
  createActiveOrganization,
473
+ extractOrganizationHoldMemberships,
474
+ isOrganizationRequiredEnvelope,
475
+ isOrganizationRequiredError,
476
+ isOrganizationRequiredServerError,
352
477
  memoryHeldOrganization,
353
478
  oldestMembership,
354
479
  orderOrganizations,
355
480
  organizationMatches,
481
+ organizationRequired,
482
+ organizationRequiredEnvelope,
483
+ organizationRequiredResponse,
356
484
  readAccountOrganizationChoices,
485
+ readCallerMemberships,
357
486
  readMemberships,
358
487
  readOrganizationFavourites,
359
488
  resolveActiveOrganization,
@@ -31,7 +31,17 @@ declare class SignInError extends Error {
31
31
  }
32
32
  /** The stored session (no network unless a refresh is due), or null when signed out. */
33
33
  declare function getSession(client: AuthClient): Promise<Session | null>;
34
- /** The signed-in person, verified by the auth server, or null when signed out. */
34
+ /**
35
+ * Forget the cached person for this client, so the next {@link getPerson} asks the auth server.
36
+ * Sign-in, sign-out, a profile edit and an organization switch already call this; call it yourself
37
+ * only after changing the person outside this package.
38
+ */
39
+ declare function invalidatePerson(client: AuthClient): void;
40
+ /**
41
+ * The signed-in person, verified by the auth server, or null when signed out. The verified read is
42
+ * kept per client until the session changes (sign-in, sign-out, profile edit, organization switch),
43
+ * so repeated access checks cost zero extra auth-server calls; concurrent callers share one read.
44
+ */
35
45
  declare function getPerson(client: AuthClient): Promise<User | null>;
36
46
  /** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */
37
47
  declare function onSessionChange(client: AuthClient, cb: (event: AuthChangeEvent, session: Session | null) => void): () => void;
@@ -99,4 +109,4 @@ declare const userCredentials: typeof sessionCredentials;
99
109
  */
100
110
  declare function refreshSession(client: AuthClient): Promise<Session>;
101
111
 
102
- export { type AuthClient as A, type SessionSourceLike as S, type UserCredentialsPort as U, SignInError as a, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, onSessionChange as o, refreshSession as r, sessionCredentials as s, updatePerson as u };
112
+ export { type AuthClient as A, type SessionSourceLike as S, type UserCredentialsPort as U, SignInError as a, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, invalidatePerson as i, onSessionChange as o, refreshSession as r, sessionCredentials as s, updatePerson as u };
@@ -31,7 +31,17 @@ declare class SignInError extends Error {
31
31
  }
32
32
  /** The stored session (no network unless a refresh is due), or null when signed out. */
33
33
  declare function getSession(client: AuthClient): Promise<Session | null>;
34
- /** The signed-in person, verified by the auth server, or null when signed out. */
34
+ /**
35
+ * Forget the cached person for this client, so the next {@link getPerson} asks the auth server.
36
+ * Sign-in, sign-out, a profile edit and an organization switch already call this; call it yourself
37
+ * only after changing the person outside this package.
38
+ */
39
+ declare function invalidatePerson(client: AuthClient): void;
40
+ /**
41
+ * The signed-in person, verified by the auth server, or null when signed out. The verified read is
42
+ * kept per client until the session changes (sign-in, sign-out, profile edit, organization switch),
43
+ * so repeated access checks cost zero extra auth-server calls; concurrent callers share one read.
44
+ */
35
45
  declare function getPerson(client: AuthClient): Promise<User | null>;
36
46
  /** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */
37
47
  declare function onSessionChange(client: AuthClient, cb: (event: AuthChangeEvent, session: Session | null) => void): () => void;
@@ -99,4 +109,4 @@ declare const userCredentials: typeof sessionCredentials;
99
109
  */
100
110
  declare function refreshSession(client: AuthClient): Promise<Session>;
101
111
 
102
- export { type AuthClient as A, type SessionSourceLike as S, type UserCredentialsPort as U, SignInError as a, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, onSessionChange as o, refreshSession as r, sessionCredentials as s, updatePerson as u };
112
+ export { type AuthClient as A, type SessionSourceLike as S, type UserCredentialsPort as U, SignInError as a, getSession as b, signInWithOAuth as c, signInWithPassword as d, signOut as e, userCredentials as f, getPerson as g, invalidatePerson as i, onSessionChange as o, refreshSession as r, sessionCredentials as s, updatePerson as u };
@@ -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",
3
+ "version": "0.2.0",
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": "7ee2d68e156416d62a043d8f5053ff7f0b5792fc"
204
+ "gitHead": "eeeafb1ae105f78c584681829e13dd69b200abc6"
195
205
  }