@ai-matrx/auth 0.1.4 → 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.
package/dist/node.js CHANGED
@@ -1,3 +1,135 @@
1
+ // src/server-auth.ts
2
+ import { createClient } from "@supabase/supabase-js";
3
+
4
+ // src/claims.ts
5
+ import {
6
+ AuthError,
7
+ AuthSessionMissingError
8
+ } from "@supabase/supabase-js";
9
+ async function getClaimsUser(client, jwt, options) {
10
+ const { data, error } = options ? await client.auth.getClaims(jwt, options) : await client.auth.getClaims(jwt);
11
+ if (error) {
12
+ if (isSignedOutError(error)) return { data: { user: null }, error: null };
13
+ return { data: { user: null }, error };
14
+ }
15
+ if (!data?.claims) return { data: { user: null }, error: null };
16
+ const user = userFromClaims(data.claims);
17
+ if (!user) {
18
+ return {
19
+ data: { user: null },
20
+ error: new AuthError("Verified token carries no `sub` claim", 401, "bad_jwt")
21
+ };
22
+ }
23
+ return { data: { user }, error: null };
24
+ }
25
+ var BUDGET_EXHAUSTED_STATUS = 408;
26
+ var BUDGET_EXHAUSTED_CODE = "auth_budget_exhausted";
27
+ function isAuthTransportFailure(error) {
28
+ if (!error || typeof error !== "object") return false;
29
+ const name = String(error.name ?? "");
30
+ if (name === "AuthRetryableFetchError" || name === "TimeoutError" || name === "AbortError") return true;
31
+ const status = error.status;
32
+ if (status === 0 || status === 504 || status === 503) return true;
33
+ return status === BUDGET_EXHAUSTED_STATUS || error.code === BUDGET_EXHAUSTED_CODE;
34
+ }
35
+ function isSignedOutError(error) {
36
+ return error instanceof AuthSessionMissingError || error.name === "AuthSessionMissingError" || error.code === "session_not_found";
37
+ }
38
+ function userFromClaims(claims) {
39
+ if (!claims || typeof claims.sub !== "string") return null;
40
+ return {
41
+ ...claims,
42
+ id: claims.sub,
43
+ app_metadata: claims.app_metadata ?? {},
44
+ user_metadata: claims.user_metadata ?? {}
45
+ };
46
+ }
47
+ function accountFlagsFor(claims) {
48
+ if (!claims) return { isGuest: false, hasAccount: false };
49
+ const isGuest = claims.is_anonymous === true;
50
+ return { isGuest, hasAccount: !isGuest };
51
+ }
52
+ var PROJECT_SIGNING_KEY_AUTHORITIES = [
53
+ "db.matrxserver.com",
54
+ "brsgrqvjdzwihsvnfqkf.supabase.co"
55
+ ];
56
+ var PROJECT_SIGNING_KEYS = [
57
+ {
58
+ alg: "ES256",
59
+ crv: "P-256",
60
+ ext: true,
61
+ key_ops: ["verify"],
62
+ kid: "a00aed0b-202c-483a-91ff-fc74719a3bb2",
63
+ kty: "EC",
64
+ use: "sig",
65
+ x: "guqOsgjJqGVU6g_K0iIXKrzixlkeXKOqkPUQCconp6U",
66
+ y: "39zQtkzlfQuPf_MtIhYmMdxNQpIR2Pkl11AToywaslY"
67
+ }
68
+ ];
69
+ function pinnedSigningKeysFor(supabaseUrl) {
70
+ if (!supabaseUrl) return [];
71
+ let host;
72
+ try {
73
+ host = new URL(supabaseUrl).host.toLowerCase();
74
+ } catch {
75
+ return [];
76
+ }
77
+ return PROJECT_SIGNING_KEY_AUTHORITIES.includes(host) ? PROJECT_SIGNING_KEYS : [];
78
+ }
79
+
80
+ // src/server-auth.ts
81
+ function createServerAuth(options) {
82
+ const warn = options.warn ?? ((m) => console.warn(m));
83
+ const resolveOnce = async () => {
84
+ const client = await options.createClient();
85
+ const {
86
+ data: { user },
87
+ error
88
+ } = await getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.supabaseUrl) } });
89
+ const authUnavailable = !user && error !== null && isAuthTransportFailure(error);
90
+ if (authUnavailable) {
91
+ warn(
92
+ `[getServerAuth] identity could not be verified on this attempt \u2014 rendering without a user, NOT as signed out. Cause: ${error?.name ?? "unknown"}: ${error?.message ?? ""}`
93
+ );
94
+ }
95
+ return { isAuthenticated: !!user, user, authUnavailable, ...accountFlagsFor(user) };
96
+ };
97
+ const door = async () => {
98
+ const first = await resolveOnce();
99
+ if (!first.authUnavailable) return first;
100
+ await new Promise((resolve) => setTimeout(resolve, options.retryAfterMs ?? 250));
101
+ const second = await resolveOnce();
102
+ if (!second.authUnavailable) warn("[getServerAuth] identity verified on the retry \u2014 the first attempt's budget was spent.");
103
+ return second;
104
+ };
105
+ return options.cache ? options.cache(door) : door;
106
+ }
107
+ async function verifyRequest(request, options) {
108
+ const header = request.headers.get("Authorization") ?? request.headers.get("authorization");
109
+ if (header?.startsWith("Bearer ")) {
110
+ const token = header.slice(7);
111
+ const client2 = createClient(options.url, options.publishableKey, {
112
+ global: { headers: { Authorization: `Bearer ${token}` } },
113
+ auth: { persistSession: false, autoRefreshToken: false }
114
+ });
115
+ const { data: data2, error: error2 } = await getClaimsUser(client2, token, {
116
+ jwks: { keys: pinnedSigningKeysFor(options.url) }
117
+ });
118
+ return { user: data2.user, error: error2 };
119
+ }
120
+ if (!options.cookieClient) return { user: null, error: null };
121
+ const client = await options.cookieClient();
122
+ const { data, error } = await getClaimsUser(client, void 0, { jwks: { keys: pinnedSigningKeysFor(options.url) } });
123
+ return { user: data.user, error };
124
+ }
125
+ function createServiceClient(options) {
126
+ const url = options.url?.trim();
127
+ const key = options.secretKey?.trim();
128
+ if (!url) throw new Error("createServiceClient: the Supabase URL is not configured");
129
+ if (!key) throw new Error("createServiceClient: the secret key (SUPABASE_SECRET_KEY) is not configured");
130
+ return createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
131
+ }
132
+
1
133
  // src/node.ts
