@ragable/sdk 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +205 -14
- package/dist/index.d.ts +205 -14
- package/dist/index.js +345 -81
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +343 -81
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ __export(index_exports, {
|
|
|
53
53
|
RagableError: () => RagableError,
|
|
54
54
|
RagableNetworkError: () => RagableNetworkError,
|
|
55
55
|
RagableSdkError: () => RagableSdkError,
|
|
56
|
+
RagableServerClient: () => RagableServerClient,
|
|
56
57
|
RagableTimeoutError: () => RagableTimeoutError,
|
|
57
58
|
SessionStorageAdapter: () => SessionStorageAdapter,
|
|
58
59
|
Transport: () => Transport,
|
|
@@ -67,6 +68,7 @@ __export(index_exports, {
|
|
|
67
68
|
createBrowserClient: () => createBrowserClient,
|
|
68
69
|
createClient: () => createClient,
|
|
69
70
|
createRagableBrowserClient: () => createRagableBrowserClient,
|
|
71
|
+
createServerClient: () => createServerClient,
|
|
70
72
|
createStreamResultFromParts: () => createStreamResultFromParts,
|
|
71
73
|
detectStorage: () => detectStorage,
|
|
72
74
|
effectiveDataAuth: () => effectiveDataAuth,
|
|
@@ -760,7 +762,11 @@ function generateIdempotencyKey() {
|
|
|
760
762
|
return `idk-${Date.now()}-${_uuidCounter}-${Math.random().toString(36).slice(2, 10)}`;
|
|
761
763
|
}
|
|
762
764
|
function requestCacheKey(req) {
|
|
763
|
-
|
|
765
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
766
|
+
const dbInstance = req.headers.get("x-database-instance-id") ?? "";
|
|
767
|
+
return `${req.method}:${req.url}
|
|
768
|
+
${auth}
|
|
769
|
+
${dbInstance}`;
|
|
764
770
|
}
|
|
765
771
|
var Transport = class {
|
|
766
772
|
constructor(options = {}) {
|
|
@@ -787,13 +793,16 @@ var Transport = class {
|
|
|
787
793
|
async execute(req) {
|
|
788
794
|
if (req.method === "GET") {
|
|
789
795
|
const key = requestCacheKey(req);
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
this.inflightGets.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
796
|
+
let base = this.inflightGets.get(key);
|
|
797
|
+
if (!base) {
|
|
798
|
+
base = this._executeWithRetry(req);
|
|
799
|
+
this.inflightGets.set(key, base);
|
|
800
|
+
const clear = () => {
|
|
801
|
+
if (this.inflightGets.get(key) === base) this.inflightGets.delete(key);
|
|
802
|
+
};
|
|
803
|
+
base.then(clear, clear);
|
|
804
|
+
}
|
|
805
|
+
return base.then((r) => r.clone());
|
|
797
806
|
}
|
|
798
807
|
return this._executeWithRetry(req);
|
|
799
808
|
}
|
|
@@ -813,6 +822,8 @@ var Transport = class {
|
|
|
813
822
|
let lastError;
|
|
814
823
|
const maxAttempts = 1 + retryOpts.maxRetries;
|
|
815
824
|
let did401Refresh = false;
|
|
825
|
+
const isIdempotent = req.method === "GET" || req.method === "HEAD";
|
|
826
|
+
const retryEnabled = retryOpts.maxRetries > 0 && (isIdempotent || req.retry !== void 0);
|
|
816
827
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
817
828
|
try {
|
|
818
829
|
const response = await this._singleFetch(finalReq, timeoutMs);
|
|
@@ -825,7 +836,7 @@ var Transport = class {
|
|
|
825
836
|
continue;
|
|
826
837
|
}
|
|
827
838
|
}
|
|
828
|
-
if (!response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
|
|
839
|
+
if (retryEnabled && !response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
|
|
829
840
|
let delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
|
|
830
841
|
if (retryOpts.respectRetryAfter) {
|
|
831
842
|
const ra = parseRetryAfter(response.headers.get("retry-after"));
|
|
@@ -841,7 +852,7 @@ var Transport = class {
|
|
|
841
852
|
throw e;
|
|
842
853
|
}
|
|
843
854
|
lastError = e;
|
|
844
|
-
if (attempt < maxAttempts - 1) {
|
|
855
|
+
if (retryEnabled && attempt < maxAttempts - 1) {
|
|
845
856
|
const delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
|
|
846
857
|
this.onRetry?.(finalReq, attempt + 1, delayMs, e.message);
|
|
847
858
|
await sleep(delayMs);
|
|
@@ -973,13 +984,29 @@ function extractPostgRESTErrorMessage(payload, status, statusText) {
|
|
|
973
984
|
return msg;
|
|
974
985
|
}
|
|
975
986
|
async function parsePostgRESTResponse(response) {
|
|
976
|
-
if (response.status === 204) return null;
|
|
987
|
+
if (response.status === 204 && response.ok) return null;
|
|
977
988
|
const text = await response.text();
|
|
978
|
-
if (!text)
|
|
989
|
+
if (!text) {
|
|
990
|
+
if (!response.ok) {
|
|
991
|
+
throw new RagableError(
|
|
992
|
+
response.statusText || `HTTP ${response.status}`,
|
|
993
|
+
response.status,
|
|
994
|
+
null
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
979
999
|
let payload;
|
|
980
1000
|
try {
|
|
981
1001
|
payload = JSON.parse(text);
|
|
982
1002
|
} catch {
|
|
1003
|
+
if (!response.ok) {
|
|
1004
|
+
throw new RagableError(
|
|
1005
|
+
extractPostgRESTErrorMessage(text, response.status, response.statusText),
|
|
1006
|
+
response.status,
|
|
1007
|
+
null
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
983
1010
|
throw new RagableError(
|
|
984
1011
|
`PostgREST response parse error: ${text.slice(0, 200)}`,
|
|
985
1012
|
response.status,
|
|
@@ -1948,6 +1975,7 @@ var AuthBroadcastChannel = class {
|
|
|
1948
1975
|
};
|
|
1949
1976
|
|
|
1950
1977
|
// src/auth.ts
|
|
1978
|
+
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
1951
1979
|
function parseExpiresInSeconds(raw) {
|
|
1952
1980
|
if (typeof raw === "number") return raw;
|
|
1953
1981
|
const s = raw.trim().toLowerCase();
|
|
@@ -1994,7 +2022,14 @@ var RagableAuth = class {
|
|
|
1994
2022
|
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
1995
2023
|
__publicField(this, "broadcast", null);
|
|
1996
2024
|
__publicField(this, "visibilityHandler", null);
|
|
1997
|
-
|
|
2025
|
+
/** Memoizes the one-shot restore so concurrent callers (constructor eager init,
|
|
2026
|
+
* every `onAuthStateChange` subscriber, `getSession`) share a single result.
|
|
2027
|
+
* Non-null also means "restore has started", replacing the old boolean flag. */
|
|
2028
|
+
__publicField(this, "initializePromise", null);
|
|
2029
|
+
/** Bumped on every explicit session change (sign-in/out, refresh). The async
|
|
2030
|
+
* restore captures this and refuses to overwrite a newer session op that
|
|
2031
|
+
* landed while it was reading storage (e.g. a sign-out during page load). */
|
|
2032
|
+
__publicField(this, "sessionEpoch", 0);
|
|
1998
2033
|
this.baseUrl = DEFAULT_RAGABLE_API_BASE.replace(/\/+$/, "");
|
|
1999
2034
|
this.authGroupId = config.authGroupId;
|
|
2000
2035
|
this.fetchImpl = bindFetch(config.fetch);
|
|
@@ -2028,33 +2063,43 @@ var RagableAuth = class {
|
|
|
2028
2063
|
if (this.debug) console.debug("[RagableAuth]", ...args);
|
|
2029
2064
|
}
|
|
2030
2065
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
this.
|
|
2066
|
+
/**
|
|
2067
|
+
* Restore a persisted session (once). Memoized: every caller awaits the same
|
|
2068
|
+
* promise, so the eager constructor init, `getSession`, and each
|
|
2069
|
+
* `onAuthStateChange` subscriber's INITIAL_SESSION replay never race or
|
|
2070
|
+
* double-restore. Does NOT emit `INITIAL_SESSION` globally — that event is
|
|
2071
|
+
* delivered per-subscriber by `onAuthStateChange` (Supabase-parity), so a
|
|
2072
|
+
* listener attached after restore still sees the existing session.
|
|
2073
|
+
*/
|
|
2074
|
+
initialize() {
|
|
2075
|
+
if (this.initializePromise) return this.initializePromise;
|
|
2076
|
+
this.initializePromise = this._initialize();
|
|
2077
|
+
return this.initializePromise;
|
|
2078
|
+
}
|
|
2079
|
+
async _initialize() {
|
|
2080
|
+
if (!this.persistSession) return this.currentSession;
|
|
2081
|
+
const epoch = this.sessionEpoch;
|
|
2082
|
+
try {
|
|
2083
|
+
const raw = await this.storage.getItem(this.storageKey);
|
|
2084
|
+
if (!raw) return this.currentSession;
|
|
2085
|
+
if (this.sessionEpoch !== epoch) return this.currentSession;
|
|
2086
|
+
const session = JSON.parse(raw);
|
|
2087
|
+
const stillValid = !!session.expires_at && session.expires_at > nowSeconds();
|
|
2088
|
+
if (stillValid) {
|
|
2089
|
+
this.currentSession = session;
|
|
2090
|
+
this.scheduleRefresh(session);
|
|
2091
|
+
this.log("Restored session from storage");
|
|
2092
|
+
} else if (session.refresh_token) {
|
|
2093
|
+
this.currentSession = session;
|
|
2094
|
+
this.log("Stored session expired, attempting refresh");
|
|
2095
|
+
const refreshed = await this.singleFlightRefresh(session.refresh_token);
|
|
2096
|
+
if (refreshed) this.currentSession = refreshed;
|
|
2097
|
+
} else {
|
|
2098
|
+
await this.storage.removeItem(this.storageKey);
|
|
2055
2099
|
}
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
this.log("Failed to restore session", e);
|
|
2056
2102
|
}
|
|
2057
|
-
this.emit("INITIAL_SESSION", this.currentSession);
|
|
2058
2103
|
return this.currentSession;
|
|
2059
2104
|
}
|
|
2060
2105
|
// ── Auth methods ───────────────────────────────────────────────────────────
|
|
@@ -2084,6 +2129,7 @@ var RagableAuth = class {
|
|
|
2084
2129
|
});
|
|
2085
2130
|
}
|
|
2086
2131
|
async signOut(_options) {
|
|
2132
|
+
this.sessionEpoch++;
|
|
2087
2133
|
this.currentSession = null;
|
|
2088
2134
|
this.clearRefreshTimer();
|
|
2089
2135
|
if (this.persistSession) {
|
|
@@ -2103,12 +2149,12 @@ var RagableAuth = class {
|
|
|
2103
2149
|
});
|
|
2104
2150
|
}
|
|
2105
2151
|
async getSession() {
|
|
2106
|
-
|
|
2152
|
+
await this.initialize();
|
|
2107
2153
|
return { data: { session: this.currentSession }, error: null };
|
|
2108
2154
|
}
|
|
2109
2155
|
async getUser() {
|
|
2110
2156
|
return asPostgrestResponse(async () => {
|
|
2111
|
-
const token = this.
|
|
2157
|
+
const token = await this.getValidAccessToken();
|
|
2112
2158
|
if (!token) throw new RagableError("Not authenticated", 401, null);
|
|
2113
2159
|
return this.fetchAuthWithBearer("/me", "GET", token);
|
|
2114
2160
|
});
|
|
@@ -2153,6 +2199,15 @@ var RagableAuth = class {
|
|
|
2153
2199
|
_subCounter++;
|
|
2154
2200
|
const id = `sub-${_subCounter}`;
|
|
2155
2201
|
this.listeners.set(id, callback);
|
|
2202
|
+
void this.initialize().then((session) => {
|
|
2203
|
+
const cb = this.listeners.get(id);
|
|
2204
|
+
if (!cb) return;
|
|
2205
|
+
try {
|
|
2206
|
+
cb("INITIAL_SESSION", session);
|
|
2207
|
+
} catch (e) {
|
|
2208
|
+
this.log("Listener threw", e);
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2156
2211
|
const unsubscribe = () => {
|
|
2157
2212
|
this.listeners.delete(id);
|
|
2158
2213
|
};
|
|
@@ -2162,12 +2217,19 @@ var RagableAuth = class {
|
|
|
2162
2217
|
getAccessToken() {
|
|
2163
2218
|
return this.currentSession?.access_token ?? null;
|
|
2164
2219
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2220
|
+
/**
|
|
2221
|
+
* Returns an access token guaranteed fresh for at least `refreshSkewSeconds`,
|
|
2222
|
+
* refreshing (single-flight) if needed. Pass `force: true` to bypass the skew
|
|
2223
|
+
* check and refresh now — used by the transport's 401 handler so a token the
|
|
2224
|
+
* server rejected (key rotation, clock skew, early revocation) self-heals on
|
|
2225
|
+
* retry instead of failing the call.
|
|
2226
|
+
*/
|
|
2227
|
+
async getValidAccessToken(force = false) {
|
|
2228
|
+
await this.initialize();
|
|
2167
2229
|
const session = this.currentSession;
|
|
2168
2230
|
if (!session) return null;
|
|
2169
2231
|
const secondsUntilExpiry = session.expires_at - nowSeconds();
|
|
2170
|
-
if (secondsUntilExpiry <= this.refreshSkewSeconds) {
|
|
2232
|
+
if (force || secondsUntilExpiry <= this.refreshSkewSeconds) {
|
|
2171
2233
|
const refreshed = await this.singleFlightRefresh(session.refresh_token);
|
|
2172
2234
|
return refreshed?.access_token ?? null;
|
|
2173
2235
|
}
|
|
@@ -2260,6 +2322,7 @@ var RagableAuth = class {
|
|
|
2260
2322
|
};
|
|
2261
2323
|
}
|
|
2262
2324
|
async setSessionInternal(session, event) {
|
|
2325
|
+
this.sessionEpoch++;
|
|
2263
2326
|
this.currentSession = session;
|
|
2264
2327
|
await this.persistCurrentSession();
|
|
2265
2328
|
this.broadcast?.postSessionUpdated(JSON.stringify(session));
|
|
@@ -2290,12 +2353,13 @@ var RagableAuth = class {
|
|
|
2290
2353
|
if (!this.autoRefreshToken) return;
|
|
2291
2354
|
const secondsUntilExpiry = session.expires_at - nowSeconds();
|
|
2292
2355
|
const refreshIn = Math.max(0, secondsUntilExpiry - this.refreshSkewSeconds);
|
|
2293
|
-
|
|
2356
|
+
const delayMs = Math.min(refreshIn * 1e3, MAX_TIMEOUT_MS);
|
|
2357
|
+
this.log(`Scheduling refresh in ${Math.round(delayMs / 1e3)}s`);
|
|
2294
2358
|
this.refreshTimer = setTimeout(() => {
|
|
2295
2359
|
this.singleFlightRefresh(session.refresh_token).catch((e) => {
|
|
2296
2360
|
this.log("Scheduled refresh failed", e);
|
|
2297
2361
|
});
|
|
2298
|
-
},
|
|
2362
|
+
}, delayMs);
|
|
2299
2363
|
}
|
|
2300
2364
|
clearRefreshTimer() {
|
|
2301
2365
|
if (this.refreshTimer !== null) {
|
|
@@ -2303,16 +2367,57 @@ var RagableAuth = class {
|
|
|
2303
2367
|
this.refreshTimer = null;
|
|
2304
2368
|
}
|
|
2305
2369
|
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Refresh the session, deduplicating concurrent callers onto one in-flight
|
|
2372
|
+
* request. Side effects (persisting the new session, or clearing it and
|
|
2373
|
+
* emitting SIGNED_OUT / TOKEN_REFRESH_FAILED) run exactly once inside the
|
|
2374
|
+
* shared promise, so two callers can't double-emit. Resolves to the new
|
|
2375
|
+
* session, or `null` when the refresh failed.
|
|
2376
|
+
*/
|
|
2306
2377
|
async singleFlightRefresh(refreshToken) {
|
|
2307
2378
|
if (this.refreshPromise) return this.refreshPromise;
|
|
2308
|
-
this.refreshPromise =
|
|
2379
|
+
this.refreshPromise = (async () => {
|
|
2380
|
+
const outcome = await this._doRefresh(refreshToken);
|
|
2381
|
+
if (outcome.ok) return outcome.session;
|
|
2382
|
+
if (outcome.terminal) {
|
|
2383
|
+
await this.handleTerminalRefreshFailure();
|
|
2384
|
+
} else {
|
|
2385
|
+
this.emit("TOKEN_REFRESH_FAILED", this.currentSession);
|
|
2386
|
+
}
|
|
2387
|
+
return null;
|
|
2388
|
+
})().finally(() => {
|
|
2309
2389
|
this.refreshPromise = null;
|
|
2310
2390
|
});
|
|
2311
2391
|
return this.refreshPromise;
|
|
2312
2392
|
}
|
|
2393
|
+
/** Clear the session locally and emit SIGNED_OUT after a definitively-rejected
|
|
2394
|
+
* refresh, so onAuthStateChange-driven UI redirects to login. */
|
|
2395
|
+
async handleTerminalRefreshFailure() {
|
|
2396
|
+
this.sessionEpoch++;
|
|
2397
|
+
this.currentSession = null;
|
|
2398
|
+
this.clearRefreshTimer();
|
|
2399
|
+
if (this.persistSession) {
|
|
2400
|
+
try {
|
|
2401
|
+
await this.storage.removeItem(this.storageKey);
|
|
2402
|
+
} catch (e) {
|
|
2403
|
+
this.log("Failed to clear session after terminal refresh failure", e);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
this.broadcast?.postSessionRemoved();
|
|
2407
|
+
this.emit("TOKEN_REFRESH_FAILED", null);
|
|
2408
|
+
this.emit("SIGNED_OUT", null);
|
|
2409
|
+
}
|
|
2313
2410
|
async _doRefresh(refreshToken) {
|
|
2411
|
+
let raw;
|
|
2412
|
+
try {
|
|
2413
|
+
raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
|
|
2414
|
+
} catch (e) {
|
|
2415
|
+
const status = e instanceof RagableError ? e.status : 0;
|
|
2416
|
+
const terminal = status === 400 || status === 401 || status === 403;
|
|
2417
|
+
this.log("Refresh request failed", { status, terminal });
|
|
2418
|
+
return { ok: false, terminal, error: e };
|
|
2419
|
+
}
|
|
2314
2420
|
try {
|
|
2315
|
-
const raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
|
|
2316
2421
|
const me = await this.fetchAuthWithBearer("/me", "GET", raw.accessToken);
|
|
2317
2422
|
const expiresIn = parseExpiresInSeconds(raw.expiresIn);
|
|
2318
2423
|
const session = {
|
|
@@ -2324,10 +2429,10 @@ var RagableAuth = class {
|
|
|
2324
2429
|
user: me.user
|
|
2325
2430
|
};
|
|
2326
2431
|
await this.setSessionInternal(session, "TOKEN_REFRESHED");
|
|
2327
|
-
return session;
|
|
2432
|
+
return { ok: true, session };
|
|
2328
2433
|
} catch (e) {
|
|
2329
|
-
this.log("
|
|
2330
|
-
return
|
|
2434
|
+
this.log("Post-refresh /me failed", e);
|
|
2435
|
+
return { ok: false, terminal: false, error: e };
|
|
2331
2436
|
}
|
|
2332
2437
|
}
|
|
2333
2438
|
// ─── Visibility listener ───────────────────────────────────────────────────
|
|
@@ -3104,6 +3209,8 @@ function normalizeBrowserApiBase() {
|
|
|
3104
3209
|
function effectiveDataAuth(options) {
|
|
3105
3210
|
if (options.dataAuth) return options.dataAuth;
|
|
3106
3211
|
const hasStatic = Boolean(options.dataStaticKey?.trim()) || typeof options.getDataStaticKey === "function";
|
|
3212
|
+
const canUserAuth = Boolean(options.authGroupId?.trim()) || typeof options.getAccessToken === "function";
|
|
3213
|
+
if (hasStatic && canUserAuth) return "auto";
|
|
3107
3214
|
if (hasStatic) return "publicAnon";
|
|
3108
3215
|
return "user";
|
|
3109
3216
|
}
|
|
@@ -3118,16 +3225,26 @@ function requireAuthGroupId(options) {
|
|
|
3118
3225
|
}
|
|
3119
3226
|
return id;
|
|
3120
3227
|
}
|
|
3121
|
-
async function
|
|
3228
|
+
async function tryGetUserAccessToken(options, ragableAuth) {
|
|
3122
3229
|
if (ragableAuth) {
|
|
3123
|
-
const token = await ragableAuth.getValidAccessToken();
|
|
3124
|
-
if (token) return token;
|
|
3230
|
+
const token = await ragableAuth.getValidAccessToken().catch(() => null);
|
|
3231
|
+
if (token?.trim()) return token.trim();
|
|
3125
3232
|
}
|
|
3126
3233
|
const getter = options.getAccessToken;
|
|
3127
3234
|
if (getter) {
|
|
3128
3235
|
const token = await getter();
|
|
3129
3236
|
if (token?.trim()) return token.trim();
|
|
3130
3237
|
}
|
|
3238
|
+
return null;
|
|
3239
|
+
}
|
|
3240
|
+
async function resolveStaticDataKey(options) {
|
|
3241
|
+
const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
|
|
3242
|
+
const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
|
|
3243
|
+
return key || null;
|
|
3244
|
+
}
|
|
3245
|
+
async function requireAccessToken(options, ragableAuth) {
|
|
3246
|
+
const token = await tryGetUserAccessToken(options, ragableAuth);
|
|
3247
|
+
if (token) return token;
|
|
3131
3248
|
throw new RagableError(
|
|
3132
3249
|
"No access token available. Sign in first with auth.signInWithPassword() or provide getAccessToken callback.",
|
|
3133
3250
|
401,
|
|
@@ -3139,8 +3256,18 @@ async function resolveDatabaseAuthBearer(options, ragableAuth) {
|
|
|
3139
3256
|
if (mode === "user") {
|
|
3140
3257
|
return requireAccessToken(options, ragableAuth);
|
|
3141
3258
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3259
|
+
if (mode === "auto") {
|
|
3260
|
+
const userTok = await tryGetUserAccessToken(options, ragableAuth);
|
|
3261
|
+
if (userTok) return userTok;
|
|
3262
|
+
const key2 = await resolveStaticDataKey(options);
|
|
3263
|
+
if (key2) return key2;
|
|
3264
|
+
throw new RagableError(
|
|
3265
|
+
"No access token or data key available. Sign in with auth.signInWithPassword() or configure dataStaticKey.",
|
|
3266
|
+
401,
|
|
3267
|
+
{ code: "SDK_NO_ACCESS_TOKEN" }
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
const key = await resolveStaticDataKey(options);
|
|
3144
3271
|
if (!key) {
|
|
3145
3272
|
throw new RagableError(
|
|
3146
3273
|
mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey" : "dataAuth admin requires getDataStaticKey or dataStaticKey",
|
|
@@ -3244,6 +3371,14 @@ var RagableBrowserAuthClient = class {
|
|
|
3244
3371
|
getSession() {
|
|
3245
3372
|
return this.auth.getSession();
|
|
3246
3373
|
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Returns a valid (auto-refreshed) access token for the current session, or
|
|
3376
|
+
* `null` if signed out. The sanctioned way to obtain a token for a hand-rolled
|
|
3377
|
+
* `fetch` to a custom endpoint — never read tokens out of storage yourself.
|
|
3378
|
+
*/
|
|
3379
|
+
getValidAccessToken() {
|
|
3380
|
+
return this.ragableAuth ? this.ragableAuth.getValidAccessToken() : Promise.resolve(null);
|
|
3381
|
+
}
|
|
3247
3382
|
};
|
|
3248
3383
|
function collectionRecordToRowWithMeta(record) {
|
|
3249
3384
|
const { data, id, createdAt, updatedAt } = record;
|
|
@@ -3282,13 +3417,11 @@ var BrowserCollectionApi = class {
|
|
|
3282
3417
|
const { returnMode, body } = this.normalizeFindArgs(whereOrParams);
|
|
3283
3418
|
const res = await this.requestFind(body);
|
|
3284
3419
|
if (res.error) return res;
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
}
|
|
3291
|
-
return res;
|
|
3420
|
+
const data = returnMode === "flat" ? collectionRecordsToRowWithMeta(res.data) : res.data;
|
|
3421
|
+
return {
|
|
3422
|
+
data,
|
|
3423
|
+
error: null
|
|
3424
|
+
};
|
|
3292
3425
|
});
|
|
3293
3426
|
/**
|
|
3294
3427
|
* @deprecated Use {@link BrowserCollectionApi.findMany} — same behavior.
|
|
@@ -3477,7 +3610,16 @@ var RagableBrowserDatabaseClient = class {
|
|
|
3477
3610
|
const headers = this.baseHeaders();
|
|
3478
3611
|
headers.set("Authorization", `Bearer ${token}`);
|
|
3479
3612
|
headers.set("Content-Type", "application/json");
|
|
3480
|
-
const
|
|
3613
|
+
const dataMode = effectiveDataAuth(this.options);
|
|
3614
|
+
let readOnly;
|
|
3615
|
+
if (dataMode === "publicAnon") {
|
|
3616
|
+
readOnly = true;
|
|
3617
|
+
} else if (dataMode === "auto") {
|
|
3618
|
+
const userTok = await tryGetUserAccessToken(this.options, this.ragableAuth);
|
|
3619
|
+
readOnly = userTok ? params.readOnly !== false : true;
|
|
3620
|
+
} else {
|
|
3621
|
+
readOnly = params.readOnly !== false;
|
|
3622
|
+
}
|
|
3481
3623
|
const response = await this.fetchImpl(
|
|
3482
3624
|
this.toUrl(`/auth-groups/${gid}/data/query`),
|
|
3483
3625
|
{
|
|
@@ -3770,10 +3912,11 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
3770
3912
|
return subscription;
|
|
3771
3913
|
}
|
|
3772
3914
|
var BrowserStorageBucketClient = class {
|
|
3773
|
-
constructor(options, fetchImpl, bucketId) {
|
|
3915
|
+
constructor(options, fetchImpl, bucketId, ragableAuth = null) {
|
|
3774
3916
|
this.options = options;
|
|
3775
3917
|
this.fetchImpl = fetchImpl;
|
|
3776
3918
|
this.bucketId = bucketId;
|
|
3919
|
+
this.ragableAuth = ragableAuth;
|
|
3777
3920
|
}
|
|
3778
3921
|
get authGroupId() {
|
|
3779
3922
|
const id = this.options.authGroupId?.trim();
|
|
@@ -3783,14 +3926,28 @@ var BrowserStorageBucketClient = class {
|
|
|
3783
3926
|
base() {
|
|
3784
3927
|
return `${normalizeBrowserApiBase()}/auth-groups/${this.authGroupId}/storage/buckets/${encodeURIComponent(this.bucketId)}`;
|
|
3785
3928
|
}
|
|
3929
|
+
/**
|
|
3930
|
+
* Same credential resolution as the database client (see resolveDatabaseAuthBearer):
|
|
3931
|
+
* in the generated-site default (`auto`), a signed-in user's auto-refreshed JWT
|
|
3932
|
+
* is used so storage calls carry the user's identity; logged-out visitors fall
|
|
3933
|
+
* back to the anon key. Previously storage ignored the managed session entirely.
|
|
3934
|
+
*/
|
|
3786
3935
|
async bearerToken() {
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3936
|
+
return resolveDatabaseAuthBearer(this.options, this.ragableAuth);
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* The storage backend has historically returned HTTP 200 with an `{ error }`
|
|
3940
|
+
* body on some failures; without this guard the SDK would resolve those as
|
|
3941
|
+
* successful uploads/deletes. Treat any 2xx whose body carries a non-empty
|
|
3942
|
+
* `error` as a failure.
|
|
3943
|
+
*/
|
|
3944
|
+
assertNoEmbeddedError(payload, status) {
|
|
3945
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
3946
|
+
const err = payload.error;
|
|
3947
|
+
if (typeof err === "string" && err.trim()) {
|
|
3948
|
+
throw new RagableError(err, status, payload);
|
|
3949
|
+
}
|
|
3792
3950
|
}
|
|
3793
|
-
throw new RagableError("No auth token for storage. Provide dataStaticKey or getAccessToken.", 401, { code: "SDK_NO_ACCESS_TOKEN" });
|
|
3794
3951
|
}
|
|
3795
3952
|
async req(method, path, body) {
|
|
3796
3953
|
const token = await this.bearerToken();
|
|
@@ -3803,7 +3960,8 @@ var BrowserStorageBucketClient = class {
|
|
|
3803
3960
|
body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
|
|
3804
3961
|
});
|
|
3805
3962
|
const payload = await res.json().catch(() => ({}));
|
|
3806
|
-
if (!res.ok) throw new RagableError(payload
|
|
3963
|
+
if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
|
|
3964
|
+
this.assertNoEmbeddedError(payload, res.status);
|
|
3807
3965
|
return payload;
|
|
3808
3966
|
}
|
|
3809
3967
|
list(params = {}) {
|
|
@@ -3827,7 +3985,8 @@ var BrowserStorageBucketClient = class {
|
|
|
3827
3985
|
if (params.cacheControl) form.set("cacheControl", params.cacheControl);
|
|
3828
3986
|
const res = await this.fetchImpl(`${this.base()}/upload`, { method: "POST", headers, body: form });
|
|
3829
3987
|
const payload = await res.json().catch(() => ({}));
|
|
3830
|
-
if (!res.ok) throw new RagableError(payload
|
|
3988
|
+
if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
|
|
3989
|
+
this.assertNoEmbeddedError(payload, res.status);
|
|
3831
3990
|
return payload;
|
|
3832
3991
|
}
|
|
3833
3992
|
download(params) {
|
|
@@ -3868,12 +4027,18 @@ var BrowserStorageBucketClient = class {
|
|
|
3868
4027
|
}
|
|
3869
4028
|
};
|
|
3870
4029
|
var RagableBrowserStorageClient = class {
|
|
3871
|
-
constructor(options, fetchImpl) {
|
|
4030
|
+
constructor(options, fetchImpl, ragableAuth = null) {
|
|
3872
4031
|
this.options = options;
|
|
3873
4032
|
this.fetchImpl = fetchImpl;
|
|
4033
|
+
this.ragableAuth = ragableAuth;
|
|
3874
4034
|
}
|
|
3875
4035
|
from(bucketId) {
|
|
3876
|
-
return new BrowserStorageBucketClient(
|
|
4036
|
+
return new BrowserStorageBucketClient(
|
|
4037
|
+
this.options,
|
|
4038
|
+
this.fetchImpl,
|
|
4039
|
+
bucketId,
|
|
4040
|
+
this.ragableAuth
|
|
4041
|
+
);
|
|
3877
4042
|
}
|
|
3878
4043
|
};
|
|
3879
4044
|
var RagableBrowserMailClient = class {
|
|
@@ -4434,13 +4599,12 @@ var RagableBrowser = class {
|
|
|
4434
4599
|
auth: options.auth
|
|
4435
4600
|
});
|
|
4436
4601
|
this.transport.setRefreshHandler(async () => {
|
|
4437
|
-
|
|
4438
|
-
|
|
4602
|
+
const mode = effectiveDataAuth(options);
|
|
4603
|
+
if (mode !== "user" && mode !== "auto") return null;
|
|
4604
|
+
return this._ragableAuth.getValidAccessToken(true).catch(() => null);
|
|
4605
|
+
});
|
|
4606
|
+
this._ragableAuth.initialize().catch(() => {
|
|
4439
4607
|
});
|
|
4440
|
-
if (!options.getAccessToken && effectiveDataAuth(options) === "user") {
|
|
4441
|
-
this._ragableAuth.initialize().catch(() => {
|
|
4442
|
-
});
|
|
4443
|
-
}
|
|
4444
4608
|
} else {
|
|
4445
4609
|
this._ragableAuth = null;
|
|
4446
4610
|
}
|
|
@@ -4459,13 +4623,26 @@ var RagableBrowser = class {
|
|
|
4459
4623
|
);
|
|
4460
4624
|
this.database._setTransport(this.transport);
|
|
4461
4625
|
this.db = this.database;
|
|
4462
|
-
this.storage = new RagableBrowserStorageClient(
|
|
4626
|
+
this.storage = new RagableBrowserStorageClient(
|
|
4627
|
+
options,
|
|
4628
|
+
bindFetch(options.fetch),
|
|
4629
|
+
this._ragableAuth
|
|
4630
|
+
);
|
|
4463
4631
|
this.mail = new RagableBrowserMailClient(options, this._ragableAuth);
|
|
4464
4632
|
this.functions = new RagableBrowserFunctionsClient(
|
|
4465
4633
|
options,
|
|
4466
4634
|
this._ragableAuth
|
|
4467
4635
|
).asInvoker();
|
|
4468
4636
|
}
|
|
4637
|
+
/**
|
|
4638
|
+
* Resolves once the persisted session has been restored (and refreshed if it
|
|
4639
|
+
* was expired). Await this before reading auth state at startup to avoid a
|
|
4640
|
+
* logged-out flash, e.g. `const session = await client.ready()`. Resolves
|
|
4641
|
+
* `null` when no auth group is configured or no session is stored.
|
|
4642
|
+
*/
|
|
4643
|
+
ready() {
|
|
4644
|
+
return this._ragableAuth ? this._ragableAuth.initialize() : Promise.resolve(null);
|
|
4645
|
+
}
|
|
4469
4646
|
destroy() {
|
|
4470
4647
|
this._ragableAuth?.destroy();
|
|
4471
4648
|
}
|
|
@@ -4475,6 +4652,91 @@ function createBrowserClient(options) {
|
|
|
4475
4652
|
}
|
|
4476
4653
|
var createRagableBrowserClient = createBrowserClient;
|
|
4477
4654
|
|
|
4655
|
+
// src/server.ts
|
|
4656
|
+
function optionsForPrivilege(config, privilege) {
|
|
4657
|
+
const base = {
|
|
4658
|
+
organizationId: config.organizationId,
|
|
4659
|
+
...config.websiteId !== void 0 ? { websiteId: config.websiteId } : {},
|
|
4660
|
+
...config.authGroupId !== void 0 ? { authGroupId: config.authGroupId } : {},
|
|
4661
|
+
...config.databaseInstanceId !== void 0 ? { databaseInstanceId: config.databaseInstanceId } : {},
|
|
4662
|
+
...config.fetch !== void 0 ? { fetch: config.fetch } : {},
|
|
4663
|
+
...config.headers !== void 0 ? { headers: config.headers } : {}
|
|
4664
|
+
};
|
|
4665
|
+
if (privilege === "admin") {
|
|
4666
|
+
const key = config.adminKey?.trim();
|
|
4667
|
+
if (!key) {
|
|
4668
|
+
throw new RagableError(
|
|
4669
|
+
"Admin privilege requires `adminKey` (the auth group's data-admin key). It is not available \u2014 admin functions may be disabled for this project.",
|
|
4670
|
+
403,
|
|
4671
|
+
{ code: "SDK_ADMIN_KEY_REQUIRED" }
|
|
4672
|
+
);
|
|
4673
|
+
}
|
|
4674
|
+
return { ...base, dataAuth: "admin", dataStaticKey: key };
|
|
4675
|
+
}
|
|
4676
|
+
return {
|
|
4677
|
+
...base,
|
|
4678
|
+
dataAuth: "auto",
|
|
4679
|
+
getAccessToken: () => config.callerToken ?? null,
|
|
4680
|
+
...config.publicAnonKey !== void 0 ? { dataStaticKey: config.publicAnonKey } : {}
|
|
4681
|
+
};
|
|
4682
|
+
}
|
|
4683
|
+
var RagableServerClient = class _RagableServerClient {
|
|
4684
|
+
constructor(config, privilege) {
|
|
4685
|
+
this.config = config;
|
|
4686
|
+
__publicField(this, "database");
|
|
4687
|
+
__publicField(this, "db");
|
|
4688
|
+
__publicField(this, "storage");
|
|
4689
|
+
__publicField(this, "mail");
|
|
4690
|
+
__publicField(this, "functions");
|
|
4691
|
+
__publicField(this, "ai");
|
|
4692
|
+
__publicField(this, "agents");
|
|
4693
|
+
/** Which credential this client is using. */
|
|
4694
|
+
__publicField(this, "privilege");
|
|
4695
|
+
this.privilege = privilege;
|
|
4696
|
+
const options = optionsForPrivilege(config, privilege);
|
|
4697
|
+
const transport = new Transport({
|
|
4698
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
4699
|
+
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
4700
|
+
...options.transport
|
|
4701
|
+
});
|
|
4702
|
+
this.database = new RagableBrowserDatabaseClient(options, null);
|
|
4703
|
+
this.database._setTransport(transport);
|
|
4704
|
+
this.db = this.database;
|
|
4705
|
+
this.storage = new RagableBrowserStorageClient(options, bindFetch(options.fetch), null);
|
|
4706
|
+
this.mail = new RagableBrowserMailClient(options, null);
|
|
4707
|
+
this.functions = new RagableBrowserFunctionsClient(options, null).asInvoker();
|
|
4708
|
+
this.ai = new RagableBrowserAiClient({
|
|
4709
|
+
organizationId: options.organizationId,
|
|
4710
|
+
...options.websiteId !== void 0 ? { websiteId: options.websiteId } : {},
|
|
4711
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
4712
|
+
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
4713
|
+
apiBase: normalizeBrowserApiBase()
|
|
4714
|
+
});
|
|
4715
|
+
this.agents = new RagableBrowserAgentsClient(options);
|
|
4716
|
+
}
|
|
4717
|
+
/**
|
|
4718
|
+
* A client authenticated with the **data-admin key** — bypasses collection
|
|
4719
|
+
* security (owner/group/claim grants do not apply) and can write protected
|
|
4720
|
+
* collections. Throws `SDK_ADMIN_KEY_REQUIRED` when no admin key is configured.
|
|
4721
|
+
*/
|
|
4722
|
+
asAdmin() {
|
|
4723
|
+
return new _RagableServerClient(this.config, "admin");
|
|
4724
|
+
}
|
|
4725
|
+
/**
|
|
4726
|
+
* A client scoped to the invoking end user's identity (respects collection
|
|
4727
|
+
* security). This is already the default; use it to drop back from `asAdmin()`.
|
|
4728
|
+
*/
|
|
4729
|
+
asCaller() {
|
|
4730
|
+
return new _RagableServerClient(this.config, "caller");
|
|
4731
|
+
}
|
|
4732
|
+
};
|
|
4733
|
+
function createServerClient(config) {
|
|
4734
|
+
return new RagableServerClient(
|
|
4735
|
+
config,
|
|
4736
|
+
config.defaultPrivilege ?? "caller"
|
|
4737
|
+
);
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4478
4740
|
// src/index.ts
|
|
4479
4741
|
function createClient(options) {
|
|
4480
4742
|
return createBrowserClient(options);
|
|
@@ -4512,6 +4774,7 @@ function createClient(options) {
|
|
|
4512
4774
|
RagableError,
|
|
4513
4775
|
RagableNetworkError,
|
|
4514
4776
|
RagableSdkError,
|
|
4777
|
+
RagableServerClient,
|
|
4515
4778
|
RagableTimeoutError,
|
|
4516
4779
|
SessionStorageAdapter,
|
|
4517
4780
|
Transport,
|
|
@@ -4526,6 +4789,7 @@ function createClient(options) {
|
|
|
4526
4789
|
createBrowserClient,
|
|
4527
4790
|
createClient,
|
|
4528
4791
|
createRagableBrowserClient,
|
|
4792
|
+
createServerClient,
|
|
4529
4793
|
createStreamResultFromParts,
|
|
4530
4794
|
detectStorage,
|
|
4531
4795
|
effectiveDataAuth,
|