@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.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AiClient = exports.CreditsClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.FilesClient = exports.PurchasesClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = exports.CLIENT_INFO = exports.SDK_VERSION = void 0;
|
|
4
|
+
exports.clientInfoFor = clientInfoFor;
|
|
4
5
|
exports.gemmein = gemmein;
|
|
5
6
|
exports.gemmeinServer = gemmeinServer;
|
|
6
7
|
/** This build's version — the value `package.json` carries. Kept inline
|
|
@@ -9,13 +10,83 @@ exports.gemmeinServer = gemmeinServer;
|
|
|
9
10
|
* second module). `scripts/sync-version.mjs` rewrites the literal from
|
|
10
11
|
* package.json before every build (`prebuild`), and a test pins the two
|
|
11
12
|
* equal, so a bump can never ship with a stale header. */
|
|
12
|
-
exports.SDK_VERSION = "0.
|
|
13
|
+
exports.SDK_VERSION = "0.10.0"; // synced from package.json — do not edit by hand
|
|
13
14
|
/** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
|
|
14
15
|
* `x-client-info: gemmein-sdk/<version>`. The server records it on the
|
|
15
16
|
* secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
|
|
16
17
|
* misbehaving integration can be attributed to an SDK version from day
|
|
17
18
|
* one. It is a report, not a proof — any caller can set it. */
|
|
18
19
|
exports.CLIENT_INFO = `gemmein-sdk/${exports.SDK_VERSION}`;
|
|
20
|
+
/** W10 §1 A: a mobile entry adds its platform — `gemmein-sdk/<version>
|
|
21
|
+
* expo-ios`. The ledger that records this header caps it at 64 characters
|
|
22
|
+
* and strips control characters (`MAX_CLIENT_LENGTH` / `capClient`,
|
|
23
|
+
* packages/db/src/keyUsageStore.ts), so the value is cleaned and capped
|
|
24
|
+
* HERE: a tag that arrives truncated attributes nothing. Anything outside
|
|
25
|
+
* `[A-Za-z0-9._/-]` collapses to a hyphen, so the label can never carry a
|
|
26
|
+
* newline — or a second space — into the ledger. */
|
|
27
|
+
function clientInfoFor(platform) {
|
|
28
|
+
if (typeof platform !== "string")
|
|
29
|
+
return exports.CLIENT_INFO;
|
|
30
|
+
const clean = platform.replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31
|
+
if (clean.length === 0)
|
|
32
|
+
return exports.CLIENT_INFO;
|
|
33
|
+
return `${exports.CLIENT_INFO} ${clean}`.slice(0, 64);
|
|
34
|
+
}
|
|
35
|
+
/** W10 §1 A: every request goes through ONE function, so a platform that
|
|
36
|
+
* needs its own fetch injects it once. The default reads `globalThis.fetch`
|
|
37
|
+
* at call time (a polyfill installed later still counts), and both forms
|
|
38
|
+
* are invoked as plain calls — which is what keeps a browser's
|
|
39
|
+
* `window.fetch` legal when it is handed over as a bare reference.
|
|
40
|
+
*
|
|
41
|
+
* W10 row 9, found by driving the Expo app with the engine stopped: a
|
|
42
|
+
* transport failure — connection refused, no DNS, no Wi-Fi, an `apiUrl`
|
|
43
|
+
* pointing at nothing — never reaches `handleResponse`, so whatever the
|
|
44
|
+
* fetch implementation threw came out unchanged (a `TypeError` in a
|
|
45
|
+
* browser and on Expo's fetch). An app branching on `err.code`, which is
|
|
46
|
+
* what every teaching tells it to do, got `undefined` there and nowhere
|
|
47
|
+
* else. EVERY request the client makes — the runtime clients, a
|
|
48
|
+
* collection, the S3 upload POST, both AI streams — goes through this one
|
|
49
|
+
* wrapper, so every one of them now answers a `GemmeinError`
|
|
50
|
+
* `network_unreachable` (status 0, the SDK's convention for "no HTTP
|
|
51
|
+
* status applies"), with the fetch's own error kept on `err.cause`.
|
|
52
|
+
*
|
|
53
|
+
* Two throws pass through untouched: an `AbortError` — the caller
|
|
54
|
+
* cancelled on purpose, and `{ signal }` is a documented option on the AI
|
|
55
|
+
* calls — and a `GemmeinError` an injected fetch raised itself. */
|
|
56
|
+
function resolveFetch(injected, apiUrl) {
|
|
57
|
+
const call = injected
|
|
58
|
+
? (input, init) => injected(input, init)
|
|
59
|
+
: (input, init) => globalThis.fetch(input, init);
|
|
60
|
+
// The host is read ONCE, here, and only for the sentence: a client whose
|
|
61
|
+
// `apiUrl` is unparseable still gets a readable error, never a second throw.
|
|
62
|
+
const host = (() => {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(apiUrl).host;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return apiUrl;
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
return (async (input, init) => {
|
|
71
|
+
try {
|
|
72
|
+
return await call(input, init);
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
// The caller's own cancel, and an SDK refusal an injected fetch chose
|
|
76
|
+
// to raise, are both already typed — re-typing them would lie.
|
|
77
|
+
if (cause instanceof GemmeinError)
|
|
78
|
+
throw cause;
|
|
79
|
+
if (typeof cause === "object" && cause !== null && cause.name === "AbortError")
|
|
80
|
+
throw cause;
|
|
81
|
+
throw new GemmeinError({
|
|
82
|
+
status: 0,
|
|
83
|
+
code: "network_unreachable",
|
|
84
|
+
message: `Gemmein could not be reached — check the connection and the apiUrl (${host})`,
|
|
85
|
+
cause,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
19
90
|
class GemmeinError extends Error {
|
|
20
91
|
constructor(input) {
|
|
21
92
|
super(input.message);
|
|
@@ -24,6 +95,7 @@ class GemmeinError extends Error {
|
|
|
24
95
|
this.code = input.code;
|
|
25
96
|
this.resetAt = input.resetAt;
|
|
26
97
|
this.requires = input.requires;
|
|
98
|
+
this.cause = input.cause;
|
|
27
99
|
}
|
|
28
100
|
}
|
|
29
101
|
exports.GemmeinError = GemmeinError;
|
|
@@ -42,8 +114,15 @@ exports.MemoryTokenStore = MemoryTokenStore;
|
|
|
42
114
|
// Persists the session across page reloads — the default in browsers.
|
|
43
115
|
// Sessions are long-lived server-side; a memory-only default would log
|
|
44
116
|
// users out on every refresh. Keyed per app key so two Gemmein apps on
|
|
45
|
-
// one origin never share a token.
|
|
46
|
-
//
|
|
117
|
+
// one origin never share a token.
|
|
118
|
+
//
|
|
119
|
+
// READS AND CLEARS ARE LENIENT, WRITES ARE NOT — the same law the two
|
|
120
|
+
// mobile stores keep (`SecureStoreTokenStore` in `@gemmein/sdk/expo`,
|
|
121
|
+
// `KeychainTokenStore` in GemmeinSwift). A store that cannot be READ means
|
|
122
|
+
// signed out, which every app already handles; a store that cannot KEEP
|
|
123
|
+
// the session has broken the promise it exists for, and silence there
|
|
124
|
+
// signs the person out on the next reload with nothing anywhere saying
|
|
125
|
+
// why.
|
|
47
126
|
class BrowserTokenStore {
|
|
48
127
|
constructor(appKey) {
|
|
49
128
|
this.key = `gemmein_session_${appKey.slice(0, 20)}`;
|
|
@@ -56,11 +135,30 @@ class BrowserTokenStore {
|
|
|
56
135
|
return undefined;
|
|
57
136
|
}
|
|
58
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* A blocked `localStorage` — Safari's private mode past its quota, a
|
|
140
|
+
* browser set to block site data, a sandboxed iframe whose access throws
|
|
141
|
+
* — throws `secure_store_unavailable` (status 0), the same code the
|
|
142
|
+
* mobile stores answer, with the browser's own error on `cause`.
|
|
143
|
+
* `auth.verifyEmailCode()` carries it to the caller: the session is real
|
|
144
|
+
* (the server minted it), so an app that would rather run than stop
|
|
145
|
+
* catches this one code and rebuilds its client with a
|
|
146
|
+
* `MemoryTokenStore`, which never throws.
|
|
147
|
+
*/
|
|
59
148
|
set(token) {
|
|
60
149
|
try {
|
|
61
150
|
window.localStorage.setItem(this.key, token);
|
|
62
151
|
}
|
|
63
|
-
catch {
|
|
152
|
+
catch (cause) {
|
|
153
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
154
|
+
throw new GemmeinError({
|
|
155
|
+
status: 0,
|
|
156
|
+
code: "secure_store_unavailable",
|
|
157
|
+
message: `The session could not be stored: this browser refused the localStorage write (${detail}) ` +
|
|
158
|
+
"— pass a tokenStore, e.g. new MemoryTokenStore()",
|
|
159
|
+
cause,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
64
162
|
}
|
|
65
163
|
clear() {
|
|
66
164
|
try {
|
|
@@ -112,10 +210,14 @@ class Gemmein {
|
|
|
112
210
|
'copy the key from the Setup page. Then: gemmein("pk_...")'
|
|
113
211
|
});
|
|
114
212
|
}
|
|
213
|
+
const apiUrl = options.apiUrl ?? "https://api.gemmein.com";
|
|
115
214
|
const config = {
|
|
116
|
-
apiUrl
|
|
215
|
+
apiUrl,
|
|
117
216
|
appKey: options.appKey,
|
|
118
|
-
tokenStore: options.tokenStore ?? defaultTokenStore(options.appKey)
|
|
217
|
+
tokenStore: options.tokenStore ?? defaultTokenStore(options.appKey),
|
|
218
|
+
fetch: resolveFetch(options.fetch, apiUrl),
|
|
219
|
+
visibility: options.visibility,
|
|
220
|
+
clientInfo: clientInfoFor(options.platform)
|
|
119
221
|
};
|
|
120
222
|
this.auth = new AuthClient(config);
|
|
121
223
|
this.storage = new StorageClient(config);
|
|
@@ -137,6 +239,7 @@ class Gemmein {
|
|
|
137
239
|
* suggestion attached — always pass it when you aren't certain the
|
|
138
240
|
* collection exists yet. The cloud ignores it.
|
|
139
241
|
*/
|
|
242
|
+
// route: none
|
|
140
243
|
collection(name, options = {}) {
|
|
141
244
|
return this.storage.collection(name, options);
|
|
142
245
|
}
|
|
@@ -162,6 +265,7 @@ class AuthClient {
|
|
|
162
265
|
constructor(config) {
|
|
163
266
|
this.config = config;
|
|
164
267
|
}
|
|
268
|
+
// route: POST /auth/email/start
|
|
165
269
|
async sendEmailCode(email) {
|
|
166
270
|
await this.request("/auth/email/start", {
|
|
167
271
|
method: "POST",
|
|
@@ -169,6 +273,7 @@ class AuthClient {
|
|
|
169
273
|
headers: { "content-type": "application/json" }
|
|
170
274
|
});
|
|
171
275
|
}
|
|
276
|
+
// route: POST /auth/email/verify
|
|
172
277
|
async verifyEmailCode(input) {
|
|
173
278
|
const result = await this.request("/auth/email/verify", {
|
|
174
279
|
method: "POST",
|
|
@@ -189,15 +294,33 @@ class AuthClient {
|
|
|
189
294
|
message: "Gemmein auth response did not include a session token"
|
|
190
295
|
});
|
|
191
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* End this device's session. IDEMPOTENT: signing out of a session that is
|
|
299
|
+
* already gone is the outcome asked for, not a failure.
|
|
300
|
+
*
|
|
301
|
+
* W10 row 9, found by driving the Expo app: the commonest way this
|
|
302
|
+
* round-trip fails is `401 auth_expired` — the owner already signed this
|
|
303
|
+
* person out everywhere from the console, which is the remedy for a
|
|
304
|
+
* stolen phone. The session IS ended; throwing there made an app that did
|
|
305
|
+
* everything right show an error for the thing it asked for. Every other
|
|
306
|
+
* failure still throws, and the token store is cleared either way.
|
|
307
|
+
*/
|
|
308
|
+
// route: POST /auth/logout
|
|
192
309
|
async logout() {
|
|
193
310
|
try {
|
|
194
311
|
await this.request("/auth/logout", { method: "POST" });
|
|
195
312
|
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const alreadyEnded = err instanceof GemmeinError && err.status === 401 && err.code === "auth_expired";
|
|
315
|
+
if (!alreadyEnded)
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
196
318
|
finally {
|
|
197
319
|
// Even if the network call fails, the user asked to be signed out.
|
|
198
320
|
await this.config.tokenStore.clear();
|
|
199
321
|
}
|
|
200
322
|
}
|
|
323
|
+
// route: GET /auth/current-user
|
|
201
324
|
async currentUser() {
|
|
202
325
|
// Contract: "who am I?" never throws for session state — the server
|
|
203
326
|
// answers 200 {authenticated:false} for a token it won't honour right
|
|
@@ -255,6 +378,7 @@ class PurchasesClient {
|
|
|
255
378
|
* or `{ type: "external_url", url }` (a plain handover). A refunded
|
|
256
379
|
* purchase never carries delivery.
|
|
257
380
|
*/
|
|
381
|
+
// route: GET /auth/purchases
|
|
258
382
|
async mine() {
|
|
259
383
|
const result = (await runtimeRequest(this.config, "/auth/purchases"));
|
|
260
384
|
return result.purchases;
|
|
@@ -287,6 +411,7 @@ class FilesClient {
|
|
|
287
411
|
constructor(config) {
|
|
288
412
|
this.config = config;
|
|
289
413
|
}
|
|
414
|
+
// route: GET /files/{ref}/link
|
|
290
415
|
async link(ref, options = {}) {
|
|
291
416
|
const query = options.intent === "download" ? "?intent=download" : "";
|
|
292
417
|
return (await runtimeRequest(this.config, `/files/${encodeURIComponent(String(ref))}/link${query}`));
|
|
@@ -304,6 +429,7 @@ class SubscriptionsClient {
|
|
|
304
429
|
* nobody is signed in — a data route, not the never-throw current-user
|
|
305
430
|
* contract.
|
|
306
431
|
*/
|
|
432
|
+
// route: GET /auth/subscription
|
|
307
433
|
async mine() {
|
|
308
434
|
const result = (await runtimeRequest(this.config, "/auth/subscription"));
|
|
309
435
|
return result.subscription;
|
|
@@ -317,6 +443,7 @@ class SubscriptionsClient {
|
|
|
317
443
|
* and a plan whose Payment Link the app owner has pasted in their
|
|
318
444
|
* dashboard; omit `plan` to buy the app's paid plan.
|
|
319
445
|
*/
|
|
446
|
+
// route: GET /auth/checkout
|
|
320
447
|
async checkout(plan) {
|
|
321
448
|
const query = plan ? `?plan=${encodeURIComponent(plan)}` : "";
|
|
322
449
|
const result = (await runtimeRequest(this.config, `/auth/checkout${query}`));
|
|
@@ -338,10 +465,16 @@ class PaymentsClient {
|
|
|
338
465
|
* WHAT is being bought when one product covers many things (e.g. a
|
|
339
466
|
* license tier across a catalog):
|
|
340
467
|
* `g.payments.buy("premium license", { item: "beat_37" })`.
|
|
341
|
-
*
|
|
342
|
-
* the
|
|
343
|
-
*
|
|
468
|
+
* Gemmein records every completed payment itself: `g.purchases.mine()` is
|
|
469
|
+
* the buyer's proof, and a product that delivers a file carries
|
|
470
|
+
* `delivery` on it. Gate fulfilment on the purchase (or the entitlement it
|
|
471
|
+
* granted), never on the redirect coming back. A receipts collection is
|
|
472
|
+
* optional, for proof records only.
|
|
473
|
+
*
|
|
474
|
+
* A product sold via a RELAY, or not sold yet, has no Payment Link to
|
|
475
|
+
* open: this answers 409 `product_not_sellable`.
|
|
344
476
|
*/
|
|
477
|
+
// route: GET /auth/pay
|
|
345
478
|
async buy(product, options) {
|
|
346
479
|
const params = new URLSearchParams({ product });
|
|
347
480
|
if (options?.item)
|
|
@@ -367,6 +500,7 @@ class AccountClient {
|
|
|
367
500
|
* subscription row removed. Irreversible — put a real confirm in front
|
|
368
501
|
* of it.
|
|
369
502
|
*/
|
|
503
|
+
// route: POST /auth/delete-account
|
|
370
504
|
async delete() {
|
|
371
505
|
const result = await runtimeRequest(this.config, "/auth/delete-account", { method: "POST" });
|
|
372
506
|
await this.config.tokenStore.clear();
|
|
@@ -385,22 +519,31 @@ class CreditsClient {
|
|
|
385
519
|
*
|
|
386
520
|
* const { balance } = await g.credits.balance();
|
|
387
521
|
*/
|
|
522
|
+
// route: GET /auth/credits
|
|
388
523
|
async balance() {
|
|
389
524
|
return runtimeRequest(this.config, "/auth/credits");
|
|
390
525
|
}
|
|
391
526
|
}
|
|
392
527
|
exports.CreditsClient = CreditsClient;
|
|
393
528
|
/**
|
|
394
|
-
* The AI route.
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* `
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
529
|
+
* The AI route. The primary path is a NAMED TOOL defined on the server:
|
|
530
|
+
* `run(name, inputs)` sends a name and inputs, the server composes the
|
|
531
|
+
* provider request from the tool's own instructions and template (never
|
|
532
|
+
* the browser), gates it, spends the tool's credits and streams the
|
|
533
|
+
* answer back; `runText` is the same call collected to one string;
|
|
534
|
+
* `calls()` is the signed-in person's own history. `chat` is the RAW
|
|
535
|
+
* call: it takes the provider's own request body — exactly what you would
|
|
536
|
+
* POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages or
|
|
537
|
+
* Google's generateContent — and answers with the fetch `Response`
|
|
538
|
+
* untouched, streaming intact (SSE stays SSE). Raw calls are off by
|
|
539
|
+
* default for every provider key (`raw_calls_off`, 403) until the founder
|
|
540
|
+
* switches them on for that key on the AI tools page. Gemmein spends a
|
|
541
|
+
* credit, adds the owner's key, forwards, and passes status and bytes
|
|
542
|
+
* back. Pass `tool` (W9.3b) to a raw call to price and gate it as a named
|
|
543
|
+
* tool instead of the implicit default (one credit, any allowed model, no
|
|
544
|
+
* gate). Response headers: `x-gemmein-credits-remaining` on every answer
|
|
545
|
+
* that passed the spend; `x-gemmein-credit: refunded` when the provider
|
|
546
|
+
* failed before its first byte.
|
|
404
547
|
*/
|
|
405
548
|
class AiClient {
|
|
406
549
|
constructor(config) {
|
|
@@ -411,7 +554,10 @@ class AiClient {
|
|
|
411
554
|
* for await (const chunk of res.body) { … }
|
|
412
555
|
*
|
|
413
556
|
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
414
|
-
* Refusals, all `GemmeinError`: `
|
|
557
|
+
* Refusals, all `GemmeinError`: `raw_calls_off` (403 — raw calls are off
|
|
558
|
+
* for this provider until the founder switches them on for its key on
|
|
559
|
+
* the AI tools page; call a named tool with `run` instead) ·
|
|
560
|
+
* `session_required` (401) ·
|
|
415
561
|
* `credits_exhausted` (402 — the message carries the balance; show your
|
|
416
562
|
* own "buy more" door, which is a product checkout) · `ai_not_configured`
|
|
417
563
|
* (409 — the owner has set no key) · `provider_required` (400) ·
|
|
@@ -430,12 +576,13 @@ class AiClient {
|
|
|
430
576
|
* `x-gemmein-credit: refunded`). `x-gemmein-tool` names the tool; absent
|
|
431
577
|
* on the implicit default.
|
|
432
578
|
*/
|
|
579
|
+
// route: POST /ai/chat
|
|
433
580
|
async chat(body, options = {}) {
|
|
434
581
|
const payload = options.provider ? { provider: options.provider, ...body } : body;
|
|
435
582
|
const url = new URL("/ai/chat", this.config.apiUrl);
|
|
436
583
|
if (options.tool)
|
|
437
584
|
url.searchParams.set("tool", options.tool);
|
|
438
|
-
const response = await fetch(url, {
|
|
585
|
+
const response = await this.config.fetch(url, {
|
|
439
586
|
method: "POST",
|
|
440
587
|
body: JSON.stringify(payload),
|
|
441
588
|
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
|
@@ -457,6 +604,94 @@ class AiClient {
|
|
|
457
604
|
}
|
|
458
605
|
return response;
|
|
459
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* W9.6: run a named tool with INPUTS — the server composes the provider
|
|
609
|
+
* request from the tool's own instructions and template (never the
|
|
610
|
+
* browser), gates it, spends its credits and streams the answer back.
|
|
611
|
+
* The answer is the provider's own shape for the tool's provider (SSE
|
|
612
|
+
* when `stream`), so read it as you would `chat()`'s.
|
|
613
|
+
*
|
|
614
|
+
* const res = await g.ai.run("summarise", { text }, { stream: true });
|
|
615
|
+
*
|
|
616
|
+
* Browser sessions only — a server key is refused (`scope_denied`, 403).
|
|
617
|
+
* Refusals, all `GemmeinError`: `session_required` (401 — sign in
|
|
618
|
+
* first) · `ai_capped` (429 — 20 calls a minute per person;
|
|
619
|
+
* `err.resetAt`) · `unknown_tool` (404 — no tool by that name in this
|
|
620
|
+
* environment) · `tool_disabled` (403 — the owner switched it off) ·
|
|
621
|
+
* `entitlement_required` (403 — the message names the plan or product
|
|
622
|
+
* it needs) · `payload_too_large` (413 — inputs over 64 KB) ·
|
|
623
|
+
* `invalid_body` (400 — the body must be a JSON object
|
|
624
|
+
* `{ inputs, stream? }`) · `invalid_inputs` (400 — the message names the
|
|
625
|
+
* input and the rule) · `tool_incomplete` (409 — the tool composes
|
|
626
|
+
* nothing; a founder's fix) · `ai_not_configured` (409 — the tool's
|
|
627
|
+
* provider has no key set; the owner pastes one) · `credits_exhausted`
|
|
628
|
+
* (402 — the message names the tool, its price and the balance) ·
|
|
629
|
+
* `provider_unreachable` (502 — no answer before the first byte; the
|
|
630
|
+
* tool's credits are refunded, header `x-gemmein-credit: refunded`).
|
|
631
|
+
* The provider's own answer — 2xx or not — is returned as it came; read
|
|
632
|
+
* `res.ok` yourself. `x-gemmein-tool` names the tool.
|
|
633
|
+
*/
|
|
634
|
+
// route: POST /ai/run/{tool}
|
|
635
|
+
async run(tool, inputs = {}, options = {}) {
|
|
636
|
+
const url = new URL(`/ai/run/${encodeURIComponent(tool)}`, this.config.apiUrl);
|
|
637
|
+
const response = await this.config.fetch(url, {
|
|
638
|
+
method: "POST",
|
|
639
|
+
body: JSON.stringify({ inputs, ...(options.stream ? { stream: true } : {}) }),
|
|
640
|
+
headers: await runtimeHeaders(this.config, { "content-type": "application/json" }),
|
|
641
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
642
|
+
});
|
|
643
|
+
if (!response.ok) {
|
|
644
|
+
if (isForwardedAnswer(response)) {
|
|
645
|
+
const peek = (await response.clone().json().catch(() => null));
|
|
646
|
+
if (peek?.code !== "provider_unreachable")
|
|
647
|
+
return response;
|
|
648
|
+
}
|
|
649
|
+
const errorBody = await readErrorBody(response);
|
|
650
|
+
if (errorBody.code === "auth_expired")
|
|
651
|
+
await this.config.tokenStore.clear();
|
|
652
|
+
throw new GemmeinError({ status: response.status, ...errorBody });
|
|
653
|
+
}
|
|
654
|
+
return response;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* W9.6: `run()` without a stream, as one string — the text lifted out
|
|
658
|
+
* of the tool's provider's answer (the same readers `text()` uses).
|
|
659
|
+
* `run()`'s refusals, plus: a provider's own non-2xx throws
|
|
660
|
+
* `provider_error` with the provider's status and message; an answer
|
|
661
|
+
* with no text to lift out throws `invalid_response` (status 0).
|
|
662
|
+
*
|
|
663
|
+
* const summary = await g.ai.runText("summarise", { text });
|
|
664
|
+
*/
|
|
665
|
+
// route: POST /ai/run/{tool}
|
|
666
|
+
async runText(tool, inputs = {}, options = {}) {
|
|
667
|
+
const response = await this.run(tool, inputs, options);
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
throw new GemmeinError({ status: response.status, code: "provider_error", message: await providerErrorMessage(response) });
|
|
670
|
+
}
|
|
671
|
+
const data = (await response.json());
|
|
672
|
+
const text = extractAiText(data);
|
|
673
|
+
if (text === null) {
|
|
674
|
+
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" });
|
|
675
|
+
}
|
|
676
|
+
return text;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* W9.6 §16: the signed-in person's OWN AI calls, newest first — what they
|
|
680
|
+
* ran, when, what it cost, how it ended; the prompt and answer only where
|
|
681
|
+
* the tool keeps them. Session required.
|
|
682
|
+
*
|
|
683
|
+
* const { calls, nextCursor } = await g.ai.calls();
|
|
684
|
+
*/
|
|
685
|
+
// route: GET /auth/ai-calls
|
|
686
|
+
async calls(options = {}) {
|
|
687
|
+
const params = new URLSearchParams();
|
|
688
|
+
if (options.limit)
|
|
689
|
+
params.set("limit", String(options.limit));
|
|
690
|
+
if (options.before)
|
|
691
|
+
params.set("before", options.before);
|
|
692
|
+
const query = params.toString();
|
|
693
|
+
return runtimeRequest(this.config, `/auth/ai-calls${query ? `?${query}` : ""}`);
|
|
694
|
+
}
|
|
460
695
|
/**
|
|
461
696
|
* The non-streaming convenience: one call, one string. Pass a body that
|
|
462
697
|
* does NOT stream (`stream` unset or false); the provider's JSON answer is
|
|
@@ -470,6 +705,7 @@ class AiClient {
|
|
|
470
705
|
*
|
|
471
706
|
* const answer = await g.ai.text({ model: "claude-sonnet-4-5", max_tokens: 400, messages });
|
|
472
707
|
*/
|
|
708
|
+
// route: POST /ai/chat
|
|
473
709
|
async text(body, options = {}) {
|
|
474
710
|
const response = await this.chat(body, options);
|
|
475
711
|
if (!response.ok) {
|
|
@@ -558,6 +794,7 @@ class StorageClient {
|
|
|
558
794
|
this.config = config;
|
|
559
795
|
}
|
|
560
796
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
797
|
+
// route: none
|
|
561
798
|
collection(name, options = {}) {
|
|
562
799
|
assertCollectionName(name);
|
|
563
800
|
return new CollectionClient(this.config, name, options);
|
|
@@ -602,6 +839,7 @@ class CollectionClient {
|
|
|
602
839
|
* fake drafts with a data field + client-side filtering: on a public
|
|
603
840
|
* collection the data still reaches everyone.
|
|
604
841
|
*/
|
|
842
|
+
// route: POST /storage/{collection}
|
|
605
843
|
async create(data, options = {}) {
|
|
606
844
|
const query = new URLSearchParams();
|
|
607
845
|
if (options.key !== undefined)
|
|
@@ -621,6 +859,7 @@ class CollectionClient {
|
|
|
621
859
|
* rule (the app owner sees everyone's). Returns `{ records, hasMore }` —
|
|
622
860
|
* an object, not a bare array.
|
|
623
861
|
*/
|
|
862
|
+
// route: GET /storage/{collection}
|
|
624
863
|
async list(options = {}) {
|
|
625
864
|
const query = new URLSearchParams();
|
|
626
865
|
if (options.limit !== undefined)
|
|
@@ -661,6 +900,7 @@ class CollectionClient {
|
|
|
661
900
|
* doubling up to 60s, and honours a rate limit's reset time. One watch per
|
|
662
901
|
* page is the intended shape — share its result, don't stack watchers.
|
|
663
902
|
*/
|
|
903
|
+
// route: GET /storage/{collection}
|
|
664
904
|
watch(onChange, options = {}) {
|
|
665
905
|
const asked = options.every;
|
|
666
906
|
const every = Math.max(5000, Math.min(300000, Number.isFinite(asked) ? asked : 10000));
|
|
@@ -668,6 +908,11 @@ class CollectionClient {
|
|
|
668
908
|
let stopped = false;
|
|
669
909
|
let timer;
|
|
670
910
|
let delayMs = every;
|
|
911
|
+
// W10 §1 A: the platform's own answer to "is the app in front of the
|
|
912
|
+
// user?" when one was injected — `document` when it wasn't. One
|
|
913
|
+
// `unsubscribe` either way, so every teardown path is the same line.
|
|
914
|
+
const vis = this.config.visibility;
|
|
915
|
+
let unsubscribe;
|
|
671
916
|
let watermark;
|
|
672
917
|
// One tick at a time. A visibility flip mid-tick sets resyncPending
|
|
673
918
|
// instead of racing a second loop into existence (each raced loop would
|
|
@@ -733,9 +978,8 @@ class CollectionClient {
|
|
|
733
978
|
clearTimeout(timer);
|
|
734
979
|
timer = undefined;
|
|
735
980
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
981
|
+
unsubscribe?.();
|
|
982
|
+
unsubscribe = undefined;
|
|
739
983
|
if (typeof console !== "undefined")
|
|
740
984
|
console.warn(`gemmein watch stopped: ${err.code} — start a new watch after signing in`);
|
|
741
985
|
return;
|
|
@@ -759,7 +1003,9 @@ class CollectionClient {
|
|
|
759
1003
|
}
|
|
760
1004
|
schedule();
|
|
761
1005
|
};
|
|
762
|
-
const hidden = () =>
|
|
1006
|
+
const hidden = () => (vis
|
|
1007
|
+
? vis.isHidden()
|
|
1008
|
+
: typeof document !== "undefined" && document.visibilityState === "hidden");
|
|
763
1009
|
const schedule = () => {
|
|
764
1010
|
if (stopped || hidden() || timer !== undefined)
|
|
765
1011
|
return;
|
|
@@ -793,8 +1039,12 @@ class CollectionClient {
|
|
|
793
1039
|
void tick(true);
|
|
794
1040
|
}
|
|
795
1041
|
};
|
|
796
|
-
if (
|
|
1042
|
+
if (vis) {
|
|
1043
|
+
unsubscribe = vis.onChange(onVisibility);
|
|
1044
|
+
}
|
|
1045
|
+
else if (typeof document !== "undefined") {
|
|
797
1046
|
document.addEventListener("visibilitychange", onVisibility);
|
|
1047
|
+
unsubscribe = () => document.removeEventListener("visibilitychange", onVisibility);
|
|
798
1048
|
}
|
|
799
1049
|
// Born hidden: wait for the tab — the visibility handler runs the first
|
|
800
1050
|
// sync when the user actually looks.
|
|
@@ -808,12 +1058,12 @@ class CollectionClient {
|
|
|
808
1058
|
if (timer !== undefined)
|
|
809
1059
|
clearTimeout(timer);
|
|
810
1060
|
timer = undefined;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
1061
|
+
unsubscribe?.();
|
|
1062
|
+
unsubscribe = undefined;
|
|
814
1063
|
},
|
|
815
1064
|
};
|
|
816
1065
|
}
|
|
1066
|
+
// route: GET /storage/{collection}/{id}
|
|
817
1067
|
async get(id, options = {}) {
|
|
818
1068
|
const qs = options.expand && options.expand.length > 0 ? `?expand=${encodeURIComponent(options.expand.join(","))}` : "";
|
|
819
1069
|
return this.request(`/${encodeURIComponent(id)}${qs}`);
|
|
@@ -833,6 +1083,7 @@ class CollectionClient {
|
|
|
833
1083
|
* docs), pass `{ ifVersion: record.version }` — a stale save gets a 409
|
|
834
1084
|
* `conflict` instead of clobbering; re-read, reapply, retry.
|
|
835
1085
|
*/
|
|
1086
|
+
// route: PATCH /storage/{collection}/{id}
|
|
836
1087
|
async update(id, data, options = {}) {
|
|
837
1088
|
const query = new URLSearchParams();
|
|
838
1089
|
if (options.ifVersion !== undefined)
|
|
@@ -845,11 +1096,12 @@ class CollectionClient {
|
|
|
845
1096
|
body: JSON.stringify(data)
|
|
846
1097
|
});
|
|
847
1098
|
}
|
|
1099
|
+
// route: DELETE /storage/{collection}/{id}
|
|
848
1100
|
async delete(id) {
|
|
849
1101
|
await this.request(`/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
850
1102
|
}
|
|
851
1103
|
/**
|
|
852
|
-
* Upload a file and get back a REFERENCE — `file
|
|
1104
|
+
* Upload a file and get back a REFERENCE — `file:<uuid>` — not a URL.
|
|
853
1105
|
*
|
|
854
1106
|
* Store the reference. It never expires, it is safe to log and export, and
|
|
855
1107
|
* it grants nothing on its own. To show or download the file, call
|
|
@@ -876,23 +1128,43 @@ class CollectionClient {
|
|
|
876
1128
|
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
877
1129
|
* bug this replaced.
|
|
878
1130
|
*/
|
|
1131
|
+
// route: POST /storage/{collection}/upload
|
|
1132
|
+
// route: POST /storage/{collection}/upload/{fileId}/confirm
|
|
879
1133
|
async upload(file, options) {
|
|
880
|
-
|
|
1134
|
+
// W10 §1 A: `File` is a browser class. Where it does not exist — React
|
|
1135
|
+
// Native — a bare `instanceof File` is a ReferenceError, not a false,
|
|
1136
|
+
// so the guard comes first. The three fields the presign needs are then
|
|
1137
|
+
// read the same way off every shape: a File, a Blob, an
|
|
1138
|
+
// `expo-file-system` File, or the picker's `{ uri, name, type, size }`.
|
|
1139
|
+
const part = file;
|
|
1140
|
+
const name = options?.name
|
|
1141
|
+
?? (typeof File !== "undefined" && file instanceof File
|
|
1142
|
+
? file.name
|
|
1143
|
+
: typeof part.name === "string" ? part.name : "upload");
|
|
881
1144
|
// A Blob built from bytes has type "" — `contentType` names it. The
|
|
882
1145
|
// server proves the bytes either way.
|
|
883
|
-
const contentType = options?.contentType ??
|
|
1146
|
+
const contentType = options?.contentType ?? part.type ?? "";
|
|
1147
|
+
// A presign under one byte is refused ("file must not be empty"): a
|
|
1148
|
+
// Blob knows its own size, a React Native part carries the picker's.
|
|
1149
|
+
const size = typeof part.size === "number" ? part.size : 0;
|
|
884
1150
|
// Step 1: Get presigned upload URL
|
|
885
1151
|
const presign = await this.request("/upload", {
|
|
886
1152
|
method: "POST",
|
|
887
|
-
body: JSON.stringify({ name, size
|
|
1153
|
+
body: JSON.stringify({ name, size, contentType, ...(options?.for ? { for: options.for } : {}) }),
|
|
888
1154
|
});
|
|
889
1155
|
// Step 2: Upload directly to S3 via presigned POST
|
|
890
1156
|
const form = new FormData();
|
|
891
1157
|
for (const [key, value] of Object.entries(presign.fields)) {
|
|
892
1158
|
form.append(key, value);
|
|
893
1159
|
}
|
|
894
|
-
|
|
895
|
-
|
|
1160
|
+
// Must be last — S3 presigned POST requirement. Whatever arrived is
|
|
1161
|
+
// appended AS IT IS: a Blob, a File, or — on a client given React
|
|
1162
|
+
// Native's own fetch — the picker's `{ uri, ... }` part, whose bytes
|
|
1163
|
+
// RN's FormData reads off the device when it serialises. Expo's fetch
|
|
1164
|
+
// refuses that part, so `@gemmein/sdk/expo` has already turned it into
|
|
1165
|
+
// an `expo-file-system` `File` by the time it gets here.
|
|
1166
|
+
form.append("file", file);
|
|
1167
|
+
const s3Response = await this.config.fetch(presign.uploadUrl, { method: "POST", body: form });
|
|
896
1168
|
if (!s3Response.ok) {
|
|
897
1169
|
throw new GemmeinError({
|
|
898
1170
|
status: s3Response.status,
|
|
@@ -907,7 +1179,7 @@ class CollectionClient {
|
|
|
907
1179
|
return confirmed;
|
|
908
1180
|
}
|
|
909
1181
|
async request(suffix, init = {}) {
|
|
910
|
-
const response = await fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.config.apiUrl), {
|
|
1182
|
+
const response = await this.config.fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.config.apiUrl), {
|
|
911
1183
|
...init,
|
|
912
1184
|
headers: await runtimeHeaders(this.config, {
|
|
913
1185
|
"content-type": "application/json",
|
|
@@ -937,10 +1209,12 @@ class GemmeinServer {
|
|
|
937
1209
|
}
|
|
938
1210
|
this.apiUrl = options.apiUrl ?? "https://api.gemmein.com";
|
|
939
1211
|
this.secretKey = options.secretKey;
|
|
1212
|
+
this.fetch = resolveFetch(options.fetch, this.apiUrl);
|
|
940
1213
|
}
|
|
1214
|
+
// route: none
|
|
941
1215
|
collection(name) {
|
|
942
1216
|
assertCollectionName(name);
|
|
943
|
-
return new ServerCollectionClient(this.apiUrl, this.secretKey, name);
|
|
1217
|
+
return new ServerCollectionClient(this.apiUrl, this.secretKey, name, this.fetch);
|
|
944
1218
|
}
|
|
945
1219
|
/**
|
|
946
1220
|
* Mint a member session for a test email WITHOUT an OTP round-trip — so a CI
|
|
@@ -950,6 +1224,7 @@ class GemmeinServer {
|
|
|
950
1224
|
* `sk_live` key, and the server refuses it too. Never ship this in app code.
|
|
951
1225
|
* Pass the returned `token` to `gemmein(pk, { tokenStore })` to act as that user.
|
|
952
1226
|
*/
|
|
1227
|
+
// route: POST /server/test-session
|
|
953
1228
|
async testSession(email) {
|
|
954
1229
|
if (this.secretKey.startsWith("sk_live")) {
|
|
955
1230
|
throw new GemmeinError({
|
|
@@ -958,7 +1233,7 @@ class GemmeinServer {
|
|
|
958
1233
|
message: "test sessions are only available in a development environment — never with a live (sk_live) key",
|
|
959
1234
|
});
|
|
960
1235
|
}
|
|
961
|
-
const response = await fetch(new URL("/server/test-session", this.apiUrl), {
|
|
1236
|
+
const response = await this.fetch(new URL("/server/test-session", this.apiUrl), {
|
|
962
1237
|
method: "POST",
|
|
963
1238
|
headers: { "x-app-key": this.secretKey, "x-client-info": exports.CLIENT_INFO, "content-type": "application/json" },
|
|
964
1239
|
body: JSON.stringify({ email }),
|
|
@@ -995,8 +1270,9 @@ class GemmeinServer {
|
|
|
995
1270
|
* (a security notice must never lose to five order emails); misusing it
|
|
996
1271
|
* for campaigns is visible in your own audit trail.
|
|
997
1272
|
*/
|
|
1273
|
+
// route: POST /server/notify
|
|
998
1274
|
async notify(personId, input) {
|
|
999
|
-
const response = await fetch(new URL("/server/notify", this.apiUrl), {
|
|
1275
|
+
const response = await this.fetch(new URL("/server/notify", this.apiUrl), {
|
|
1000
1276
|
method: "POST",
|
|
1001
1277
|
headers: { "x-app-key": this.secretKey, "x-client-info": exports.CLIENT_INFO, "content-type": "application/json" },
|
|
1002
1278
|
body: JSON.stringify({ personId, subject: input.subject, text: input.text, ...(input.kind ? { kind: input.kind } : {}), ...(input.key ? { key: input.key } : {}) }),
|
|
@@ -1033,6 +1309,7 @@ class GemmeinServer {
|
|
|
1033
1309
|
* off until the owner reactivates them in the dashboard) ·
|
|
1034
1310
|
* `invalid_body` (token missing, not a string, or over 512 chars).
|
|
1035
1311
|
*/
|
|
1312
|
+
// route: POST /server/verify-session
|
|
1036
1313
|
async verifySession(token) {
|
|
1037
1314
|
return this.gate("/server/verify-session", {
|
|
1038
1315
|
method: "POST",
|
|
@@ -1062,6 +1339,7 @@ class GemmeinServer {
|
|
|
1062
1339
|
* `invite_capped` (429 — 500 invite calls per app per day, a fetch of an existing person counting too; the message says
|
|
1063
1340
|
* where to write to raise it; `err.resetAt` says when the window ends).
|
|
1064
1341
|
*/
|
|
1342
|
+
// route: POST /server/people
|
|
1065
1343
|
async invitePerson(email) {
|
|
1066
1344
|
return this.gate("/server/people", {
|
|
1067
1345
|
method: "POST",
|
|
@@ -1084,6 +1362,7 @@ class GemmeinServer {
|
|
|
1084
1362
|
* `person_not_found` (404 — no person with this id in this app and
|
|
1085
1363
|
* environment; existence is never leaked).
|
|
1086
1364
|
*/
|
|
1365
|
+
// route: GET /server/people/{personId}/holdings
|
|
1087
1366
|
async holdings(personId) {
|
|
1088
1367
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/holdings`);
|
|
1089
1368
|
}
|
|
@@ -1116,6 +1395,7 @@ class GemmeinServer {
|
|
|
1116
1395
|
* `invalid_entitlement` / `unknown_plan` (no plan or product by that
|
|
1117
1396
|
* name — the owner adds it on the Payments page) · `person_not_found`.
|
|
1118
1397
|
*/
|
|
1398
|
+
// route: POST /server/people/{personId}/grants
|
|
1119
1399
|
async grantAccess(personId, input) {
|
|
1120
1400
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/grants`, {
|
|
1121
1401
|
method: "POST",
|
|
@@ -1154,6 +1434,7 @@ class GemmeinServer {
|
|
|
1154
1434
|
* `invalid_reason` / `invalid_key` (400) · `dedupe_conflict` (409 — the
|
|
1155
1435
|
* key already names a different movement).
|
|
1156
1436
|
*/
|
|
1437
|
+
// route: POST /server/people/{personId}/credits/spend
|
|
1157
1438
|
async spendCredits(personId, input) {
|
|
1158
1439
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/credits/spend`, {
|
|
1159
1440
|
method: "POST",
|
|
@@ -1181,6 +1462,7 @@ class GemmeinServer {
|
|
|
1181
1462
|
* `grant_not_found` (404 — not this person's grant, in this app and
|
|
1182
1463
|
* environment; existence is never leaked) · `already_revoked` (409).
|
|
1183
1464
|
*/
|
|
1465
|
+
// route: POST /server/people/{personId}/grants/{grantId}/revoke
|
|
1184
1466
|
async revokeAccess(personId, grantId, input = {}) {
|
|
1185
1467
|
return this.gate(`/server/people/${encodeURIComponent(personId)}/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST", body: JSON.stringify({ ...(input.reason ? { reason: input.reason } : {}) }) });
|
|
1186
1468
|
}
|
|
@@ -1192,7 +1474,7 @@ class GemmeinServer {
|
|
|
1192
1474
|
const headers = { "x-app-key": this.secretKey, "x-client-info": exports.CLIENT_INFO };
|
|
1193
1475
|
if (init.body)
|
|
1194
1476
|
headers["content-type"] = "application/json";
|
|
1195
|
-
const response = await fetch(new URL(path, this.apiUrl), { ...init, headers });
|
|
1477
|
+
const response = await this.fetch(new URL(path, this.apiUrl), { ...init, headers });
|
|
1196
1478
|
if (!response.ok) {
|
|
1197
1479
|
throw new GemmeinError({ status: response.status, ...(await readErrorBody(response)) });
|
|
1198
1480
|
}
|
|
@@ -1201,14 +1483,17 @@ class GemmeinServer {
|
|
|
1201
1483
|
}
|
|
1202
1484
|
exports.GemmeinServer = GemmeinServer;
|
|
1203
1485
|
class ServerCollectionClient {
|
|
1204
|
-
constructor(apiUrl, secretKey, name) {
|
|
1486
|
+
constructor(apiUrl, secretKey, name, fetchImpl) {
|
|
1205
1487
|
this.apiUrl = apiUrl;
|
|
1206
1488
|
this.secretKey = secretKey;
|
|
1207
1489
|
this.name = name;
|
|
1490
|
+
this.fetch = fetchImpl;
|
|
1208
1491
|
}
|
|
1492
|
+
// route: GET /storage/{collection}/{id}
|
|
1209
1493
|
async get(id) {
|
|
1210
1494
|
return this.request(`/${encodeURIComponent(id)}`);
|
|
1211
1495
|
}
|
|
1496
|
+
// route: GET /storage/{collection}
|
|
1212
1497
|
async list(options = {}) {
|
|
1213
1498
|
const query = new URLSearchParams();
|
|
1214
1499
|
if (options.limit !== undefined)
|
|
@@ -1230,6 +1515,7 @@ class ServerCollectionClient {
|
|
|
1230
1515
|
const qs = query.toString();
|
|
1231
1516
|
return this.request(qs ? `?${qs}` : "");
|
|
1232
1517
|
}
|
|
1518
|
+
// route: PATCH /storage/{collection}/{id}
|
|
1233
1519
|
async update(id, data) {
|
|
1234
1520
|
return this.request(`/${encodeURIComponent(id)}`, {
|
|
1235
1521
|
method: "PATCH",
|
|
@@ -1244,7 +1530,7 @@ class ServerCollectionClient {
|
|
|
1244
1530
|
if (init.body) {
|
|
1245
1531
|
headers["content-type"] = "application/json";
|
|
1246
1532
|
}
|
|
1247
|
-
const response = await fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.apiUrl), { ...init, headers });
|
|
1533
|
+
const response = await this.fetch(new URL(`/storage/${encodeURIComponent(this.name)}${suffix}`, this.apiUrl), { ...init, headers });
|
|
1248
1534
|
if (response.status === 204)
|
|
1249
1535
|
return undefined;
|
|
1250
1536
|
if (!response.ok) {
|
|
@@ -1304,7 +1590,7 @@ async function readErrorBody(response) {
|
|
|
1304
1590
|
// One request path for every runtime client (auth, subscriptions,
|
|
1305
1591
|
// payments, account) — same headers, same error handling, same signposts.
|
|
1306
1592
|
async function runtimeRequest(config, path, init = {}) {
|
|
1307
|
-
const response = await fetch(new URL(path, config.apiUrl), {
|
|
1593
|
+
const response = await config.fetch(new URL(path, config.apiUrl), {
|
|
1308
1594
|
...init,
|
|
1309
1595
|
headers: await runtimeHeaders(config, init.headers)
|
|
1310
1596
|
});
|
|
@@ -1315,7 +1601,7 @@ async function runtimeHeaders(config, headers) {
|
|
|
1315
1601
|
return {
|
|
1316
1602
|
...headers,
|
|
1317
1603
|
"x-app-key": config.appKey,
|
|
1318
|
-
"x-client-info":
|
|
1604
|
+
"x-client-info": config.clientInfo,
|
|
1319
1605
|
...(token ? { authorization: `Bearer ${token}` } : {})
|
|
1320
1606
|
};
|
|
1321
1607
|
}
|