2
134
  function builtin(name) {
3
135
  const get = globalThis.process?.getBuiltinModule;
@@ -32,5 +164,8 @@ function fileStorage(path) {
32
164
  };
33
165
  }
34
166
  export {
35
- fileStorage
167
+ createServerAuth,
168
+ createServiceClient,
169
+ fileStorage,
170
+ verifyRequest
36
171
  };
@@ -20,16 +20,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/organizations.ts
21
21
  var organizations_exports = {};
22
22
  __export(organizations_exports, {
23
+ MEMBERSHIPS_URL: () => MEMBERSHIPS_URL,
23
24
  NoActiveOrganizationError: () => NoActiveOrganizationError,
24
25
  NotAMemberError: () => NotAMemberError,
26
+ ORGANIZATION_CONTEXT_REQUIRED_CODE: () => ORGANIZATION_CONTEXT_REQUIRED_CODE,
25
27
  ORGANIZATION_ENTITY_TYPE: () => ORGANIZATION_ENTITY_TYPE,
28
+ ORGANIZATION_REQUIRED_CODE: () => ORGANIZATION_REQUIRED_CODE,
29
+ ORGANIZATION_REQUIRED_WIRE_CODE: () => ORGANIZATION_REQUIRED_WIRE_CODE,
30
+ OrganizationRequiredServerError: () => OrganizationRequiredServerError,
26
31
  classifyOrganizationRefusal: () => classifyOrganizationRefusal,
27
32
  createActiveOrganization: () => createActiveOrganization,
33
+ extractOrganizationHoldMemberships: () => extractOrganizationHoldMemberships,
34
+ isOrganizationRequiredEnvelope: () => isOrganizationRequiredEnvelope,
35
+ isOrganizationRequiredError: () => isOrganizationRequiredError,
36
+ isOrganizationRequiredServerError: () => isOrganizationRequiredServerError,
28
37
  memoryHeldOrganization: () => memoryHeldOrganization,
29
38
  oldestMembership: () => oldestMembership,
30
39
  orderOrganizations: () => orderOrganizations,
31
40
  organizationMatches: () => organizationMatches,
41
+ organizationRequired: () => organizationRequired,
42
+ organizationRequiredEnvelope: () => organizationRequiredEnvelope,
43
+ organizationRequiredResponse: () => organizationRequiredResponse,
32
44
  readAccountOrganizationChoices: () => readAccountOrganizationChoices,
45
+ readCallerMemberships: () => readCallerMemberships,
33
46
  readMemberships: () => readMemberships,
34
47
  readOrganizationFavourites: () => readOrganizationFavourites,
35
48
  resolveActiveOrganization: () => resolveActiveOrganization,
@@ -422,3 +435,84 @@ function createActiveOrganization(options) {
422
435
  if (options.autoLoad ?? true) void load();
423
436
  return api;
424
437
  }
438
+
439
+ // src/organization-required.ts
440
+ var ORGANIZATION_REQUIRED_CODE = "organization_required";
441
+ var ORGANIZATION_REQUIRED_WIRE_CODE = ORGANIZATION_REQUIRED_CODE;
442
+ var ORGANIZATION_CONTEXT_REQUIRED_CODE = "organization_context_required";
443
+ var MEMBERSHIPS_URL = "/auth/organizations";
444
+ function isOrganizationRequiredError(err) {
445
+ if (!(err instanceof Error)) return false;
446
+ const code = err.code;
447
+ return code === ORGANIZATION_CONTEXT_REQUIRED_CODE || code === ORGANIZATION_REQUIRED_CODE;
448
+ }
449
+ function isOrganizationRequiredEnvelope(body) {
450
+ return typeof body === "object" && body !== null && body.code === ORGANIZATION_REQUIRED_CODE;
451
+ }
452
+ function extractOrganizationHoldMemberships(err) {
453
+ if (!err || typeof err !== "object") return null;
454
+ if (err.code !== ORGANIZATION_REQUIRED_CODE) return null;
455
+ const container = err;
456
+ return readOrganizationsField(container.details) ?? readOrganizationsField(container.serverDetail);
457
+ }
458
+ function readOrganizationsField(details) {
459
+ if (!details || typeof details !== "object") return null;
460
+ const organizations = details.organizations;
461
+ if (!Array.isArray(organizations)) return null;
462
+ return organizations.filter(
463
+ (org) => !!org && typeof org === "object" && typeof org.id === "string" && typeof org.name === "string"
464
+ );
465
+ }
466
+ var OrganizationRequiredServerError = class extends Error {
467
+ name = "OrganizationRequiredServerError";
468
+ code = ORGANIZATION_REQUIRED_CODE;
469
+ /** `null` only when the list could not be read ("could not ask"), distinct from `[]`. */
470
+ memberships;
471
+ constructor(message, memberships = null) {
472
+ super(message);
473
+ this.memberships = memberships;
474
+ }
475
+ };
476
+ function isOrganizationRequiredServerError(error) {
477
+ return error instanceof OrganizationRequiredServerError || error instanceof Error && error.name === "OrganizationRequiredServerError";
478
+ }
479
+ async function readCallerMemberships(client) {
480
+ try {
481
+ const { data: claims } = await client.auth.getClaims();
482
+ const userId = claims?.claims?.sub;
483
+ if (typeof userId !== "string" || !userId) return null;
484
+ const query = client.schema("iam").from("memberships").select("organization_id, organizations:organization_id(id,name,abbreviation)");
485
+ const { data, error } = await query.eq("user_id", userId).eq("container_type", "organization").eq("status", "active").is("deleted_at", null);
486
+ if (error || !Array.isArray(data)) return null;
487
+ const rows = data;
488
+ return rows.map((row) => ({
489
+ id: row.organizations?.id ?? row.organization_id,
490
+ name: row.organizations?.name ?? "Untitled organization",
491
+ ...row.organizations?.abbreviation ? { abbreviation: row.organizations.abbreviation } : {}
492
+ })).sort((a, b) => a.name.localeCompare(b.name));
493
+ } catch {
494
+ return null;
495
+ }
496
+ }
497
+ async function organizationRequired(client, message) {
498
+ throw new OrganizationRequiredServerError(message, await readCallerMemberships(client));
499
+ }
500
+ function organizationRequiredEnvelope(error, setOn = "request") {
501
+ return {
502
+ error: ORGANIZATION_REQUIRED_CODE,
503
+ code: ORGANIZATION_REQUIRED_CODE,
504
+ message: error.message,
505
+ user_message: "Choose the organization you're working in, then try again.",
506
+ details: {
507
+ hold: ORGANIZATION_REQUIRED_CODE,
508
+ can_choose: true,
509
+ set_on: setOn,
510
+ remedy: "Choose the organization you're working in and send it with the request (the X-Organization-Id header), then try again.",
511
+ organizations: error.memberships,
512
+ memberships_url: MEMBERSHIPS_URL
513
+ }
514
+ };
515
+ }
516
+ function organizationRequiredResponse(error) {
517
+ return Response.json(organizationRequiredEnvelope(error), { status: 400 });
518
+ }
@@ -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 };
@@ -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 };
@@ -378,17 +378,111 @@ function createActiveOrganization(options) {
378
378
  if (options.autoLoad ?? true) void load();
379
379
  return api;
380
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
+ }
381
462
  export {
463
+ MEMBERSHIPS_URL,
382
464
  NoActiveOrganizationError,
383
465
  NotAMemberError,
466
+ ORGANIZATION_CONTEXT_REQUIRED_CODE,
384
467
  ORGANIZATION_ENTITY_TYPE,
468
+ ORGANIZATION_REQUIRED_CODE,
469
+ ORGANIZATION_REQUIRED_WIRE_CODE,
470
+ OrganizationRequiredServerError,
385
471
  classifyOrganizationRefusal,
386
472
  createActiveOrganization,
473
+ extractOrganizationHoldMemberships,
474
+ isOrganizationRequiredEnvelope,
475
+ isOrganizationRequiredError,
476
+ isOrganizationRequiredServerError,
387
477
  memoryHeldOrganization,
388
478
  oldestMembership,
389
479
  orderOrganizations,
390
480
  organizationMatches,
481
+ organizationRequired,
482
+ organizationRequiredEnvelope,
483
+ organizationRequiredResponse,
391
484
  readAccountOrganizationChoices,
485
+ readCallerMemberships,
392
486
  readMemberships,
393
487
  readOrganizationFavourites,
394
488
  resolveActiveOrganization,
@@ -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 };