@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.mjs
CHANGED
|
@@ -666,7 +666,11 @@ function generateIdempotencyKey() {
|
|
|
666
666
|
return `idk-${Date.now()}-${_uuidCounter}-${Math.random().toString(36).slice(2, 10)}`;
|
|
667
667
|
}
|
|
668
668
|
function requestCacheKey(req) {
|
|
669
|
-
|
|
669
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
670
|
+
const dbInstance = req.headers.get("x-database-instance-id") ?? "";
|
|
671
|
+
return `${req.method}:${req.url}
|
|
672
|
+
${auth}
|
|
673
|
+
${dbInstance}`;
|
|
670
674
|
}
|
|
671
675
|
var Transport = class {
|
|
672
676
|
constructor(options = {}) {
|
|
@@ -693,13 +697,16 @@ var Transport = class {
|
|
|
693
697
|
async execute(req) {
|
|
694
698
|
if (req.method === "GET") {
|
|
695
699
|
const key = requestCacheKey(req);
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
this.inflightGets.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
700
|
+
let base = this.inflightGets.get(key);
|
|
701
|
+
if (!base) {
|
|
702
|
+
base = this._executeWithRetry(req);
|
|
703
|
+
this.inflightGets.set(key, base);
|
|
704
|
+
const clear = () => {
|
|
705
|
+
if (this.inflightGets.get(key) === base) this.inflightGets.delete(key);
|
|
706
|
+
};
|
|
707
|
+
base.then(clear, clear);
|
|
708
|
+
}
|
|
709
|
+
return base.then((r) => r.clone());
|
|
703
710
|
}
|
|
704
711
|
return this._executeWithRetry(req);
|
|
705
712
|
}
|
|
@@ -719,6 +726,8 @@ var Transport = class {
|
|
|
719
726
|
let lastError;
|
|
720
727
|
const maxAttempts = 1 + retryOpts.maxRetries;
|
|
721
728
|
let did401Refresh = false;
|
|
729
|
+
const isIdempotent = req.method === "GET" || req.method === "HEAD";
|
|
730
|
+
const retryEnabled = retryOpts.maxRetries > 0 && (isIdempotent || req.retry !== void 0);
|
|
722
731
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
723
732
|
try {
|
|
724
733
|
const response = await this._singleFetch(finalReq, timeoutMs);
|
|
@@ -731,7 +740,7 @@ var Transport = class {
|
|
|
731
740
|
continue;
|
|
732
741
|
}
|
|
733
742
|
}
|
|
734
|
-
if (!response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
|
|
743
|
+
if (retryEnabled && !response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
|
|
735
744
|
let delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
|
|
736
745
|
if (retryOpts.respectRetryAfter) {
|
|
737
746
|
const ra = parseRetryAfter(response.headers.get("retry-after"));
|
|
@@ -747,7 +756,7 @@ var Transport = class {
|
|
|
747
756
|
throw e;
|
|
748
757
|
}
|
|
749
758
|
lastError = e;
|
|
750
|
-
if (attempt < maxAttempts - 1) {
|
|
759
|
+
if (retryEnabled && attempt < maxAttempts - 1) {
|
|
751
760
|
const delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
|
|
752
761
|
this.onRetry?.(finalReq, attempt + 1, delayMs, e.message);
|
|
753
762
|
await sleep(delayMs);
|
|
@@ -879,13 +888,29 @@ function extractPostgRESTErrorMessage(payload, status, statusText) {
|
|
|
879
888
|
return msg;
|
|
880
889
|
}
|
|
881
890
|
async function parsePostgRESTResponse(response) {
|
|
882
|
-
if (response.status === 204) return null;
|
|
891
|
+
if (response.status === 204 && response.ok) return null;
|
|
883
892
|
const text = await response.text();
|
|
884
|
-
if (!text)
|
|
893
|
+
if (!text) {
|
|
894
|
+
if (!response.ok) {
|
|
895
|
+
throw new RagableError(
|
|
896
|
+
response.statusText || `HTTP ${response.status}`,
|
|
897
|
+
response.status,
|
|
898
|
+
null
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
885
903
|
let payload;
|
|
886
904
|
try {
|
|
887
905
|
payload = JSON.parse(text);
|
|
888
906
|
} catch {
|
|
907
|
+
if (!response.ok) {
|
|
908
|
+
throw new RagableError(
|
|
909
|
+
extractPostgRESTErrorMessage(text, response.status, response.statusText),
|
|
910
|
+
response.status,
|
|
911
|
+
null
|
|
912
|
+
);
|
|
913
|
+
}
|
|
889
914
|
throw new RagableError(
|
|
890
915
|
`PostgREST response parse error: ${text.slice(0, 200)}`,
|
|
891
916
|
response.status,
|
|
@@ -1854,6 +1879,7 @@ var AuthBroadcastChannel = class {
|
|
|
1854
1879
|
};
|
|
1855
1880
|
|
|
1856
1881
|
// src/auth.ts
|
|
1882
|
+
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
1857
1883
|
function parseExpiresInSeconds(raw) {
|
|
1858
1884
|
if (typeof raw === "number") return raw;
|
|
1859
1885
|
const s = raw.trim().toLowerCase();
|
|
@@ -1900,7 +1926,14 @@ var RagableAuth = class {
|
|
|
1900
1926
|
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
1901
1927
|
__publicField(this, "broadcast", null);
|
|
1902
1928
|
__publicField(this, "visibilityHandler", null);
|
|
1903
|
-
|
|
1929
|
+
/** Memoizes the one-shot restore so concurrent callers (constructor eager init,
|
|
1930
|
+
* every `onAuthStateChange` subscriber, `getSession`) share a single result.
|
|
1931
|
+
* Non-null also means "restore has started", replacing the old boolean flag. */
|
|
1932
|
+
__publicField(this, "initializePromise", null);
|
|
1933
|
+
/** Bumped on every explicit session change (sign-in/out, refresh). The async
|
|
1934
|
+
* restore captures this and refuses to overwrite a newer session op that
|
|
1935
|
+
* landed while it was reading storage (e.g. a sign-out during page load). */
|
|
1936
|
+
__publicField(this, "sessionEpoch", 0);
|
|
1904
1937
|
this.baseUrl = DEFAULT_RAGABLE_API_BASE.replace(/\/+$/, "");
|
|
1905
1938
|
this.authGroupId = config.authGroupId;
|
|
1906
1939
|
this.fetchImpl = bindFetch(config.fetch);
|
|
@@ -1934,33 +1967,43 @@ var RagableAuth = class {
|
|
|
1934
1967
|
if (this.debug) console.debug("[RagableAuth]", ...args);
|
|
1935
1968
|
}
|
|
1936
1969
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
this.
|
|
1970
|
+
/**
|
|
1971
|
+
* Restore a persisted session (once). Memoized: every caller awaits the same
|
|
1972
|
+
* promise, so the eager constructor init, `getSession`, and each
|
|
1973
|
+
* `onAuthStateChange` subscriber's INITIAL_SESSION replay never race or
|
|
1974
|
+
* double-restore. Does NOT emit `INITIAL_SESSION` globally — that event is
|
|
1975
|
+
* delivered per-subscriber by `onAuthStateChange` (Supabase-parity), so a
|
|
1976
|
+
* listener attached after restore still sees the existing session.
|
|
1977
|
+
*/
|
|
1978
|
+
initialize() {
|
|
1979
|
+
if (this.initializePromise) return this.initializePromise;
|
|
1980
|
+
this.initializePromise = this._initialize();
|
|
1981
|
+
return this.initializePromise;
|
|
1982
|
+
}
|
|
1983
|
+
async _initialize() {
|
|
1984
|
+
if (!this.persistSession) return this.currentSession;
|
|
1985
|
+
const epoch = this.sessionEpoch;
|
|
1986
|
+
try {
|
|
1987
|
+
const raw = await this.storage.getItem(this.storageKey);
|
|
1988
|
+
if (!raw) return this.currentSession;
|
|
1989
|
+
if (this.sessionEpoch !== epoch) return this.currentSession;
|
|
1990
|
+
const session = JSON.parse(raw);
|
|
1991
|
+
const stillValid = !!session.expires_at && session.expires_at > nowSeconds();
|
|
1992
|
+
if (stillValid) {
|
|
1993
|
+
this.currentSession = session;
|
|
1994
|
+
this.scheduleRefresh(session);
|
|
1995
|
+
this.log("Restored session from storage");
|
|
1996
|
+
} else if (session.refresh_token) {
|
|
1997
|
+
this.currentSession = session;
|
|
1998
|
+
this.log("Stored session expired, attempting refresh");
|
|
1999
|
+
const refreshed = await this.singleFlightRefresh(session.refresh_token);
|
|
2000
|
+
if (refreshed) this.currentSession = refreshed;
|
|
2001
|
+
} else {
|
|
2002
|
+
await this.storage.removeItem(this.storageKey);
|
|
1961
2003
|
}
|
|
2004
|
+
} catch (e) {
|
|
2005
|
+
this.log("Failed to restore session", e);
|
|
1962
2006
|
}
|
|
1963
|
-
this.emit("INITIAL_SESSION", this.currentSession);
|
|
1964
2007
|
return this.currentSession;
|
|
1965
2008
|
}
|
|
1966
2009
|
// ── Auth methods ───────────────────────────────────────────────────────────
|
|
@@ -1990,6 +2033,7 @@ var RagableAuth = class {
|
|
|
1990
2033
|
});
|
|
1991
2034
|
}
|
|
1992
2035
|
async signOut(_options) {
|
|
2036
|
+
this.sessionEpoch++;
|
|
1993
2037
|
this.currentSession = null;
|
|
1994
2038
|
this.clearRefreshTimer();
|
|
1995
2039
|
if (this.persistSession) {
|
|
@@ -2009,12 +2053,12 @@ var RagableAuth = class {
|
|
|
2009
2053
|
});
|
|
2010
2054
|
}
|
|
2011
2055
|
async getSession() {
|
|
2012
|
-
|
|
2056
|
+
await this.initialize();
|
|
2013
2057
|
return { data: { session: this.currentSession }, error: null };
|
|
2014
2058
|
}
|
|
2015
2059
|
async getUser() {
|
|
2016
2060
|
return asPostgrestResponse(async () => {
|
|
2017
|
-
const token = this.
|
|
2061
|
+
const token = await this.getValidAccessToken();
|
|
2018
2062
|
if (!token) throw new RagableError("Not authenticated", 401, null);
|
|
2019
2063
|
return this.fetchAuthWithBearer("/me", "GET", token);
|
|
2020
2064
|
});
|
|
@@ -2059,6 +2103,15 @@ var RagableAuth = class {
|
|
|
2059
2103
|
_subCounter++;
|
|
2060
2104
|
const id = `sub-${_subCounter}`;
|
|
2061
2105
|
this.listeners.set(id, callback);
|
|
2106
|
+
void this.initialize().then((session) => {
|
|
2107
|
+
const cb = this.listeners.get(id);
|
|
2108
|
+
if (!cb) return;
|
|
2109
|
+
try {
|
|
2110
|
+
cb("INITIAL_SESSION", session);
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
this.log("Listener threw", e);
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2062
2115
|
const unsubscribe = () => {
|
|
2063
2116
|
this.listeners.delete(id);
|
|
2064
2117
|
};
|
|
@@ -2068,12 +2121,19 @@ var RagableAuth = class {
|
|
|
2068
2121
|
getAccessToken() {
|
|
2069
2122
|
return this.currentSession?.access_token ?? null;
|
|
2070
2123
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2124
|
+
/**
|
|
2125
|
+
* Returns an access token guaranteed fresh for at least `refreshSkewSeconds`,
|
|
2126
|
+
* refreshing (single-flight) if needed. Pass `force: true` to bypass the skew
|
|
2127
|
+
* check and refresh now — used by the transport's 401 handler so a token the
|
|
2128
|
+
* server rejected (key rotation, clock skew, early revocation) self-heals on
|
|
2129
|
+
* retry instead of failing the call.
|
|
2130
|
+
*/
|
|
2131
|
+
async getValidAccessToken(force = false) {
|
|
2132
|
+
await this.initialize();
|
|
2073
2133
|
const session = this.currentSession;
|
|
2074
2134
|
if (!session) return null;
|
|
2075
2135
|
const secondsUntilExpiry = session.expires_at - nowSeconds();
|
|
2076
|
-
if (secondsUntilExpiry <= this.refreshSkewSeconds) {
|
|
2136
|
+
if (force || secondsUntilExpiry <= this.refreshSkewSeconds) {
|
|
2077
2137
|
const refreshed = await this.singleFlightRefresh(session.refresh_token);
|
|
2078
2138
|
return refreshed?.access_token ?? null;
|
|
2079
2139
|
}
|
|
@@ -2166,6 +2226,7 @@ var RagableAuth = class {
|
|
|
2166
2226
|
};
|
|
2167
2227
|
}
|
|
2168
2228
|
async setSessionInternal(session, event) {
|
|
2229
|
+
this.sessionEpoch++;
|
|
2169
2230
|
this.currentSession = session;
|
|
2170
2231
|
await this.persistCurrentSession();
|
|
2171
2232
|
this.broadcast?.postSessionUpdated(JSON.stringify(session));
|
|
@@ -2196,12 +2257,13 @@ var RagableAuth = class {
|
|
|
2196
2257
|
if (!this.autoRefreshToken) return;
|
|
2197
2258
|
const secondsUntilExpiry = session.expires_at - nowSeconds();
|
|
2198
2259
|
const refreshIn = Math.max(0, secondsUntilExpiry - this.refreshSkewSeconds);
|
|
2199
|
-
|
|
2260
|
+
const delayMs = Math.min(refreshIn * 1e3, MAX_TIMEOUT_MS);
|
|
2261
|
+
this.log(`Scheduling refresh in ${Math.round(delayMs / 1e3)}s`);
|
|
2200
2262
|
this.refreshTimer = setTimeout(() => {
|
|
2201
2263
|
this.singleFlightRefresh(session.refresh_token).catch((e) => {
|
|
2202
2264
|
this.log("Scheduled refresh failed", e);
|
|
2203
2265
|
});
|
|
2204
|
-
},
|
|
2266
|
+
}, delayMs);
|
|
2205
2267
|
}
|
|
2206
2268
|
clearRefreshTimer() {
|
|
2207
2269
|
if (this.refreshTimer !== null) {
|
|
@@ -2209,16 +2271,57 @@ var RagableAuth = class {
|
|
|
2209
2271
|
this.refreshTimer = null;
|
|
2210
2272
|
}
|
|
2211
2273
|
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Refresh the session, deduplicating concurrent callers onto one in-flight
|
|
2276
|
+
* request. Side effects (persisting the new session, or clearing it and
|
|
2277
|
+
* emitting SIGNED_OUT / TOKEN_REFRESH_FAILED) run exactly once inside the
|
|
2278
|
+
* shared promise, so two callers can't double-emit. Resolves to the new
|
|
2279
|
+
* session, or `null` when the refresh failed.
|
|
2280
|
+
*/
|
|
2212
2281
|
async singleFlightRefresh(refreshToken) {
|
|
2213
2282
|
if (this.refreshPromise) return this.refreshPromise;
|
|
2214
|
-
this.refreshPromise =
|
|
2283
|
+
this.refreshPromise = (async () => {
|
|
2284
|
+
const outcome = await this._doRefresh(refreshToken);
|
|
2285
|
+
if (outcome.ok) return outcome.session;
|
|
2286
|
+
if (outcome.terminal) {
|
|
2287
|
+
await this.handleTerminalRefreshFailure();
|
|
2288
|
+
} else {
|
|
2289
|
+
this.emit("TOKEN_REFRESH_FAILED", this.currentSession);
|
|
2290
|
+
}
|
|
2291
|
+
return null;
|
|
2292
|
+
})().finally(() => {
|
|
2215
2293
|
this.refreshPromise = null;
|
|
2216
2294
|
});
|
|
2217
2295
|
return this.refreshPromise;
|
|
2218
2296
|
}
|
|
2297
|
+
/** Clear the session locally and emit SIGNED_OUT after a definitively-rejected
|
|
2298
|
+
* refresh, so onAuthStateChange-driven UI redirects to login. */
|
|
2299
|
+
async handleTerminalRefreshFailure() {
|
|
2300
|
+
this.sessionEpoch++;
|
|
2301
|
+
this.currentSession = null;
|
|
2302
|
+
this.clearRefreshTimer();
|
|
2303
|
+
if (this.persistSession) {
|
|
2304
|
+
try {
|
|
2305
|
+
await this.storage.removeItem(this.storageKey);
|
|
2306
|
+
} catch (e) {
|
|
2307
|
+
this.log("Failed to clear session after terminal refresh failure", e);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
this.broadcast?.postSessionRemoved();
|
|
2311
|
+
this.emit("TOKEN_REFRESH_FAILED", null);
|
|
2312
|
+
this.emit("SIGNED_OUT", null);
|
|
2313
|
+
}
|
|
2219
2314
|
async _doRefresh(refreshToken) {
|
|
2315
|
+
let raw;
|
|
2316
|
+
try {
|
|
2317
|
+
raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
|
|
2318
|
+
} catch (e) {
|
|
2319
|
+
const status = e instanceof RagableError ? e.status : 0;
|
|
2320
|
+
const terminal = status === 400 || status === 401 || status === 403;
|
|
2321
|
+
this.log("Refresh request failed", { status, terminal });
|
|
2322
|
+
return { ok: false, terminal, error: e };
|
|
2323
|
+
}
|
|
2220
2324
|
try {
|
|
2221
|
-
const raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
|
|
2222
2325
|
const me = await this.fetchAuthWithBearer("/me", "GET", raw.accessToken);
|
|
2223
2326
|
const expiresIn = parseExpiresInSeconds(raw.expiresIn);
|
|
2224
2327
|
const session = {
|
|
@@ -2230,10 +2333,10 @@ var RagableAuth = class {
|
|
|
2230
2333
|
user: me.user
|
|
2231
2334
|
};
|
|
2232
2335
|
await this.setSessionInternal(session, "TOKEN_REFRESHED");
|
|
2233
|
-
return session;
|
|
2336
|
+
return { ok: true, session };
|
|
2234
2337
|
} catch (e) {
|
|
2235
|
-
this.log("
|
|
2236
|
-
return
|
|
2338
|
+
this.log("Post-refresh /me failed", e);
|
|
2339
|
+
return { ok: false, terminal: false, error: e };
|
|
2237
2340
|
}
|
|
2238
2341
|
}
|
|
2239
2342
|
// ─── Visibility listener ───────────────────────────────────────────────────
|
|
@@ -3010,6 +3113,8 @@ function normalizeBrowserApiBase() {
|
|
|
3010
3113
|
function effectiveDataAuth(options) {
|
|
3011
3114
|
if (options.dataAuth) return options.dataAuth;
|
|
3012
3115
|
const hasStatic = Boolean(options.dataStaticKey?.trim()) || typeof options.getDataStaticKey === "function";
|
|
3116
|
+
const canUserAuth = Boolean(options.authGroupId?.trim()) || typeof options.getAccessToken === "function";
|
|
3117
|
+
if (hasStatic && canUserAuth) return "auto";
|
|
3013
3118
|
if (hasStatic) return "publicAnon";
|
|
3014
3119
|
return "user";
|
|
3015
3120
|
}
|
|
@@ -3024,16 +3129,26 @@ function requireAuthGroupId(options) {
|
|
|
3024
3129
|
}
|
|
3025
3130
|
return id;
|
|
3026
3131
|
}
|
|
3027
|
-
async function
|
|
3132
|
+
async function tryGetUserAccessToken(options, ragableAuth) {
|
|
3028
3133
|
if (ragableAuth) {
|
|
3029
|
-
const token = await ragableAuth.getValidAccessToken();
|
|
3030
|
-
if (token) return token;
|
|
3134
|
+
const token = await ragableAuth.getValidAccessToken().catch(() => null);
|
|
3135
|
+
if (token?.trim()) return token.trim();
|
|
3031
3136
|
}
|
|
3032
3137
|
const getter = options.getAccessToken;
|
|
3033
3138
|
if (getter) {
|
|
3034
3139
|
const token = await getter();
|
|
3035
3140
|
if (token?.trim()) return token.trim();
|
|
3036
3141
|
}
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
async function resolveStaticDataKey(options) {
|
|
3145
|
+
const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
|
|
3146
|
+
const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
|
|
3147
|
+
return key || null;
|
|
3148
|
+
}
|
|
3149
|
+
async function requireAccessToken(options, ragableAuth) {
|
|
3150
|
+
const token = await tryGetUserAccessToken(options, ragableAuth);
|
|
3151
|
+
if (token) return token;
|
|
3037
3152
|
throw new RagableError(
|
|
3038
3153
|
"No access token available. Sign in first with auth.signInWithPassword() or provide getAccessToken callback.",
|
|
3039
3154
|
401,
|
|
@@ -3045,8 +3160,18 @@ async function resolveDatabaseAuthBearer(options, ragableAuth) {
|
|
|
3045
3160
|
if (mode === "user") {
|
|
3046
3161
|
return requireAccessToken(options, ragableAuth);
|
|
3047
3162
|
}
|
|
3048
|
-
|
|
3049
|
-
|
|
3163
|
+
if (mode === "auto") {
|
|
3164
|
+
const userTok = await tryGetUserAccessToken(options, ragableAuth);
|
|
3165
|
+
if (userTok) return userTok;
|
|
3166
|
+
const key2 = await resolveStaticDataKey(options);
|
|
3167
|
+
if (key2) return key2;
|
|
3168
|
+
throw new RagableError(
|
|
3169
|
+
"No access token or data key available. Sign in with auth.signInWithPassword() or configure dataStaticKey.",
|
|
3170
|
+
401,
|
|
3171
|
+
{ code: "SDK_NO_ACCESS_TOKEN" }
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
const key = await resolveStaticDataKey(options);
|
|
3050
3175
|
if (!key) {
|
|
3051
3176
|
throw new RagableError(
|
|
3052
3177
|
mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey" : "dataAuth admin requires getDataStaticKey or dataStaticKey",
|
|
@@ -3150,6 +3275,14 @@ var RagableBrowserAuthClient = class {
|
|
|
3150
3275
|
getSession() {
|
|
3151
3276
|
return this.auth.getSession();
|
|
3152
3277
|
}
|
|
3278
|
+
/**
|
|
3279
|
+
* Returns a valid (auto-refreshed) access token for the current session, or
|
|
3280
|
+
* `null` if signed out. The sanctioned way to obtain a token for a hand-rolled
|
|
3281
|
+
* `fetch` to a custom endpoint — never read tokens out of storage yourself.
|
|
3282
|
+
*/
|
|
3283
|
+
getValidAccessToken() {
|
|
3284
|
+
return this.ragableAuth ? this.ragableAuth.getValidAccessToken() : Promise.resolve(null);
|
|
3285
|
+
}
|
|
3153
3286
|
};
|
|
3154
3287
|
function collectionRecordToRowWithMeta(record) {
|
|
3155
3288
|
const { data, id, createdAt, updatedAt } = record;
|
|
@@ -3188,13 +3321,11 @@ var BrowserCollectionApi = class {
|
|
|
3188
3321
|
const { returnMode, body } = this.normalizeFindArgs(whereOrParams);
|
|
3189
3322
|
const res = await this.requestFind(body);
|
|
3190
3323
|
if (res.error) return res;
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
}
|
|
3197
|
-
return res;
|
|
3324
|
+
const data = returnMode === "flat" ? collectionRecordsToRowWithMeta(res.data) : res.data;
|
|
3325
|
+
return {
|
|
3326
|
+
data,
|
|
3327
|
+
error: null
|
|
3328
|
+
};
|
|
3198
3329
|
});
|
|
3199
3330
|
/**
|
|
3200
3331
|
* @deprecated Use {@link BrowserCollectionApi.findMany} — same behavior.
|
|
@@ -3383,7 +3514,16 @@ var RagableBrowserDatabaseClient = class {
|
|
|
3383
3514
|
const headers = this.baseHeaders();
|
|
3384
3515
|
headers.set("Authorization", `Bearer ${token}`);
|
|
3385
3516
|
headers.set("Content-Type", "application/json");
|
|
3386
|
-
const
|
|
3517
|
+
const dataMode = effectiveDataAuth(this.options);
|
|
3518
|
+
let readOnly;
|
|
3519
|
+
if (dataMode === "publicAnon") {
|
|
3520
|
+
readOnly = true;
|
|
3521
|
+
} else if (dataMode === "auto") {
|
|
3522
|
+
const userTok = await tryGetUserAccessToken(this.options, this.ragableAuth);
|
|
3523
|
+
readOnly = userTok ? params.readOnly !== false : true;
|
|
3524
|
+
} else {
|
|
3525
|
+
readOnly = params.readOnly !== false;
|
|
3526
|
+
}
|
|
3387
3527
|
const response = await this.fetchImpl(
|
|
3388
3528
|
this.toUrl(`/auth-groups/${gid}/data/query`),
|
|
3389
3529
|
{
|
|
@@ -3676,10 +3816,11 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
3676
3816
|
return subscription;
|
|
3677
3817
|
}
|
|
3678
3818
|
var BrowserStorageBucketClient = class {
|
|
3679
|
-
constructor(options, fetchImpl, bucketId) {
|
|
3819
|
+
constructor(options, fetchImpl, bucketId, ragableAuth = null) {
|
|
3680
3820
|
this.options = options;
|
|
3681
3821
|
this.fetchImpl = fetchImpl;
|
|
3682
3822
|
this.bucketId = bucketId;
|
|
3823
|
+
this.ragableAuth = ragableAuth;
|
|
3683
3824
|
}
|
|
3684
3825
|
get authGroupId() {
|
|
3685
3826
|
const id = this.options.authGroupId?.trim();
|
|
@@ -3689,14 +3830,28 @@ var BrowserStorageBucketClient = class {
|
|
|
3689
3830
|
base() {
|
|
3690
3831
|
return `${normalizeBrowserApiBase()}/auth-groups/${this.authGroupId}/storage/buckets/${encodeURIComponent(this.bucketId)}`;
|
|
3691
3832
|
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Same credential resolution as the database client (see resolveDatabaseAuthBearer):
|
|
3835
|
+
* in the generated-site default (`auto`), a signed-in user's auto-refreshed JWT
|
|
3836
|
+
* is used so storage calls carry the user's identity; logged-out visitors fall
|
|
3837
|
+
* back to the anon key. Previously storage ignored the managed session entirely.
|
|
3838
|
+
*/
|
|
3692
3839
|
async bearerToken() {
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3840
|
+
return resolveDatabaseAuthBearer(this.options, this.ragableAuth);
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* The storage backend has historically returned HTTP 200 with an `{ error }`
|
|
3844
|
+
* body on some failures; without this guard the SDK would resolve those as
|
|
3845
|
+
* successful uploads/deletes. Treat any 2xx whose body carries a non-empty
|
|
3846
|
+
* `error` as a failure.
|
|
3847
|
+
*/
|
|
3848
|
+
assertNoEmbeddedError(payload, status) {
|
|
3849
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
3850
|
+
const err = payload.error;
|
|
3851
|
+
if (typeof err === "string" && err.trim()) {
|
|
3852
|
+
throw new RagableError(err, status, payload);
|
|
3853
|
+
}
|
|
3698
3854
|
}
|
|
3699
|
-
throw new RagableError("No auth token for storage. Provide dataStaticKey or getAccessToken.", 401, { code: "SDK_NO_ACCESS_TOKEN" });
|
|
3700
3855
|
}
|
|
3701
3856
|
async req(method, path, body) {
|
|
3702
3857
|
const token = await this.bearerToken();
|
|
@@ -3709,7 +3864,8 @@ var BrowserStorageBucketClient = class {
|
|
|
3709
3864
|
body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
|
|
3710
3865
|
});
|
|
3711
3866
|
const payload = await res.json().catch(() => ({}));
|
|
3712
|
-
if (!res.ok) throw new RagableError(payload
|
|
3867
|
+
if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
|
|
3868
|
+
this.assertNoEmbeddedError(payload, res.status);
|
|
3713
3869
|
return payload;
|
|
3714
3870
|
}
|
|
3715
3871
|
list(params = {}) {
|
|
@@ -3733,7 +3889,8 @@ var BrowserStorageBucketClient = class {
|
|
|
3733
3889
|
if (params.cacheControl) form.set("cacheControl", params.cacheControl);
|
|
3734
3890
|
const res = await this.fetchImpl(`${this.base()}/upload`, { method: "POST", headers, body: form });
|
|
3735
3891
|
const payload = await res.json().catch(() => ({}));
|
|
3736
|
-
if (!res.ok) throw new RagableError(payload
|
|
3892
|
+
if (!res.ok) throw new RagableError(extractErrorMessage(payload, res.statusText), res.status, payload);
|
|
3893
|
+
this.assertNoEmbeddedError(payload, res.status);
|
|
3737
3894
|
return payload;
|
|
3738
3895
|
}
|
|
3739
3896
|
download(params) {
|
|
@@ -3774,12 +3931,18 @@ var BrowserStorageBucketClient = class {
|
|
|
3774
3931
|
}
|
|
3775
3932
|
};
|
|
3776
3933
|
var RagableBrowserStorageClient = class {
|
|
3777
|
-
constructor(options, fetchImpl) {
|
|
3934
|
+
constructor(options, fetchImpl, ragableAuth = null) {
|
|
3778
3935
|
this.options = options;
|
|
3779
3936
|
this.fetchImpl = fetchImpl;
|
|
3937
|
+
this.ragableAuth = ragableAuth;
|
|
3780
3938
|
}
|
|
3781
3939
|
from(bucketId) {
|
|
3782
|
-
return new BrowserStorageBucketClient(
|
|
3940
|
+
return new BrowserStorageBucketClient(
|
|
3941
|
+
this.options,
|
|
3942
|
+
this.fetchImpl,
|
|
3943
|
+
bucketId,
|
|
3944
|
+
this.ragableAuth
|
|
3945
|
+
);
|
|
3783
3946
|
}
|
|
3784
3947
|
};
|
|
3785
3948
|
var RagableBrowserMailClient = class {
|
|
@@ -4340,13 +4503,12 @@ var RagableBrowser = class {
|
|
|
4340
4503
|
auth: options.auth
|
|
4341
4504
|
});
|
|
4342
4505
|
this.transport.setRefreshHandler(async () => {
|
|
4343
|
-
|
|
4344
|
-
|
|
4506
|
+
const mode = effectiveDataAuth(options);
|
|
4507
|
+
if (mode !== "user" && mode !== "auto") return null;
|
|
4508
|
+
return this._ragableAuth.getValidAccessToken(true).catch(() => null);
|
|
4509
|
+
});
|
|
4510
|
+
this._ragableAuth.initialize().catch(() => {
|
|
4345
4511
|
});
|
|
4346
|
-
if (!options.getAccessToken && effectiveDataAuth(options) === "user") {
|
|
4347
|
-
this._ragableAuth.initialize().catch(() => {
|
|
4348
|
-
});
|
|
4349
|
-
}
|
|
4350
4512
|
} else {
|
|
4351
4513
|
this._ragableAuth = null;
|
|
4352
4514
|
}
|
|
@@ -4365,13 +4527,26 @@ var RagableBrowser = class {
|
|
|
4365
4527
|
);
|
|
4366
4528
|
this.database._setTransport(this.transport);
|
|
4367
4529
|
this.db = this.database;
|
|
4368
|
-
this.storage = new RagableBrowserStorageClient(
|
|
4530
|
+
this.storage = new RagableBrowserStorageClient(
|
|
4531
|
+
options,
|
|
4532
|
+
bindFetch(options.fetch),
|
|
4533
|
+
this._ragableAuth
|
|
4534
|
+
);
|
|
4369
4535
|
this.mail = new RagableBrowserMailClient(options, this._ragableAuth);
|
|
4370
4536
|
this.functions = new RagableBrowserFunctionsClient(
|
|
4371
4537
|
options,
|
|
4372
4538
|
this._ragableAuth
|
|
4373
4539
|
).asInvoker();
|
|
4374
4540
|
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Resolves once the persisted session has been restored (and refreshed if it
|
|
4543
|
+
* was expired). Await this before reading auth state at startup to avoid a
|
|
4544
|
+
* logged-out flash, e.g. `const session = await client.ready()`. Resolves
|
|
4545
|
+
* `null` when no auth group is configured or no session is stored.
|
|
4546
|
+
*/
|
|
4547
|
+
ready() {
|
|
4548
|
+
return this._ragableAuth ? this._ragableAuth.initialize() : Promise.resolve(null);
|
|
4549
|
+
}
|
|
4375
4550
|
destroy() {
|
|
4376
4551
|
this._ragableAuth?.destroy();
|
|
4377
4552
|
}
|
|
@@ -4381,6 +4556,91 @@ function createBrowserClient(options) {
|
|
|
4381
4556
|
}
|
|
4382
4557
|
var createRagableBrowserClient = createBrowserClient;
|
|
4383
4558
|
|
|
4559
|
+
// src/server.ts
|
|
4560
|
+
function optionsForPrivilege(config, privilege) {
|
|
4561
|
+
const base = {
|
|
4562
|
+
organizationId: config.organizationId,
|
|
4563
|
+
...config.websiteId !== void 0 ? { websiteId: config.websiteId } : {},
|
|
4564
|
+
...config.authGroupId !== void 0 ? { authGroupId: config.authGroupId } : {},
|
|
4565
|
+
...config.databaseInstanceId !== void 0 ? { databaseInstanceId: config.databaseInstanceId } : {},
|
|
4566
|
+
...config.fetch !== void 0 ? { fetch: config.fetch } : {},
|
|
4567
|
+
...config.headers !== void 0 ? { headers: config.headers } : {}
|
|
4568
|
+
};
|
|
4569
|
+
if (privilege === "admin") {
|
|
4570
|
+
const key = config.adminKey?.trim();
|
|
4571
|
+
if (!key) {
|
|
4572
|
+
throw new RagableError(
|
|
4573
|
+
"Admin privilege requires `adminKey` (the auth group's data-admin key). It is not available \u2014 admin functions may be disabled for this project.",
|
|
4574
|
+
403,
|
|
4575
|
+
{ code: "SDK_ADMIN_KEY_REQUIRED" }
|
|
4576
|
+
);
|
|
4577
|
+
}
|
|
4578
|
+
return { ...base, dataAuth: "admin", dataStaticKey: key };
|
|
4579
|
+
}
|
|
4580
|
+
return {
|
|
4581
|
+
...base,
|
|
4582
|
+
dataAuth: "auto",
|
|
4583
|
+
getAccessToken: () => config.callerToken ?? null,
|
|
4584
|
+
...config.publicAnonKey !== void 0 ? { dataStaticKey: config.publicAnonKey } : {}
|
|
4585
|
+
};
|
|
4586
|
+
}
|
|
4587
|
+
var RagableServerClient = class _RagableServerClient {
|
|
4588
|
+
constructor(config, privilege) {
|
|
4589
|
+
this.config = config;
|
|
4590
|
+
__publicField(this, "database");
|
|
4591
|
+
__publicField(this, "db");
|
|
4592
|
+
__publicField(this, "storage");
|
|
4593
|
+
__publicField(this, "mail");
|
|
4594
|
+
__publicField(this, "functions");
|
|
4595
|
+
__publicField(this, "ai");
|
|
4596
|
+
__publicField(this, "agents");
|
|
4597
|
+
/** Which credential this client is using. */
|
|
4598
|
+
__publicField(this, "privilege");
|
|
4599
|
+
this.privilege = privilege;
|
|
4600
|
+
const options = optionsForPrivilege(config, privilege);
|
|
4601
|
+
const transport = new Transport({
|
|
4602
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
4603
|
+
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
4604
|
+
...options.transport
|
|
4605
|
+
});
|
|
4606
|
+
this.database = new RagableBrowserDatabaseClient(options, null);
|
|
4607
|
+
this.database._setTransport(transport);
|
|
4608
|
+
this.db = this.database;
|
|
4609
|
+
this.storage = new RagableBrowserStorageClient(options, bindFetch(options.fetch), null);
|
|
4610
|
+
this.mail = new RagableBrowserMailClient(options, null);
|
|
4611
|
+
this.functions = new RagableBrowserFunctionsClient(options, null).asInvoker();
|
|
4612
|
+
this.ai = new RagableBrowserAiClient({
|
|
4613
|
+
organizationId: options.organizationId,
|
|
4614
|
+
...options.websiteId !== void 0 ? { websiteId: options.websiteId } : {},
|
|
4615
|
+
...options.fetch !== void 0 ? { fetch: options.fetch } : {},
|
|
4616
|
+
...options.headers !== void 0 ? { headers: options.headers } : {},
|
|
4617
|
+
apiBase: normalizeBrowserApiBase()
|
|
4618
|
+
});
|
|
4619
|
+
this.agents = new RagableBrowserAgentsClient(options);
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* A client authenticated with the **data-admin key** — bypasses collection
|
|
4623
|
+
* security (owner/group/claim grants do not apply) and can write protected
|
|
4624
|
+
* collections. Throws `SDK_ADMIN_KEY_REQUIRED` when no admin key is configured.
|
|
4625
|
+
*/
|
|
4626
|
+
asAdmin() {
|
|
4627
|
+
return new _RagableServerClient(this.config, "admin");
|
|
4628
|
+
}
|
|
4629
|
+
/**
|
|
4630
|
+
* A client scoped to the invoking end user's identity (respects collection
|
|
4631
|
+
* security). This is already the default; use it to drop back from `asAdmin()`.
|
|
4632
|
+
*/
|
|
4633
|
+
asCaller() {
|
|
4634
|
+
return new _RagableServerClient(this.config, "caller");
|
|
4635
|
+
}
|
|
4636
|
+
};
|
|
4637
|
+
function createServerClient(config) {
|
|
4638
|
+
return new RagableServerClient(
|
|
4639
|
+
config,
|
|
4640
|
+
config.defaultPrivilege ?? "caller"
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4384
4644
|
// src/index.ts
|
|
4385
4645
|
function createClient(options) {
|
|
4386
4646
|
return createBrowserClient(options);
|
|
@@ -4417,6 +4677,7 @@ export {
|
|
|
4417
4677
|
RagableError,
|
|
4418
4678
|
RagableNetworkError,
|
|
4419
4679
|
RagableSdkError,
|
|
4680
|
+
RagableServerClient,
|
|
4420
4681
|
RagableTimeoutError,
|
|
4421
4682
|
SessionStorageAdapter,
|
|
4422
4683
|
Transport,
|
|
@@ -4431,6 +4692,7 @@ export {
|
|
|
4431
4692
|
createBrowserClient,
|
|
4432
4693
|
createClient,
|
|
4433
4694
|
createRagableBrowserClient,
|
|
4695
|
+
createServerClient,
|
|
4434
4696
|
createStreamResultFromParts,
|
|
4435
4697
|
detectStorage,
|
|
4436
4698
|
effectiveDataAuth,
|