@ai-matrx/data 0.14.2 → 0.15.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 +16 -0
- package/README.md +1 -0
- package/dist/db.cjs +155 -0
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +39 -1
- package/dist/db.d.ts +39 -1
- package/dist/db.js +155 -0
- package/dist/db.js.map +1 -1
- package/package.json +1 -1
package/dist/db.d.cts
CHANGED
|
@@ -523,4 +523,42 @@ declare function encodeActiveOrgSelection(selection: ActiveOrgSelection): string
|
|
|
523
523
|
declare function decodeActiveOrgSelection(raw: string | null | undefined): ActiveOrgSelection | null;
|
|
524
524
|
declare function createActiveOrgCookie(args: CreateActiveOrgCookieArgs): ActiveOrgCookie;
|
|
525
525
|
|
|
526
|
-
|
|
526
|
+
/**
|
|
527
|
+
* OAuth-aware transport for a Supabase auth-js client.
|
|
528
|
+
*
|
|
529
|
+
* Supabase's ordinary GoTrue refresh endpoint is `/auth/v1/token`, while a
|
|
530
|
+
* session minted for a registered OAuth client must refresh at
|
|
531
|
+
* `/auth/v1/oauth/token` and include that client id. auth-js owns session
|
|
532
|
+
* storage, locking, rotation and notifications; this module changes only the
|
|
533
|
+
* one outbound refresh request after proving that its refresh token belongs to
|
|
534
|
+
* an allowlisted OAuth client. It never calls `auth.getSession()`.
|
|
535
|
+
*/
|
|
536
|
+
interface OAuthRefreshStorage {
|
|
537
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
538
|
+
}
|
|
539
|
+
interface CreateOAuthRefreshFetchArgs {
|
|
540
|
+
/** The exact Supabase project origin, e.g. `https://db.matrxserver.com`. */
|
|
541
|
+
supabaseUrl: string;
|
|
542
|
+
/** The same auth storage auth-js uses. */
|
|
543
|
+
storage: OAuthRefreshStorage;
|
|
544
|
+
/** The auth-js storage key containing the persisted session JSON. */
|
|
545
|
+
storageKey: string;
|
|
546
|
+
/** Public OAuth client ids this host is intentionally allowed to refresh. */
|
|
547
|
+
oauthClientIds: readonly string[];
|
|
548
|
+
/** Injectable only for tests and non-browser hosts. */
|
|
549
|
+
fetch?: typeof globalThis.fetch;
|
|
550
|
+
}
|
|
551
|
+
interface JwtClaims {
|
|
552
|
+
sub?: unknown;
|
|
553
|
+
client_id?: unknown;
|
|
554
|
+
exp?: unknown;
|
|
555
|
+
}
|
|
556
|
+
/** Decode routing metadata only. The server remains the authentication authority. */
|
|
557
|
+
declare function decodeOAuthRefreshClaims(token: string): JwtClaims | null;
|
|
558
|
+
/**
|
|
559
|
+
* Returns a fetch implementation suitable for `createClient(..., { global:
|
|
560
|
+
* { fetch } })`. Normal sessions and unrelated requests pass through exactly.
|
|
561
|
+
*/
|
|
562
|
+
declare function createOAuthRefreshFetch(args: CreateOAuthRefreshFetchArgs): typeof globalThis.fetch;
|
|
563
|
+
|
|
564
|
+
export { type ActiveOrgCookie, type ActiveOrgCookieOptions, type ActiveOrgSelection, type AuthRetryableResult, type CreateActiveOrgCookieArgs, type CreateOAuthRefreshFetchArgs, type CreateSessionRetryOptions, DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE, DEFAULT_ACTIVE_ORG_COOKIE_NAME, type GuardedUpdateArgs, type GuardedUpdateResult, IncompleteReadError, type JsonObject, type MergeJsonColumnArgs, type MergeJsonColumnResult, type OAuthRefreshStorage, type PagedReadQuery, type PagedReadResult, type ReadAllRowsOptions, type ReadAllRowsRestOptions, type SessionSource, SessionUnavailableError, type VersionedRow, asJsonObject, createActiveOrgCookie, createOAuthRefreshFetch, createSessionRetry, decodeActiveOrgSelection, decodeOAuthRefreshClaims, encodeActiveOrgSelection, guardedUpdate, isMissingSessionError, mergeJsonColumn, readAllRows, readAllRowsRest, tryReadAllRows, tryReadAllRowsRest };
|
package/dist/db.d.ts
CHANGED
|
@@ -523,4 +523,42 @@ declare function encodeActiveOrgSelection(selection: ActiveOrgSelection): string
|
|
|
523
523
|
declare function decodeActiveOrgSelection(raw: string | null | undefined): ActiveOrgSelection | null;
|
|
524
524
|
declare function createActiveOrgCookie(args: CreateActiveOrgCookieArgs): ActiveOrgCookie;
|
|
525
525
|
|
|
526
|
-
|
|
526
|
+
/**
|
|
527
|
+
* OAuth-aware transport for a Supabase auth-js client.
|
|
528
|
+
*
|
|
529
|
+
* Supabase's ordinary GoTrue refresh endpoint is `/auth/v1/token`, while a
|
|
530
|
+
* session minted for a registered OAuth client must refresh at
|
|
531
|
+
* `/auth/v1/oauth/token` and include that client id. auth-js owns session
|
|
532
|
+
* storage, locking, rotation and notifications; this module changes only the
|
|
533
|
+
* one outbound refresh request after proving that its refresh token belongs to
|
|
534
|
+
* an allowlisted OAuth client. It never calls `auth.getSession()`.
|
|
535
|
+
*/
|
|
536
|
+
interface OAuthRefreshStorage {
|
|
537
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
538
|
+
}
|
|
539
|
+
interface CreateOAuthRefreshFetchArgs {
|
|
540
|
+
/** The exact Supabase project origin, e.g. `https://db.matrxserver.com`. */
|
|
541
|
+
supabaseUrl: string;
|
|
542
|
+
/** The same auth storage auth-js uses. */
|
|
543
|
+
storage: OAuthRefreshStorage;
|
|
544
|
+
/** The auth-js storage key containing the persisted session JSON. */
|
|
545
|
+
storageKey: string;
|
|
546
|
+
/** Public OAuth client ids this host is intentionally allowed to refresh. */
|
|
547
|
+
oauthClientIds: readonly string[];
|
|
548
|
+
/** Injectable only for tests and non-browser hosts. */
|
|
549
|
+
fetch?: typeof globalThis.fetch;
|
|
550
|
+
}
|
|
551
|
+
interface JwtClaims {
|
|
552
|
+
sub?: unknown;
|
|
553
|
+
client_id?: unknown;
|
|
554
|
+
exp?: unknown;
|
|
555
|
+
}
|
|
556
|
+
/** Decode routing metadata only. The server remains the authentication authority. */
|
|
557
|
+
declare function decodeOAuthRefreshClaims(token: string): JwtClaims | null;
|
|
558
|
+
/**
|
|
559
|
+
* Returns a fetch implementation suitable for `createClient(..., { global:
|
|
560
|
+
* { fetch } })`. Normal sessions and unrelated requests pass through exactly.
|
|
561
|
+
*/
|
|
562
|
+
declare function createOAuthRefreshFetch(args: CreateOAuthRefreshFetchArgs): typeof globalThis.fetch;
|
|
563
|
+
|
|
564
|
+
export { type ActiveOrgCookie, type ActiveOrgCookieOptions, type ActiveOrgSelection, type AuthRetryableResult, type CreateActiveOrgCookieArgs, type CreateOAuthRefreshFetchArgs, type CreateSessionRetryOptions, DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE, DEFAULT_ACTIVE_ORG_COOKIE_NAME, type GuardedUpdateArgs, type GuardedUpdateResult, IncompleteReadError, type JsonObject, type MergeJsonColumnArgs, type MergeJsonColumnResult, type OAuthRefreshStorage, type PagedReadQuery, type PagedReadResult, type ReadAllRowsOptions, type ReadAllRowsRestOptions, type SessionSource, SessionUnavailableError, type VersionedRow, asJsonObject, createActiveOrgCookie, createOAuthRefreshFetch, createSessionRetry, decodeActiveOrgSelection, decodeOAuthRefreshClaims, encodeActiveOrgSelection, guardedUpdate, isMissingSessionError, mergeJsonColumn, readAllRows, readAllRowsRest, tryReadAllRows, tryReadAllRowsRest };
|
package/dist/db.js
CHANGED
|
@@ -475,6 +475,159 @@ function createActiveOrgCookie(args) {
|
|
|
475
475
|
|
|
476
476
|
// src/db/realtime.ts
|
|
477
477
|
import { uniqueChannelTopic } from "@ai-matrx/realtime";
|
|
478
|
+
|
|
479
|
+
// src/db/oauth-refresh-transport.ts
|
|
480
|
+
function object(value) {
|
|
481
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
482
|
+
}
|
|
483
|
+
function text(value) {
|
|
484
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
485
|
+
}
|
|
486
|
+
function decodeOAuthRefreshClaims(token) {
|
|
487
|
+
const payload = token.split(".")[1];
|
|
488
|
+
if (!payload) return null;
|
|
489
|
+
try {
|
|
490
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
491
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
492
|
+
const json = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8");
|
|
493
|
+
return object(JSON.parse(json)) ?? null;
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function isOrdinaryRefreshRequest(input, init, origin) {
|
|
499
|
+
const request = input instanceof Request ? input : null;
|
|
500
|
+
const url = new URL(request?.url ?? String(input), origin);
|
|
501
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
502
|
+
return method === "POST" && url.origin === origin && url.pathname === "/auth/v1/token" && url.searchParams.get("grant_type") === "refresh_token";
|
|
503
|
+
}
|
|
504
|
+
async function refreshTokenFrom(input, init) {
|
|
505
|
+
let body = init?.body;
|
|
506
|
+
if (body === void 0 && input instanceof Request) body = await input.clone().text();
|
|
507
|
+
if (body instanceof URLSearchParams) return body.get("refresh_token");
|
|
508
|
+
if (typeof body !== "string") return null;
|
|
509
|
+
try {
|
|
510
|
+
return text(object(JSON.parse(body))?.refresh_token);
|
|
511
|
+
} catch {
|
|
512
|
+
return new URLSearchParams(body).get("refresh_token");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function rewrittenInit(input, init, headers, body) {
|
|
516
|
+
const request = input instanceof Request ? input : null;
|
|
517
|
+
const copied = {
|
|
518
|
+
// init takes precedence, just as native fetch does. Copying Request fields
|
|
519
|
+
// preserves the public Fetch contract after replacing its body.
|
|
520
|
+
method: "POST",
|
|
521
|
+
headers,
|
|
522
|
+
body
|
|
523
|
+
};
|
|
524
|
+
const inherited = {
|
|
525
|
+
credentials: init?.credentials ?? request?.credentials,
|
|
526
|
+
cache: init?.cache ?? request?.cache,
|
|
527
|
+
redirect: init?.redirect ?? request?.redirect,
|
|
528
|
+
referrer: init?.referrer ?? request?.referrer,
|
|
529
|
+
referrerPolicy: init?.referrerPolicy ?? request?.referrerPolicy,
|
|
530
|
+
integrity: init?.integrity ?? request?.integrity,
|
|
531
|
+
keepalive: init?.keepalive ?? request?.keepalive,
|
|
532
|
+
signal: init?.signal ?? request?.signal
|
|
533
|
+
};
|
|
534
|
+
for (const [key, value] of Object.entries(inherited)) {
|
|
535
|
+
if (value !== void 0) Object.assign(copied, { [key]: value });
|
|
536
|
+
}
|
|
537
|
+
return copied;
|
|
538
|
+
}
|
|
539
|
+
async function storedSession(storage, key) {
|
|
540
|
+
const raw = await storage.getItem(key);
|
|
541
|
+
if (!raw) return null;
|
|
542
|
+
try {
|
|
543
|
+
return object(JSON.parse(raw));
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function sameSubject(previousAccessToken, refreshedAccessToken, clientId) {
|
|
549
|
+
const previous = decodeOAuthRefreshClaims(previousAccessToken);
|
|
550
|
+
const refreshed = decodeOAuthRefreshClaims(refreshedAccessToken);
|
|
551
|
+
return text(previous?.sub) !== null && previous?.sub === refreshed?.sub && previous?.client_id === clientId && refreshed?.client_id === clientId;
|
|
552
|
+
}
|
|
553
|
+
function retryableRefusal(message) {
|
|
554
|
+
return new Response(JSON.stringify({ error: "temporarily_unavailable", error_description: message }), {
|
|
555
|
+
status: 503,
|
|
556
|
+
headers: { "content-type": "application/json", "retry-after": "1" }
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
async function normalizeOAuthResponse(response, session, clientId) {
|
|
560
|
+
if (!response.ok) return response;
|
|
561
|
+
const previousAccessToken = text(session.access_token);
|
|
562
|
+
const previousUser = session.user;
|
|
563
|
+
const previousSubject = previousAccessToken ? text(decodeOAuthRefreshClaims(previousAccessToken)?.sub) : null;
|
|
564
|
+
if (!previousAccessToken || !previousSubject || text(object(previousUser)?.id) !== previousSubject) {
|
|
565
|
+
return retryableRefusal("Stored OAuth identity does not match its credential. Restore the account connection.");
|
|
566
|
+
}
|
|
567
|
+
let payload = null;
|
|
568
|
+
try {
|
|
569
|
+
payload = object(await response.clone().json());
|
|
570
|
+
} catch {
|
|
571
|
+
return retryableRefusal("OAuth refresh returned an invalid response.");
|
|
572
|
+
}
|
|
573
|
+
const refreshedAccessToken = text(payload?.access_token);
|
|
574
|
+
if (!payload || !refreshedAccessToken || !sameSubject(previousAccessToken, refreshedAccessToken, clientId)) {
|
|
575
|
+
return retryableRefusal("OAuth refresh returned a credential for an unexpected identity or client.");
|
|
576
|
+
}
|
|
577
|
+
if (payload.user !== void 0 && text(object(payload.user)?.id) !== previousSubject) {
|
|
578
|
+
return retryableRefusal("OAuth refresh returned a user that does not match its credential.");
|
|
579
|
+
}
|
|
580
|
+
if (payload.user === void 0) payload.user = previousUser;
|
|
581
|
+
if (text(payload.refresh_token) === null) payload.refresh_token = session.refresh_token;
|
|
582
|
+
const headers = new Headers(response.headers);
|
|
583
|
+
headers.set("content-type", "application/json");
|
|
584
|
+
return new Response(JSON.stringify(payload), {
|
|
585
|
+
status: response.status,
|
|
586
|
+
statusText: response.statusText,
|
|
587
|
+
headers
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
async function replacementAfterRejectedRefresh(response, storage, storageKey, previous, clientId) {
|
|
591
|
+
if (response.status !== 400 && response.status !== 401) return response;
|
|
592
|
+
const replacement = await storedSession(storage, storageKey);
|
|
593
|
+
const previousRefresh = text(previous.refresh_token);
|
|
594
|
+
const replacementRefresh = text(replacement?.refresh_token);
|
|
595
|
+
const previousAccess = text(previous.access_token);
|
|
596
|
+
const replacementAccess = text(replacement?.access_token);
|
|
597
|
+
if (!replacement || !previousRefresh || !replacementRefresh || replacementRefresh === previousRefresh || !previousAccess || !replacementAccess || !sameSubject(previousAccess, replacementAccess, clientId) || replacement.user === void 0) return response;
|
|
598
|
+
return new Response(JSON.stringify(replacement), {
|
|
599
|
+
status: 200,
|
|
600
|
+
headers: { "content-type": "application/json" }
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
function createOAuthRefreshFetch(args) {
|
|
604
|
+
const origin = new URL(args.supabaseUrl).origin;
|
|
605
|
+
const upstream = args.fetch ?? globalThis.fetch;
|
|
606
|
+
const allowed = new Set(args.oauthClientIds);
|
|
607
|
+
return async (input, init) => {
|
|
608
|
+
if (!isOrdinaryRefreshRequest(input, init, origin)) return upstream(input, init);
|
|
609
|
+
const refreshToken = await refreshTokenFrom(input, init);
|
|
610
|
+
const session = await storedSession(args.storage, args.storageKey);
|
|
611
|
+
const storedRefreshToken = text(session?.refresh_token);
|
|
612
|
+
const accessToken = text(session?.access_token);
|
|
613
|
+
const clientId = accessToken ? text(decodeOAuthRefreshClaims(accessToken)?.client_id) : null;
|
|
614
|
+
if (!session || !clientId) {
|
|
615
|
+
return upstream(input, init);
|
|
616
|
+
}
|
|
617
|
+
if (!allowed.has(clientId)) return retryableRefusal("This OAuth client is not allowed to refresh in this host.");
|
|
618
|
+
if (!refreshToken || refreshToken !== storedRefreshToken) {
|
|
619
|
+
return retryableRefusal("OAuth refresh token no longer matches the stored session; resolve the session again.");
|
|
620
|
+
}
|
|
621
|
+
const request = input instanceof Request ? input : null;
|
|
622
|
+
const headers = new Headers(init?.headers ?? request?.headers);
|
|
623
|
+
headers.set("content-type", "application/x-www-form-urlencoded");
|
|
624
|
+
const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId });
|
|
625
|
+
const response = await upstream(`${origin}/auth/v1/oauth/token`, rewrittenInit(input, init, headers, body));
|
|
626
|
+
const adopted = await replacementAfterRejectedRefresh(response, args.storage, args.storageKey, session, clientId);
|
|
627
|
+
const responseSession = adopted === response ? session : await storedSession(args.storage, args.storageKey) ?? session;
|
|
628
|
+
return normalizeOAuthResponse(adopted, responseSession, clientId);
|
|
629
|
+
};
|
|
630
|
+
}
|
|
478
631
|
export {
|
|
479
632
|
DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE,
|
|
480
633
|
DEFAULT_ACTIVE_ORG_COOKIE_NAME,
|
|
@@ -484,8 +637,10 @@ export {
|
|
|
484
637
|
asJsonObject,
|
|
485
638
|
createActiveOrgCookie,
|
|
486
639
|
createAuthCookie,
|
|
640
|
+
createOAuthRefreshFetch,
|
|
487
641
|
createSessionRetry,
|
|
488
642
|
decodeActiveOrgSelection,
|
|
643
|
+
decodeOAuthRefreshClaims,
|
|
489
644
|
encodeActiveOrgSelection,
|
|
490
645
|
guardedUpdate,
|
|
491
646
|
isMissingSessionError,
|