@gemmein/sdk 0.8.0 → 0.10.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 +209 -0
- package/README.md +82 -19
- package/REFERENCE.md +401 -50
- package/dist/expo.cjs +423 -0
- package/dist/expo.d.cts +152 -0
- package/dist/expo.d.ts +152 -0
- package/dist/expo.js +387 -0
- package/dist/index.cjs +330 -44
- package/dist/index.d.cts +243 -21
- package/dist/index.d.ts +243 -21
- package/dist/index.js +329 -44
- package/llms.txt +460 -78
- package/migrations/README.md +2 -0
- package/migrations/raw-calls-off.md +51 -0
- package/migrations/secure-store-set-throws.md +75 -0
- package/package.json +23 -2
package/dist/index.js
CHANGED
|
@@ -4,13 +4,83 @@
|
|
|
4
4
|
* second module). `scripts/sync-version.mjs` rewrites the literal from
|
|
5
5
|
* package.json before every build (`prebuild`), and a test pins the two
|
|
6
6
|
* equal, so a bump can never ship with a stale header. */
|
|
7
|
-
export const SDK_VERSION = "0.
|
|
7
|
+
export const SDK_VERSION = "0.10.0"; // synced from package.json — do not edit by hand
|
|
8
8
|
/** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
|
|
9
9
|
* `x-client-info: gemmein-sdk/<version>`. The server records it on the
|
|
10
10
|
* secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
|
|
11
11
|
* misbehaving integration can be attributed to an SDK version from day
|
|
12
12
|
* one. It is a report, not a proof — any caller can set it. */
|
|
13
13
|
export const CLIENT_INFO = `gemmein-sdk/${SDK_VERSION}`;
|
|
14
|
+
/** W10 §1 A: a mobile entry adds its platform — `gemmein-sdk/<version>
|
|
15
|
+
* expo-ios`. The ledger that records this header caps it at 64 characters
|
|
16
|
+
* and strips control characters (`MAX_CLIENT_LENGTH` / `capClient`,
|
|
17
|
+
* packages/db/src/keyUsageStore.ts), so the value is cleaned and capped
|
|
18
|
+
* HERE: a tag that arrives truncated attributes nothing. Anything outside
|
|
19
|
+
* `[A-Za-z0-9._/-]` collapses to a hyphen, so the label can never carry a
|
|
20
|
+
* newline — or a second space — into the ledger. */
|
|
21
|
+
export function clientInfoFor(platform) {
|
|
22
|
+
if (typeof platform !== "string")
|
|
23
|
+
return CLIENT_INFO;
|
|
24
|
+
const clean = platform.replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
25
|
+
if (clean.length === 0)
|
|
26
|
+
return CLIENT_INFO;
|
|
27
|
+
return `${CLIENT_INFO} ${clean}`.slice(0, 64);
|
|
28
|
+
}
|
|
29
|
+
/** W10 §1 A: every request goes through ONE function, so a platform that
|
|
30
|
+
* needs its own fetch injects it once. The default reads `globalThis.fetch`
|
|
31
|
+
* at call time (a polyfill installed later still counts), and both forms
|
|
32
|
+
* are invoked as plain calls — which is what keeps a browser's
|
|
33
|
+
* `window.fetch` legal when it is handed over as a bare reference.
|
|
34
|
+
*
|
|
35
|
+
* W10 row 9, found by driving the Expo app with the engine stopped: a
|
|
36
|
+
* transport failure — connection refused, no DNS, no Wi-Fi, an `apiUrl`
|
|
37
|
+
* pointing at nothing — never reaches `handleResponse`, so whatever the
|
|
38
|
+
* fetch implementation threw came out unchanged (a `TypeError` in a
|
|
39
|
+
* browser and on Expo's fetch). An app branching on `err.code`, which is
|
|
40
|
+
* what every teaching tells it to do, got `undefined` there and nowhere
|
|
41
|
+
* else. EVERY request the client makes — the runtime clients, a
|
|
42
|
+
* collection, the S3 upload POST, both AI streams — goes through this one
|
|
43
|
+
* wrapper, so every one of them now answers a `GemmeinError`
|
|
44
|
+
* `network_unreachable` (status 0, the SDK's convention for "no HTTP
|
|
45
|
+
* status applies"), with the fetch's own error kept on `err.cause`.
|
|
46
|
+
*
|
|
47
|
+
* Two throws pass through untouched: an `AbortError` — the caller
|
|
48
|
+
* cancelled on purpose, and `{ signal }` is a documented option on the AI
|
|
49
|
+
* calls — and a `GemmeinError` an injected fetch raised itself. */
|
|
50
|
+
function resolveFetch(injected, apiUrl) {
|
|
51
|
+
const call = injected
|
|
52
|
+
? (input, init) => injected(input, init)
|
|
53
|
+
: (input, init) => globalThis.fetch(input, init);
|
|
54
|
+
// The host is read ONCE, here, and only for the sentence: a client whose
|
|
55
|
+
// `apiUrl` is unparseable still gets a readable error, never a second throw.
|
|
56
|
+
const host = (() => {
|
|
57
|
+
try {
|
|
58
|
+
return new URL(apiUrl).host;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return apiUrl;
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
return (async (input, init) => {
|
|
65
|
+
try {
|
|
66
|
+
return await call(input, init);
|
|
67
|
+
}
|
|
68
|
+
catch (cause) {
|
|
69
|
+
// The caller's own cancel, and an SDK refusal an injected fetch chose
|
|
70
|
+
// to raise, are both already typed — re-typing them would lie.
|
|
71
|
+
if (cause instanceof GemmeinError)
|
|
72
|
+
throw cause;
|
|
73
|
+
if (typeof cause === "object" && cause !== null && cause.name === "AbortError")
|
|
74
|
+
throw cause;
|
|
75
|
+
throw new GemmeinError({
|
|
76
|
+
status: 0,
|
|
77
|
+
code: "network_unreachable",
|
|
78
|
+
message: `Gemmein could not be reached — check the connection and the apiUrl (${host})`,
|
|
79
|
+
cause,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
14
84
|
export class GemmeinError extends Error {
|
|
15
85
|
constructor(input) {
|
|
16
86
|
super(input.message);
|
|
@@ -19,6 +89,7 @@ export class GemmeinError extends Error {
|
|
|
19
89
|
this.code = input.code;
|
|
20
90
|
this.resetAt = input.resetAt;
|
|
21
91
|
this.requires = input.requires;
|
|
92
|
+
this.cause = input.cause;
|
|
22
93
|
}
|
|
23
94
|
}
|
|
24
95
|
export class MemoryTokenStore {
|
|
@@ -35,8 +106,15 @@ export class MemoryTokenStore {
|
|
|
35
106
|
// Persists the session across page reloads — the default in browsers.
|
|
36
107
|
// Sessions are long-lived server-side; a memory-only default would log
|
|
37
108
|
// users out on every refresh. Keyed per app key so two Gemmein apps on
|
|
38
|
-
// one origin never share a token.
|
|
39
|
-
//
|
|
109
|
+
// one origin never share a token.
|
|
110
|
+
//
|
|
111
|
+
// READS AND CLEARS ARE LENIENT, WRITES ARE NOT — the same law the two
|
|
112
|
+
// mobile stores keep (`SecureStoreTokenStore` in `@gemmein/sdk/expo`,
|
|
113
|
+
// `KeychainTokenStore` in GemmeinSwift). A store that cannot be READ means
|
|
114
|
+
// signed out, which every app already handles; a store that cannot KEEP
|
|
115
|
+
// the session has broken the promise it exists for, and silence there
|
|
116
|
+
// signs the person out on the next reload with nothing anywhere saying
|
|
117
|
+
// why.
|
|
40
118
|
export class BrowserTokenStore {
|
|
41
119
|
constructor(appKey) {
|
|
42
120
|
this.key = `gemmein_session_${appKey.slice(0, 20)}`;
|
|
@@ -49,11 +127,30 @@ export class BrowserTokenStore {
|
|
|
49
127
|
return undefined;
|
|
50
128
|
}
|
|
51
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* A blocked `localStorage` — Safari's private mode past its quota, a
|
|
132
|
+
* browser set to block site data, a sandboxed iframe whose access throws
|
|
133
|
+
* — throws `secure_store_unavailable` (status 0), the same code the
|
|
134
|
+
* mobile stores answer, with the browser's own error on `cause`.
|
|
135
|
+
* `auth.verifyEmailCode()` carries it to the caller: the session is real
|
|
136
|
+
* (the server minted it), so an app that would rather run than stop
|
|
137
|
+
* catches this one code and rebuilds its client with a
|
|
138
|
+
* `MemoryTokenStore`, which never throws.
|
|
139
|
+
*/
|
|
52
140
|
set(token) {
|
|
53
141
|
try {
|
|
54
142
|
window.localStorage.setItem(this.key, token);
|
|
55
143
|
}
|
|
56
|
-
catch {
|
|
144
|
+
catch (cause) {
|
|
145
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
146
|
+
throw new GemmeinError({
|
|
147
|
+
status: 0,
|
|
148
|
+
code: "secure_store_unavailable",
|
|
149
|
+
message: `The session could not be stored: this browser refused the localStorage write (${detail}) ` +
|
|
150
|
+
"— pass a tokenStore, e.g. new MemoryTokenStore()",
|
|
151
|
+
cause,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
57
154
|
}
|
|
58
155
|
clear() {
|
|
59
156
|
try {
|
|
@@ -104,10 +201,14 @@ export class Gemmein {
|
|
|
104
201
|
'copy the key from the Setup page. Then: gemmein("pk_...")'
|
|
105
202
|
});
|
|
106
203
|
}
|
|
204
|
+
const apiUrl = options.apiUrl ?? "https://api.gemmein.com";
|
|
107
205
|
const config = {
|
|
108
|
-
apiUrl
|
|
206
|
+
apiUrl,
|
|
109
207
|
appKey: options.appKey,
|
|
110
|
-
tokenStore: options.tokenStore ?? defaultTokenStore(options.appKey)
|
|
208
|
+
tokenStore: options.tokenStore ?? defaultTokenStore(options.appKey),
|
|
209
|
+
fetch: resolveFetch(options.fetch, apiUrl),
|
|
210
|
+
visibility: options.visibility,
|
|
211
|
+
clientInfo: clientInfoFor(options.platform)
|
|
111
212
|
};
|
|
112
213
|
this.auth = new AuthClient(config);
|
|
113
214
|
this.storage = new StorageClient(config);
|
|
@@ -129,6 +230,7 @@ export class Gemmein {
|
|
|
129
230
|
* suggestion attached — always pass it when you aren't certain the
|
|
130
231
|
* collection exists yet. The cloud ignores it.
|
|
131
232
|
*/
|
|
233
|
+
// route: none
|
|
132
234
|
collection(name, options = {}) {
|
|
133
235
|
return this.storage.collection(name, options);
|
|
134
236
|
}
|
|
@@ -153,6 +255,7 @@ export class AuthClient {
|
|
|
153
255
|
constructor(config) {
|
|
154
256
|
this.config = config;
|
|
155
257
|
}
|
|
258
|
+
// route: POST /auth/email/start
|
|
156
259
|
async sendEmailCode(email) {
|
|
157
260
|
await this.request("/auth/email/start", {
|
|
158
261
|
method: "POST",
|
|
@@ -160,6 +263,7 @@ export class AuthClient {
|
|
|
160
263
|
headers: { "content-type": "application/json" }
|
|
161
264
|
});
|
|
162
265
|
}
|
|
266
|
+
// route: POST /auth/email/verify
|
|
163
267
|
async verifyEmailCode(input) {
|
|
164
268
|
const result = await this.request("/auth/email/verify", {
|
|
165
269
|
method: "POST",
|
|
@@ -180,15 +284,33 @@ export class AuthClient {
|
|
|
180
284
|
message: "Gemmein auth response did not include a session token"
|
|
181
285
|
});
|
|
182
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* End this device's session. IDEMPOTENT: signing out of a session that is
|
|
289
|
+
* already gone is the outcome asked for, not a failure.
|
|
290
|
+
*
|
|
291
|
+
* W10 row 9, found by driving the Expo app: the commonest way this
|
|
292
|
+
* round-trip fails is `401 auth_expired` — the owner already signed this
|
|
293
|
+
* person out everywhere from the console, which is the remedy for a
|
|
294
|
+
* stolen phone. The session IS ended; throwing there made an app that did
|
|
295
|
+
* everything right show an error for the thing it asked for. Every other
|
|
296
|
+
* failure still throws, and the token store is cleared either way.
|
|
297
|
+
*/
|
|
298
|
+
// route: POST /auth/logout
|
|
183
299
|
async logout() {
|
|
184
300
|
try {
|
|
185
301
|
await this.request("/auth/logout", { method: "POST" });
|
|
186
302
|
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
const alreadyEnded = err instanceof GemmeinError && err.status === 401 && err.code === "auth_expired";
|
|
305
|
+
if (!alreadyEnded)
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
187
308
|
finally {
|
|
188
309
|
// Even if the network call fails, the user asked to be signed out.
|
|
189
310
|
await this.config.tokenStore.clear();
|
|
190
311
|
}
|
|
191
312
|
}
|
|
313
|
+
// route: GET /auth/current-user
|
|
192
314
|
async currentUser() {
|
|
193
315
|
// Contract: "who am I?" never throws for session state — the server
|
|
194
316
|
// answers 200 {authenticated:false} for a token it won't honour right
|
|
@@ -245,6 +367,7 @@ export class PurchasesClient {
|
|
|
245
367
|
* or `{ type: "external_url", url }` (a plain handover). A refunded
|
|
246
368
|
* purchase never carries delivery.
|
|
247
369
|
*/
|
|
370
|
+
// route: GET /auth/purchases
|
|
248
371
|
async mine() {
|
|
249
372
|
const result = (await runtimeRequest(this.config, "/auth/purchases"));
|
|
250
373
|
return result.purchases;
|
|
@@ -276,6 +399,7 @@ export class FilesClient {
|
|
|
276
399
|
constructor(config) {
|
|
277
400
|
this.config = config;
|
|
278
401
|
}
|
|
402
|
+
// route: GET /files/{ref}/link
|
|
279
403
|
async link(ref, options = {}) {
|
|
280
404
|
const query = options.intent === "download" ? "?intent=download" : "";
|
|
281
405
|
return (await runtimeRequest(this.config, `/files/${encodeURIComponent(String(ref))}/link${query}`));
|
|
@@ -292,6 +416,7 @@ export class SubscriptionsClient {
|
|
|
292
416
|
* nobody is signed in — a data route, not the never-throw current-user
|
|
293
417
|
* contract.
|
|
294
418
|
*/
|
|
419
|
+
// route: GET /auth/subscription
|
|
295
420
|
async mine() {
|
|
296
421
|
const result = (await runtimeRequest(this.config, "/auth/subscription"));
|
|
297
422
|
return result.subscription;
|
|
@@ -305,6 +430,7 @@ export class SubscriptionsClient {
|
|
|
305
430
|
* and a plan whose Payment Link the app owner has pasted in their
|
|
306
431
|
* dashboard; omit `plan` to buy the app's paid plan.
|
|
307
432
|
*/
|
|
433
|
+
// route: GET /auth/checkout
|
|
308
434
|
async checkout(plan) {
|
|
309
435
|
const query = plan ? `?plan=${encodeURIComponent(plan)}` : "";
|
|
310
436
|
const result = (await runtimeRequest(this.config, `/auth/checkout${query}`));
|
|
@@ -325,10 +451,16 @@ export class PaymentsClient {
|
|
|
325
451
|
* WHAT is being bought when one product covers many things (e.g. a
|
|
326
452
|
* license tier across a catalog):
|
|
327
453
|
* `g.payments.buy("premium license", { item: "beat_37" })`.
|
|
328
|
-
*
|
|
329
|
-
* the
|
|
330
|
-
*
|
|
454
|
+
* Gemmein records every completed payment itself: `g.purchases.mine()` is
|
|
455
|
+
* the buyer's proof, and a product that delivers a file carries
|
|
456
|
+
* `delivery` on it. Gate fulfilment on the purchase (or the entitlement it
|
|
457
|
+
* granted), never on the redirect coming back. A receipts collection is
|
|
458
|
+
* optional, for proof records only.
|
|
459
|
+
*
|
|
460
|
+
* A product sold via a RELAY, or not sold yet, has no Payment Link to
|
|
461
|
+
* open: this answers 409 `product_not_sellable`.
|
|
331
462
|
*/
|
|
463
|
+
// route: GET /auth/pay
|
|
332
464
|
async buy(product, options) {
|
|
333
465
|
const params = new URLSearchParams({ product });
|
|
334
466
|
if (options?.item)
|
|
@@ -353,6 +485,7 @@ export class AccountClient {
|
|
|
353
485
|
* subscription row removed. Irreversible — put a real confirm in front
|
|
354
486
|
* of it.
|
|
355
487
|
*/
|
|
488
|
+
// route: POST /auth/delete-account
|
|
356
489
|
async delete() {
|
|
357
490
|
const result = await runtimeRequest(this.config, "/auth/delete-account", { method: "POST" });
|
|
358
491
|
await this.config.tokenStore.clear();
|
|
@@ -370,21 +503,30 @@ export class CreditsClient {
|
|
|
370
503
|
*
|
|
371
504
|
* const { balance } = await g.credits.balance();
|
|
372
505
|
*/
|
|
506
|
+
// route: GET /auth/credits
|
|
373
507
|
async balance() {
|
|
374
508
|
return runtimeRequest(this.config, "/auth/credits");
|
|
375
509
|
}
|
|
376
510
|
}
|
|
377
511
|
/**
|
|
378
|
-
* The AI route.
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* `
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
512
|
+
* The AI route. The primary path is a NAMED TOOL defined on the server:
|
|
513
|
+
* `run(name, inputs)` sends a name and inputs, the server composes the
|
|
514
|
+
* provider request from the tool's own instructions and template (never
|
|
515
|
+
* the browser), gates it, spends the tool's credits and streams the
|
|
516
|
+
* answer back; `runText` is the same call collected to one string;
|
|
517
|
+
* `calls()` is the signed-in person's own history. `chat` is the RAW
|
|
518
|
+
* call: it takes the provider's own request body — exactly what you would
|
|
519
|
+
* POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages or
|
|
520
|
+
* Google's generateContent — and answers with the fetch `Response`
|
|
521
|
+
* untouched, streaming intact (SSE stays SSE). Raw calls are off by
|
|
522
|
+
* default for every provider key (`raw_calls_off`, 403) until the founder
|
|
523
|
+
* switches them on for that key on the AI tools page. Gemmein spends a
|
|
524
|
+
* credit, adds the owner's key, forwards, and passes status and bytes
|
|
525
|
+
* back. Pass `tool` (W9.3b) to a raw call to price and gate it as a named
|
|
526
|
+
* tool instead of the implicit default (one credit, any allowed model, no
|
|
527
|
+
* gate). Response headers: `x-gemmein-credits-remaining` on every answer
|
|
528
|
+
* that passed the spend; `x-gemmein-credit: refunded` when the provider
|
|
529
|
+
* failed before its first byte.
|
|
388
530
|
*/
|
|
389
531
|
export class AiClient {
|
|
390
532
|
constructor(config) {
|
|
@@ -395,7 +537,10 @@ export class AiClient {
|
|
|
395
537
|
* for await (const chunk of res.body) { … }
|
|
396
538
|
*
|
|
397
539
|
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
398
|
-
* Refusals, all `GemmeinError`: `
|
|
540
|
+
* Refusals, all `GemmeinError`: `raw_calls_off` (403 — raw calls are off
|
|
541
|
+
* for this provider until the founder switches them on for its key on
|
|
542
|
+
* the AI tools page; call a named tool with `run` instead) ·
|
|
543
|
+
* `session_required` (401) ·
|
|
399
544
|
* `credits_exhausted` (402 — the message carries the balance; show your
|
|
400
545
|
* own "buy more" door, which is a product checkout) · `ai_not_configured`
|
|
401
546
|
* (409 — the owner has set no key) · `provider_required` (400) ·
|
|
@@ -414,12 +559,13 @@ export class AiClient {
|
|
|
414
559
|
* `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
|
|
415
560
|
* on the implicit default.
|
|
416
561
|
*/
|
|
562
|
+
// route: POST /ai/chat
|
|
417
563
|
async chat(body, options = {}) {
|
|
418
564
|
const payload = options.provider ? { provider: options.provider, ...body } : body;
|
|
419
565
|
const url = new URL("/ai/chat", this.config.apiUrl);
|
|
420
566
|
if (options.tool)
|
|
421
567
|
url.searchParams.set("tool", options.tool);
|
|
422
|
-
const response = await fetch(url, {
|
|
568
|
+
const response = await this.config.fetch(url, {
|
|
423
569
|
method: "POST",
|
|
424
570
|
body: JSON.stringify(payload),
|
|
425
571
|
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
|
@@ -441,6 +587,94 @@ export class AiClient {
|
|
|
441
587
|
}
|
|
442
588
|
return response;
|
|
443
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* W9.6: run a named tool with INPUTS — the server composes the provider
|
|
592
|
+
* request from the tool's own instructions and template (never the
|
|
593
|
+
* browser), gates it, spends its credits and streams the answer back.
|
|
594
|
+
* The answer is the provider's own shape for the tool's provider (SSE
|
|
595
|
+
* when `stream`), so read it as you would `chat()`'s.
|
|
596
|
+
*
|
|
597
|
+
* const res = await g.ai.run("summarise", { text }, { stream: true });
|
|
598
|
+
*
|
|
599
|
+
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
600
|
+
* Refusals, all `GemmeinError`: `session_required` (401 — sign in
|
|
601
|
+
* first) · `ai_capped` (429 — 20 calls a minute per person;
|
|
602
|
+
* `err.resetAt`) · `unknown_tool` (404 — no tool by that name in this
|
|
603
|
+
* environment) · `tool_disabled` (403 — the owner switched it off) ·
|
|
604
|
+
* `entitlement_required` (403 — the message names the plan or product
|
|
605
|
+
* it needs) · `payload_too_large` (413 — inputs over 64 KB) ·
|
|
606
|
+
* `invalid_body` (400 — the body must be a JSON object
|
|
607
|
+
* `{ inputs, stream? }`) · `invalid_inputs` (400 — the message names the
|
|
608
|
+
* input and the rule) · `tool_incomplete` (409 — the tool composes
|
|
609
|
+
* nothing; a founder's fix) · `ai_not_configured` (409 — the tool's
|
|
610
|
+
* provider has no key set; the owner pastes one) · `credits_exhausted`
|
|
611
|
+
* (402 — the message names the tool, its price and the balance) ·
|
|
612
|
+
* `provider_unreachable` (502 — no answer before the first byte; the
|
|
613
|
+
* tool's credits are refunded, header `x-gemmein-credit: refunded`).
|
|
614
|
+
* The provider's own answer — 2xx or not — is returned as it came; read
|
|
615
|
+
* `res.ok` yourself. `x-gemmein-tool` names the tool.
|
|
616
|
+
*/
|
|
617
|
+
// route: POST /ai/run/{tool}
|
|
618
|
+
async run(tool, inputs = {}, options = {}) {
|
|
619
|
+
const url = new URL(`/ai/run/${encodeURIComponent(tool)}`, this.config.apiUrl);
|
|
620
|
+
const response = await this.config.fetch(url, {
|
|
621
|
+
method: "POST",
|
|
622
|
+
body: JSON.stringify({ inputs, ...(options.stream ? { stream: true } : {}) }),
|
|
623
|
+
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
|
624
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
625
|
+
});
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
if (isForwardedAnswer(response)) {
|
|
628
|
+
const peek = (await response.clone().json().catch(() => null));
|
|
629
|
+
if (peek?.code !== "provider_unreachable")
|
|
630
|
+
return response;
|
|
631
|
+
}
|
|
632
|
+
const errorBody = await readErrorBody(response);
|
|
633
|
+
if (errorBody.code === "auth_expired")
|
|
634
|
+
await this.config.tokenStore.clear();
|
|
635
|
+
throw new GemmeinError({ status: response.status, ...errorBody });
|
|
636
|
+
}
|
|
637
|
+
return response;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* W9.6: `run()` without a stream, as one string — the text lifted out
|
|
641
|
+
* of the tool's provider's answer (the same readers `text()` uses).
|
|
642
|
+
* `run()`'s refusals, plus: a provider's own non-2xx throws
|
|
643
|
+
* `provider_error` with the provider's status and message; an answer
|
|
644
|
+
* with no text to lift out throws `invalid_response` (status 0).
|
|
645
|
+
*
|
|
646
|
+
* const summary = await g.ai.runText("summarise", { text });
|
|
647
|
+
*/
|
|
648
|
+
// route: POST /ai/run/{tool}
|
|
649
|
+
async runText(tool, inputs = {}, options = {}) {
|
|
650
|
+
const response = await this.run(tool, inputs, options);
|
|
651
|
+
if (!response.ok) {
|
|
652
|
+
throw new GemmeinError({ status: response.status, code: "provider_error", message: await providerErrorMessage(response) });
|
|
653
|
+
}
|
|
654
|
+
const data = (await response.json());
|
|
655
|
+
const text = extractAiText(data);
|
|
656
|
+
if (text === null) {
|
|
657
|
+
throw new GemmeinError({ status: 0, code: "invalid_response", message: "the provider answered without any text — use g.ai.run() with { stream: true } and read the stream" });
|
|
658
|
+
}
|
|
659
|
+
return text;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* W9.6 §16: the signed-in person's OWN AI calls, newest first — what they
|
|
663
|
+
* ran, when, what it cost, how it ended; the prompt and answer only where
|
|
664
|
+
* the tool keeps them. Session required.
|
|
665
|
+
*
|
|
666
|
+
* const { calls, nextCursor } = await g.ai.calls();
|
|
667
|
+
*/
|
|
668
|
+
// route: GET /auth/ai-calls
|
|
669
|
+
async calls(options = {}) {
|
|
670
|
+
const params = new URLSearchParams();
|
|
671
|
+
if (options.limit)
|
|
672
|
+
params.set("limit", String(options.limit));
|
|
673
|
+
if (options.before)
|
|
674
|
+
params.set("before", options.before);
|
|
675
|
+
const query = params.toString();
|
|
676
|
+
return runtimeRequest(this.config, `/auth/ai-calls${query ? `?${query}` : ""}`);
|
|
677
|
+
}
|
|
444
678
|
/**
|
|
445
679
|
* The non-streaming convenience: one call, one string. Pass a body that
|
|
446
680
|
* does NOT stream (`stream` unset or false); the provider's JSON answer is
|
|
@@ -454,6 +688,7 @@ export class AiClient {
|
|
|
454
688
|
*
|
|
455
689
|
* const answer = await g.ai.text({ model: "claude-sonnet-4-5", max_tokens: 400, messages });
|
|
456
690
|
*/
|
|
691
|
+
// route: POST /ai/chat
|
|
457
692
|
async text(body, options = {}) {
|
|
458
693
|
const response = await this.chat(body, options);
|
|
459
694
|
if (!response.ok) {
|
|
@@ -541,6 +776,7 @@ export class StorageClient {
|
|
|
541
776
|
this.config = config;
|
|
542
777
|
}
|
|
543
778
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
779
|
+
// route: none
|
|
544
780
|
collection(name, options = {}) {
|
|
545
781
|
assertCollectionName(name);
|
|
546
782
|
return new CollectionClient(this.config, name, options);
|
|
@@ -584,6 +820,7 @@ export class CollectionClient {
|
|
|
584
820
|
* fake drafts with a data field + client-side filtering: on a public
|
|
585
821
|
* collection the data still reaches everyone.
|
|
586
822
|
*/
|
|
823
|
+
// route: POST /storage/{collection}
|
|
587
824
|
async create(data, options = {}) {
|
|
588
825
|
const query = new URLSearchParams();
|
|
589
826
|
if (options.key !== undefined)
|
|
@@ -603,6 +840,7 @@ export class CollectionClient {
|
|
|
603
840
|
* rule (the app owner sees everyone's). Returns `{ records, hasMore }` —
|
|
604
841
|
* an object, not a bare array.
|
|
605
842
|
*/
|
|
843
|
+
// route: GET /storage/{collection}
|
|
606
844
|
async list(options = {}) {
|
|
607
845
|
const query = new URLSearchParams();
|
|
608
846
|
if (options.limit !== undefined)
|
|
@@ -643,6 +881,7 @@ export class CollectionClient {
|
|
|
643
881
|
* doubling up to 60s, and honours a rate limit's reset time. One watch per
|
|
644
882
|
* page is the intended shape — share its result, don't stack watchers.
|
|
645
883
|
*/
|
|
884
|
+
// route: GET /storage/{collection}
|
|
646
885
|
watch(onChange, options = {}) {
|
|
647
886
|
const asked = options.every;
|
|
648
887
|
const every = Math.max(5000, Math.min(300000, Number.isFinite(asked) ? asked : 10000));
|
|
@@ -650,6 +889,11 @@ export class CollectionClient {
|
|
|
650
889
|
let stopped = false;
|
|
651
890
|
let timer;
|
|
652
891
|
let delayMs = every;
|
|
892
|
+
// W10 §1 A: the platform's own answer to "is the app in front of the
|
|
893
|
+
// user?" when one was injected — `document` when it wasn't. One
|
|
894
|
+
// `unsubscribe` either way, so every teardown path is the same line.
|
|
895
|
+
const vis = this.config.visibility;
|
|
896
|
+
let unsubscribe;
|
|
653
897
|
let watermark;
|
|
654
898
|
// One tick at a time. A visibility flip mid-tick sets resyncPending
|
|
655
899
|
// instead of racing a second loop into existence (each raced loop would
|
|
@@ -715,9 +959,8 @@ export class CollectionClient {
|
|
|
715
959
|
clearTimeout(timer);
|
|
716
960
|
timer = undefined;
|
|
717
961
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
962
|
+
unsubscribe?.();
|
|
963
|
+
unsubscribe = undefined;
|
|
721
964
|
if (typeof console !== "undefined")
|
|
722
965
|
console.warn(`gemmein watch stopped: ${err.code} — start a new watch after signing in`);
|
|
723
966
|
return;
|
|
@@ -741,7 +984,9 @@ export class CollectionClient {
|
|
|
741
984
|
}
|
|
742
985
|
schedule();
|
|
743
986
|
};
|
|
744
|
-
const hidden = () =>
|
|
987
|
+
const hidden = () => (vis
|
|
988
|
+
? vis.isHidden()
|
|
989
|
+
: typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
745
990
|
const schedule = () => {
|
|
746
991
|
if (stopped || hidden() || timer !== undefined)
|
|
747
992
|
return;
|
|
@@ -775,8 +1020,12 @@ export class CollectionClient {
|
|
|
775
1020
|
void tick(true);
|
|
776
1021
|
}
|
|
777
1022
|
};
|
|
778
|
-
if (
|
|
1023
|
+
if (vis) {
|
|
1024
|
+
unsubscribe = vis.onChange(onVisibility);
|
|
1025
|
+
}
|
|
1026
|
+
else if (typeof document !== "undefined") {
|
|
779
1027
|
document.addEventListener("visibilitychange", onVisibility);
|
|
1028
|
+
unsubscribe = () => document.removeEventListener("visibilitychange", onVisibility);
|
|
780
1029
|
}
|
|
781
1030
|
// Born hidden: wait for the tab — the visibility handler runs the first
|
|
782
1031
|
// sync when the user actually looks.
|
|
@@ -790,12 +1039,12 @@ export class CollectionClient {
|
|
|
790
1039
|
if (timer !== undefined)
|
|
791
1040
|
clearTimeout(timer);
|
|
792
1041
|
timer = undefined;
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
1042
|
+
unsubscribe?.();
|
|
1043
|
+
unsubscribe = undefined;
|
|
796
1044
|
},
|
|
797
1045
|
};
|
|
798
1046
|
}
|
|
1047
|
+
// route: GET /storage/{collection}/{id}
|
|
799
1048
|
async get(id, options = {}) {
|
|
800
1049
|
const qs = options.expand && options.expand.length > 0 ? `?expand=${encodeURIComponent(options.expand.join(","))}` : "";
|
|
801
1050
|
return this.request(`/${encodeURIComponent(id)}${qs}`);
|
|
@@ -815,6 +1064,7 @@ export class CollectionClient {
|
|
|
815
1064
|
* docs), pass `{ ifVersion: record.version }` — a stale save gets a 409
|
|
816
1065
|
* `conflict` instead of clobbering; re-read, reapply, retry.
|
|
817
1066
|
*/
|
|
1067
|
+
// route: PATCH /storage/{collection}/{id}
|
|
818
1068
|
async update(id, data, options = {}) {
|
|
819
1069
|
const query = new URLSearchParams();
|
|
820
1070
|
if (options.ifVersion !== undefined)
|
|
@@ -827,11 +1077,12 @@ export class CollectionClient {
|
|
|
827
1077
|
body: JSON.stringify(data)
|
|
828
1078
|
});
|
|
829
1079
|
}
|
|
1080
|
+
// route: DELETE /storage/{collection}/{id}
|
|
830
1081
|
async delete(id) {
|
|
831
1082
|
await this.request(`/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
832
1083
|
}
|
|
833
1084
|
/**
|
|
834
|
-
* Upload a file and get back a REFERENCE — `file
|
|
1085
|
+
* Upload a file and get back a REFERENCE — `file:<uuid>` — not a URL.
|
|
835
1086
|
*
|
|
836
1087
|
* Store the reference. It never expires, it is safe to log and export, and
|
|
837
1088
|
* it grants nothing on its own. To show or download the file, call
|
|
@@ -858,23 +1109,43 @@ export class CollectionClient {
|
|
|
858
1109
|
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
859
1110
|
* bug this replaced.
|
|
860
1111
|
*/
|
|
1112
|
+
// route: POST /storage/{collection}/upload
|
|
1113
|
+
// route: POST /storage/{collection}/upload/{fileId}/confirm
|
|
861
1114
|
async upload(file, options) {
|
|
862
|
-
|
|
1115
|
+
// W10 §1 A: `File` is a browser class. Where it does not exist — React
|
|
1116
|
+
// Native — a bare `instanceof File` is a ReferenceError, not a false,
|
|
1117
|
+
// so the guard comes first. The three fields the presign needs are then
|
|
1118
|
+
// read the same way off every shape: a File, a Blob, an
|
|
1119
|
+
// `expo-file-system` File, or the picker's `{ uri, name, type, size }`.
|
|
1120
|
+
const part = file;
|
|
1121
|
+
const name = options?.name
|
|
1122
|
+
?? (typeof File !== "undefined" && file instanceof File
|
|
1123
|
+
? file.name
|
|
1124
|
+
: typeof part.name === "string" ? part.name : "upload");
|
|
863
1125
|
// A Blob built from bytes has type "" — `contentType` names it. The
|
|
864
1126
|
// server proves the bytes either way.
|
|
865
|
-
const contentType = options?.contentType ??
|
|
1127
|
+
const contentType = options?.contentType ?? part.type ?? "";
|
|
1128
|
+
// A presign under one byte is refused ("file must not be empty"): a
|
|
1129
|
+
// Blob knows its own size, a React Native part carries the picker's.
|
|
1130
|
+
const size = typeof part.size === "number" ? part.size : 0;
|
|
866
1131
|
// Step 1: Get presigned upload URL
|
|
867
1132
|
const presign = await this.request("/upload", {
|
|
868
1133
|
method: "POST",
|
|
869
|
-
body: JSON.stringify({ name, size
|
|
1134
|
+
body: JSON.stringify({ name, size, contentType, ...(options?.for ? { for: options.for } : {}) }),
|
|
870
1135
|
});
|
|
871
1136
|
// Step 2: Upload directly to S3 via presigned POST
|
|
872
1137
|
const form = new FormData();
|
|
873
1138
|
for (const [key, value] of Object.entries(presign.fields)) {
|
|
874
1139
|
form.append(key, value);
|
|
875
1140
|
}
|
|
876
|
-
|
|
877
|
-
|
|
1141
|
+
// Must be last — S3 presigned POST requirement. Whatever arrived is
|
|
1142
|
+
// appended AS IT IS: a Blob, a File, or — on a client given React
|
|
1143
|
+
// Native's own fetch — the picker's `{ uri, ... }` part, whose bytes
|
|
1144
|
+
// RN's FormData reads off the device when it serialises. Expo's fetch
|
|
1145
|
+
// refuses that part, so `@gemmein/sdk/expo` has already turned it into
|
|
1146
|
+
// an `expo-file-system` `File` by the time it gets here.
|
|
1147
|
+
form.append("file", file);
|
|
1148
|
+
const s3Response = await this.config.fetch(presign.uploadUrl, { method: "POST", body: form });
|
|
878
1149
|
if (!s3Response.ok) {
|
|
879
1150
|
throw new GemmeinError({
|
|
880
1151
|
status: s3Response.status,
|
|
@@ -889,7 +1160,7 @@ export class CollectionClient {
|
|
|
889
1160
|
return confirmed;
|
|
890
1161
|
}
|
|
891
1162
|
async request(suffix, init = {}) {
|
|
892
|
-
const response = await fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.config.apiUrl), {
|
|
1163
|
+
const response = await this.config.fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.config.apiUrl), {
|
|
893
1164
|
...init,
|
|
894
1165
|
headers: await runtimeHeaders(this.config, {
|
|
895
1166
|
"content-type": "application/json",
|
|
@@ -918,10 +1189,12 @@ export class GemmeinServer {
|
|
|
918
1189
|
}
|
|
919
1190
|
this.apiUrl = options.apiUrl ?? "https://api.gemmein.com";
|
|
920
1191
|
this.secretKey = options.secretKey;
|
|
1192
|
+
this.fetch = resolveFetch(options.fetch, this.apiUrl);
|
|
921
1193
|
}
|
|
1194
|
+
// route: none
|
|
922
1195
|
collection(name) {
|
|
923
1196
|
assertCollectionName(name);
|
|
924
|
-
return new ServerCollectionClient(this.apiUrl, this.secretKey, name);
|
|
1197
|
+
return new ServerCollectionClient(this.apiUrl, this.secretKey, name, this.fetch);
|
|
925
1198
|
}
|
|
926
1199
|
/**
|
|
927
1200
|
* Mint a member session for a test email WITHOUT an OTP round-trip — so a CI
|
|
@@ -931,6 +1204,7 @@ export class GemmeinServer {
|
|
|
931
1204
|
* `sk_live` key, and the server refuses it too. Never ship this in app code.
|
|
932
1205
|
* Pass the returned `token` to `gemmein(pk, { tokenStore })` to act as that user.
|
|
933
1206
|
*/
|
|
1207
|
+
// route: POST /server/test-session
|
|
934
1208
|
async testSession(email) {
|
|
935
1209
|
if (this.secretKey.startsWith("sk_live")) {
|
|
936
1210
|
throw new GemmeinError({
|
|
@@ -939,7 +1213,7 @@ export class GemmeinServer {
|
|
|
939
1213
|
message: "test sessions are only available in a development environment — never with a live (sk_live) key",
|
|
940
1214
|
});
|
|
941
1215
|
}
|
|
942
|
-
const response = await fetch(new URL("/server/test-session", this.apiUrl), {
|
|
1216
|
+
const response = await this.fetch(new URL("/server/test-session", this.apiUrl), {
|
|
943
1217
|
method: "POST",
|
|
944
1218
|
headers: { "x-app-key": this.secretKey, "x-client-info": CLIENT_INFO, "content-type": "application/json" },
|
|
945
1219
|
body: JSON.stringify({ email }),
|
|
@@ -976,8 +1250,9 @@ export class GemmeinServer {
|
|
|
976
1250
|
* (a security notice must never lose to five order emails); misusing it
|
|
977
1251
|
* for campaigns is visible in your own audit trail.
|
|
978
1252
|
*/
|
|
1253
|
+
// route: POST /server/notify
|
|
979
1254
|
async notify(personId, input) {
|
|
980
|
-
const response = await fetch(new URL("/server/notify", this.apiUrl), {
|
|
1255
|
+
const response = await this.fetch(new URL("/server/notify", this.apiUrl), {
|
|
981
1256
|
method: "POST",
|
|
982
1257
|
headers: { "x-app-key": this.secretKey, "x-client-info": CLIENT_INFO, "content-type": "application/json" },
|
|
983
1258
|
body: JSON.stringify({ personId, subject: input.subject, text: input.text, ...(input.kind ? { kind: input.kind } : {}), ...(input.key ? { key: input.key } : {}) }),
|
|
@@ -1014,6 +1289,7 @@ export class GemmeinServer {
|
|
|
1014
1289
|
* off until the owner reactivates them in the dashboard) ·
|
|
1015
1290
|
* `invalid_body` (token missing, not a string, or over 512 chars).
|
|
1016
1291
|
*/
|
|
1292
|
+
// route: POST /server/verify-session
|
|
1017
1293
|
async verifySession(token) {
|
|
1018
1294
|
return this.gate("/server/verify-session", {
|
|
1019
1295
|
method: "POST",
|
|
@@ -1043,6 +1319,7 @@ export class GemmeinServer {
|
|
|
1043
1319
|
* `invite_capped` (429 — 500 invite calls per app per day, a fetch of an existing person counting too; the message says
|
|
1044
1320
|
* where to write to raise it; `err.resetAt` says when the window ends).
|
|
1045
1321
|
*/
|
|
1322
|
+
// route: POST /server/people
|
|
1046
1323
|
async invitePerson(email) {
|
|
1047
1324
|
return this.gate("/server/people", {
|
|
1048
1325
|
method: "POST",
|
|
@@ -1065,6 +1342,7 @@ export class GemmeinServer {
|
|
|
1065
1342
|
* `person_not_found` (404 — no person with this id in this app and
|
|
1066
1343
|
* environment; existence is never leaked).
|
|
1067
1344
|
*/
|
|
1345
|
+
// route: GET /server/people/{personId}/holdings
|
|
1068
1346
|
async holdings(personId) {
|
|
1069
1347
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/holdings`);
|
|
1070
1348
|
}
|
|
@@ -1097,6 +1375,7 @@ export class GemmeinServer {
|
|
|
1097
1375
|
* `invalid_entitlement` / `unknown_plan` (no plan or product by that
|
|
1098
1376
|
* name — the owner adds it on the Payments page) · `person_not_found`.
|
|
1099
1377
|
*/
|
|
1378
|
+
// route: POST /server/people/{personId}/grants
|
|
1100
1379
|
async grantAccess(personId, input) {
|
|
1101
1380
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/grants`, {
|
|
1102
1381
|
method: "POST",
|
|
@@ -1135,6 +1414,7 @@ export class GemmeinServer {
|
|
|
1135
1414
|
* `invalid_reason` / `invalid_key` (400) · `dedupe_conflict` (409 — the
|
|
1136
1415
|
* key already names a different movement).
|
|
1137
1416
|
*/
|
|
1417
|
+
// route: POST /server/people/{personId}/credits/spend
|
|
1138
1418
|
async spendCredits(personId, input) {
|
|
1139
1419
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/credits/spend`, {
|
|
1140
1420
|
method: "POST",
|
|
@@ -1162,6 +1442,7 @@ export class GemmeinServer {
|
|
|
1162
1442
|
* `grant_not_found` (404 — not this person's grant, in this app and
|
|
1163
1443
|
* environment; existence is never leaked) · `already_revoked` (409).
|
|
1164
1444
|
*/
|
|
1445
|
+
// route: POST /server/people/{personId}/grants/{grantId}/revoke
|
|
1165
1446
|
async revokeAccess(personId, grantId, input = {}) {
|
|
1166
1447
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST", body: JSON.stringify({ ...(input.reason ? { reason: input.reason } : {}) }) });
|
|
1167
1448
|
}
|
|
@@ -1173,7 +1454,7 @@ export class GemmeinServer {
|
|
|
1173
1454
|
const headers = { "x-app-key": this.secretKey, "x-client-info": CLIENT_INFO };
|
|
1174
1455
|
if (init.body)
|
|
1175
1456
|
headers["content-type"] = "application/json";
|
|
1176
|
-
const response = await fetch(new URL(path, this.apiUrl), { ...init, headers });
|
|
1457
|
+
const response = await this.fetch(new URL(path, this.apiUrl), { ...init, headers });
|
|
1177
1458
|
if (!response.ok) {
|
|
1178
1459
|
throw new GemmeinError({ status: response.status, ...(await readErrorBody(response)) });
|
|
1179
1460
|
}
|
|
@@ -1181,14 +1462,17 @@ export class GemmeinServer {
|
|
|
1181
1462
|
}
|
|
1182
1463
|
}
|
|
1183
1464
|
class ServerCollectionClient {
|
|
1184
|
-
constructor(apiUrl, secretKey, name) {
|
|
1465
|
+
constructor(apiUrl, secretKey, name, fetchImpl) {
|
|
1185
1466
|
this.apiUrl = apiUrl;
|
|
1186
1467
|
this.secretKey = secretKey;
|
|
1187
1468
|
this.name = name;
|
|
1469
|
+
this.fetch = fetchImpl;
|
|
1188
1470
|
}
|
|
1471
|
+
// route: GET /storage/{collection}/{id}
|
|
1189
1472
|
async get(id) {
|
|
1190
1473
|
return this.request(`/${encodeURIComponent(id)}`);
|
|
1191
1474
|
}
|
|
1475
|
+
// route: GET /storage/{collection}
|
|
1192
1476
|
async list(options = {}) {
|
|
1193
1477
|
const query = new URLSearchParams();
|
|
1194
1478
|
if (options.limit !== undefined)
|
|
@@ -1210,6 +1494,7 @@ class ServerCollectionClient {
|
|
|
1210
1494
|
const qs = query.toString();
|
|
1211
1495
|
return this.request(qs ? `?${qs}` : "");
|
|
1212
1496
|
}
|
|
1497
|
+
// route: PATCH /storage/{collection}/{id}
|
|
1213
1498
|
async update(id, data) {
|
|
1214
1499
|
return this.request(`/${encodeURIComponent(id)}`, {
|
|
1215
1500
|
method: "PATCH",
|
|
@@ -1224,7 +1509,7 @@ class ServerCollectionClient {
|
|
|
1224
1509
|
if (init.body) {
|
|
1225
1510
|
headers["content-type"] = "application/json";
|
|
1226
1511
|
}
|
|
1227
|
-
const response = await fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.apiUrl), { ...init, headers });
|
|
1512
|
+
const response = await this.fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.apiUrl), { ...init, headers });
|
|
1228
1513
|
if (response.status === 204)
|
|
1229
1514
|
return undefined;
|
|
1230
1515
|
if (!response.ok) {
|
|
@@ -1284,7 +1569,7 @@ async function readErrorBody(response) {
|
|
|
1284
1569
|
// One request path for every runtime client (auth, subscriptions,
|
|
1285
1570
|
// payments, account) — same headers, same error handling, same signposts.
|
|
1286
1571
|
async function runtimeRequest(config, path, init = {}) {
|
|
1287
|
-
const response = await fetch(new URL(path, config.apiUrl), {
|
|
1572
|
+
const response = await config.fetch(new URL(path, config.apiUrl), {
|
|
1288
1573
|
...init,
|
|
1289
1574
|
headers: await runtimeHeaders(config, init.headers)
|
|
1290
1575
|
});
|
|
@@ -1295,7 +1580,7 @@ async function runtimeHeaders(config, headers) {
|
|
|
1295
1580
|
return {
|
|
1296
1581
|
...headers,
|
|
1297
1582
|
"x-app-key": config.appKey,
|
|
1298
|
-
"x-client-info":
|
|
1583
|
+
"x-client-info": config.clientInfo,
|
|
1299
1584
|
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
1300
1585
|
};
|
|
1301
1586
|
}
|