@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/CHANGELOG.md +22 -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 +94 -0
- package/dist/organizations.d.cts +107 -1
- package/dist/organizations.d.ts +107 -1
- package/dist/organizations.js +94 -0
- package/dist/storage-Dqnye4KF.d.cts +51 -0
- package/dist/storage-Dqnye4KF.d.ts +51 -0
- package/package.json +12 -2
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
|
-
|
|
167
|
+
createServerAuth,
|
|
168
|
+
createServiceClient,
|
|
169
|
+
fileStorage,
|
|
170
|
+
verifyRequest
|
|
36
171
|
};
|
package/dist/organizations.cjs
CHANGED
|
@@ -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
|
+
}
|
package/dist/organizations.d.cts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/organizations.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/organizations.js
CHANGED
|
@@ -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 };
|