@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.
- package/CHANGELOG.md +32 -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 +175 -4
- package/dist/index.d.cts +5 -52
- package/dist/index.d.ts +5 -52
- package/dist/index.js +178 -4
- 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 +132 -3
- package/dist/organizations.d.cts +108 -2
- package/dist/organizations.d.ts +108 -2
- package/dist/organizations.js +132 -3
- package/dist/{session-DhRO4euL.d.cts → session-DdQiStp6.d.cts} +12 -2
- package/dist/{session-DhRO4euL.d.ts → session-DdQiStp6.d.ts} +12 -2
- package/dist/storage-Dqnye4KF.d.cts +51 -0
- package/dist/storage-Dqnye4KF.d.ts +51 -0
- package/package.json +12 -2
package/dist/organizations.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
-
import { A as AuthClient } from './session-
|
|
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
|
-
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
-
import { A as AuthClient } from './session-
|
|
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
|
-
|
|
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
|
@@ -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
|
|
143
|
-
if (
|
|
144
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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": "
|
|
204
|
+
"gitHead": "eeeafb1ae105f78c584681829e13dd69b200abc6"
|
|
195
205
|
}
|