@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/dist/index.d.ts CHANGED
@@ -2,6 +2,37 @@ export type GemmeinOptions = {
2
2
  appKey: string;
3
3
  apiUrl?: string;
4
4
  tokenStore?: TokenStore;
5
+ /**
6
+ * W10 §1 A — the fetch EVERY request this client makes goes through.
7
+ * Defaults to `globalThis.fetch`, read at CALL time so a polyfill
8
+ * installed after construction still counts. Expo passes `expo/fetch`,
9
+ * whose streaming answers React Native's own fetch cannot give.
10
+ */
11
+ fetch?: typeof fetch;
12
+ /**
13
+ * W10 §1 A — where "is the app in front of the user?" comes from, for
14
+ * `watch()`'s sleep-while-hidden. Absent, it is the browser's
15
+ * `document.visibilityState`, exactly as before. Expo passes an
16
+ * AppState-driven hook.
17
+ */
18
+ visibility?: VisibilityHook;
19
+ /**
20
+ * W10 §1 A — the platform appended to `x-client-info`
21
+ * (`gemmein-sdk/<version> expo-ios`). A report of which build called,
22
+ * never a proof. Cleaned and capped here — see `clientInfoFor`.
23
+ */
24
+ platform?: string;
25
+ };
26
+ /**
27
+ * W10 §1 A — a platform's answer to "is the app in front of the user?".
28
+ * The browser answers it with `document.visibilityState`; React Native
29
+ * answers it with `AppState`. `onChange` registers a listener and returns
30
+ * the function that removes it — one shape, so `watch()` never has to know
31
+ * which platform it is running on.
32
+ */
33
+ export type VisibilityHook = {
34
+ isHidden(): boolean;
35
+ onChange(cb: () => void): () => void;
5
36
  };
6
37
  export type TokenStore = {
7
38
  get(): string | undefined | Promise<string | undefined>;
@@ -101,10 +132,18 @@ export type CurrentUser = {
101
132
  authenticated: true;
102
133
  userId: string;
103
134
  email: string;
135
+ /**
136
+ * W10 — the store account token, when the engine carries one. It is
137
+ * passed through exactly as `GET /auth/current-user` sent it, never
138
+ * synthesised here; an engine that does not send it leaves this
139
+ * absent — read that as "not carried", never as "no account".
140
+ */
141
+ storeAccountToken?: string | null;
104
142
  } | {
105
143
  authenticated: false;
106
144
  userId?: undefined;
107
145
  email?: undefined;
146
+ storeAccountToken?: undefined;
108
147
  };
109
148
  export type AuthSession = {
110
149
  token: string;
@@ -120,13 +159,21 @@ export type AuthSession = {
120
159
  * second module). `scripts/sync-version.mjs` rewrites the literal from
121
160
  * package.json before every build (`prebuild`), and a test pins the two
122
161
  * equal, so a bump can never ship with a stale header. */
123
- export declare const SDK_VERSION = "0.8.0";
162
+ export declare const SDK_VERSION = "0.10.0";
124
163
  /** W9.1 / CLIENT-INFO-1: every request the SDK makes to Gemmein carries
125
164
  * `x-client-info: gemmein-sdk/<version>`. The server records it on the
126
165
  * secret-key usage ledger ("last seen from gemmein-sdk/0.5.0"), so a
127
166
  * misbehaving integration can be attributed to an SDK version from day
128
167
  * one. It is a report, not a proof — any caller can set it. */
129
- export declare const CLIENT_INFO = "gemmein-sdk/0.8.0";
168
+ export declare const CLIENT_INFO = "gemmein-sdk/0.10.0";
169
+ /** W10 §1 A: a mobile entry adds its platform — `gemmein-sdk/<version>
170
+ * expo-ios`. The ledger that records this header caps it at 64 characters
171
+ * and strips control characters (`MAX_CLIENT_LENGTH` / `capClient`,
172
+ * packages/db/src/keyUsageStore.ts), so the value is cleaned and capped
173
+ * HERE: a tag that arrives truncated attributes nothing. Anything outside
174
+ * `[A-Za-z0-9._/-]` collapses to a hyphen, so the label can never carry a
175
+ * newline — or a second space — into the ledger. */
176
+ export declare function clientInfoFor(platform?: string): string;
130
177
  export declare class GemmeinError extends Error {
131
178
  readonly status: number;
132
179
  readonly code: string;
@@ -141,12 +188,19 @@ export declare class GemmeinError extends Error {
141
188
  * plans by name through checkout, the server never hands out the list.
142
189
  */
143
190
  readonly requires?: string;
191
+ /**
192
+ * Present on `network_unreachable` — the fetch implementation's own throw,
193
+ * kept so a bug report can say WHICH transport failure it was. Read it for
194
+ * a log; never branch on it (its shape is the runtime's, not Gemmein's).
195
+ */
196
+ readonly cause?: unknown;
144
197
  constructor(input: {
145
198
  status: number;
146
199
  code: string;
147
200
  message: string;
148
201
  resetAt?: string;
149
202
  requires?: string;
203
+ cause?: unknown;
150
204
  });
151
205
  }
152
206
  export declare class MemoryTokenStore implements TokenStore {
@@ -159,6 +213,16 @@ export declare class BrowserTokenStore implements TokenStore {
159
213
  private readonly key;
160
214
  constructor(appKey: string);
161
215
  get(): string | undefined;
216
+ /**
217
+ * A blocked `localStorage` — Safari's private mode past its quota, a
218
+ * browser set to block site data, a sandboxed iframe whose access throws
219
+ * — throws `secure_store_unavailable` (status 0), the same code the
220
+ * mobile stores answer, with the browser's own error on `cause`.
221
+ * `auth.verifyEmailCode()` carries it to the caller: the session is real
222
+ * (the server minted it), so an app that would rather run than stop
223
+ * catches this one code and rebuilds its client with a
224
+ * `MemoryTokenStore`, which never throws.
225
+ */
162
226
  set(token: string): void;
163
227
  clear(): void;
164
228
  }
@@ -208,6 +272,12 @@ type ClientConfig = {
208
272
  apiUrl: string;
209
273
  appKey: string;
210
274
  tokenStore: TokenStore;
275
+ /** W10 §1 A — always present: the injected fetch, or the lazy default. */
276
+ fetch: typeof fetch;
277
+ /** W10 §1 A — absent in a browser; `watch()` falls back to `document`. */
278
+ visibility?: VisibilityHook | undefined;
279
+ /** W10 §1 A — `CLIENT_INFO`, plus the platform when one was named. */
280
+ clientInfo: string;
211
281
  };
212
282
  export declare class AuthClient {
213
283
  private readonly config;
@@ -217,6 +287,17 @@ export declare class AuthClient {
217
287
  email: string;
218
288
  code: string;
219
289
  }): Promise<AuthSession>;
290
+ /**
291
+ * End this device's session. IDEMPOTENT: signing out of a session that is
292
+ * already gone is the outcome asked for, not a failure.
293
+ *
294
+ * W10 row 9, found by driving the Expo app: the commonest way this
295
+ * round-trip fails is `401 auth_expired` — the owner already signed this
296
+ * person out everywhere from the console, which is the remedy for a
297
+ * stolen phone. The session IS ended; throwing there made an app that did
298
+ * everything right show an error for the thing it asked for. Every other
299
+ * failure still throws, and the token store is cleared either way.
300
+ */
220
301
  logout(): Promise<void>;
221
302
  currentUser(): Promise<CurrentUser>;
222
303
  private request;
@@ -275,7 +356,7 @@ export declare class PurchasesClient {
275
356
  }>>;
276
357
  }
277
358
  /**
278
- * A file reference — `file:01K…`. What `upload()` gives you and what your
359
+ * A file reference — `file:<uuid>`. What `upload()` gives you and what your
279
360
  * record should store.
280
361
  *
281
362
  * Branded so it cannot be mistaken for a URL: `<img src={record.poster}>` is a
@@ -285,6 +366,33 @@ export declare class PurchasesClient {
285
366
  export type FileRef = string & {
286
367
  readonly __gemmeinFileRef: unique symbol;
287
368
  };
369
+ /**
370
+ * W10 §1 A — what `upload()` takes. A browser hands it a `File` or a
371
+ * `Blob`.
372
+ *
373
+ * On a phone the thing a picker returns is a `{ uri, name, type, size }`
374
+ * object, and it reaches the wire two ways (W10 row 9, found by driving
375
+ * the Expo app):
376
+ *
377
+ * * through `@gemmein/sdk/expo` — `createExpoGemmein` turns the picker
378
+ * shape into an `expo-file-system` `File`, which IS a `Blob`, before it
379
+ * reaches here. This is the path an Expo app has: Expo's own fetch (the
380
+ * entry's default, and the only one that can stream an AI answer)
381
+ * refuses a bare picker part with
382
+ * `Unsupported FormDataPart implementation`.
383
+ * * through a client given React Native's own fetch
384
+ * (`gemmein(key, { fetch })`) — RN's `FormData` reads the bytes at
385
+ * `uri` itself, so the part is appended exactly as the picker gave it.
386
+ *
387
+ * Carry the picker's `size` with it — the server refuses a presign that
388
+ * declares nothing (it cannot accept an empty file).
389
+ */
390
+ export type UploadInput = Blob | File | {
391
+ uri: string;
392
+ name?: string;
393
+ type?: string;
394
+ size?: number;
395
+ };
288
396
  /**
289
397
  * Turn a stored file reference into a URL you can actually use.
290
398
  *
@@ -360,9 +468,14 @@ export declare class PaymentsClient {
360
468
  * WHAT is being bought when one product covers many things (e.g. a
361
469
  * license tier across a catalog):
362
470
  * `g.payments.buy("premium license", { item: "beat_37" })`.
363
- * A completed payment writes a receipt record addressed to the buyer in
364
- * the owner's receipts collection; gate downloads/fulfilment on that
365
- * receipt, never on the redirect coming back.
471
+ * Gemmein records every completed payment itself: `g.purchases.mine()` is
472
+ * the buyer's proof, and a product that delivers a file carries
473
+ * `delivery` on it. Gate fulfilment on the purchase (or the entitlement it
474
+ * granted), never on the redirect coming back. A receipts collection is
475
+ * optional, for proof records only.
476
+ *
477
+ * A product sold via a RELAY, or not sold yet, has no Payment Link to
478
+ * open: this answers 409 `product_not_sellable`.
366
479
  */
367
480
  buy(product: string, options?: {
368
481
  item?: string;
@@ -387,6 +500,23 @@ export declare class AccountClient {
387
500
  delete(): Promise<unknown>;
388
501
  }
389
502
  export type AiProvider = "openai" | "anthropic" | "google";
503
+ /** W9.6 §16: one of the person's own AI calls, as `g.ai.calls()` lists them. */
504
+ export type AiCallRecord = {
505
+ id: string;
506
+ tool: string;
507
+ kind: string;
508
+ provider: string;
509
+ model: string | null;
510
+ tokensIn: number | null;
511
+ tokensOut: number | null;
512
+ credits: number;
513
+ outcome: "ok" | "refused" | "provider_error" | "unreachable" | "client_closed" | "stream_ended";
514
+ refusalCode: string | null;
515
+ latencyMs: number | null;
516
+ prompt: string | null;
517
+ answer: string | null;
518
+ createdAt: string;
519
+ };
390
520
  export type AiChatOptions = {
391
521
  /** Which configured provider answers. Optional when exactly one key is
392
522
  * set; refused `provider_required` (400) when it is ambiguous. Refused
@@ -396,7 +526,10 @@ export type AiChatOptions = {
396
526
  /** W9.3b: a named AI tool (owner-configured in the console — credits,
397
527
  * gate and provider/model are the tool's, not this call's). Sent as
398
528
  * `?tool=`, never in the body. Omitted → the implicit default tool: one
399
- * credit, any allowed model, no gate. */
529
+ * credit, any allowed model, no gate. With or without `tool`, `chat` is
530
+ * a RAW call — off by default (`raw_calls_off`, 403) until the founder
531
+ * switches raw calls on for that provider's key on the AI tools page;
532
+ * the normal path to a named tool is `run(name, inputs)`. */
400
533
  tool?: string;
401
534
  /** Abort the call — the stream closes; a call that dies mid-stream is
402
535
  * not refunded. */
@@ -433,16 +566,24 @@ export declare class CreditsClient {
433
566
  }>;
434
567
  }
435
568
  /**
436
- * The AI route. `chat` takes the provider's own request body exactly what
437
- * you would POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages
438
- * or Google's generateContent and answers with the fetch `Response`
439
- * untouched, streaming intact (SSE stays SSE). Gemmein spends a credit,
440
- * adds the owner's key, forwards, and passes status and bytes back. Pass
441
- * `tool` (W9.3b) to run a named, owner-priced-and-gated operation instead
442
- * of the implicit default (one credit, any allowed model, no gate).
443
- * Response headers: `x-gemmein-credits-remaining` on every answer that
444
- * passed the spend; `x-gemmein-credit: refunded` when the provider failed
445
- * before its first byte.
569
+ * The AI route. The primary path is a NAMED TOOL defined on the server:
570
+ * `run(name, inputs)` sends a name and inputs, the server composes the
571
+ * provider request from the tool's own instructions and template (never
572
+ * the browser), gates it, spends the tool's credits and streams the
573
+ * answer back; `runText` is the same call collected to one string;
574
+ * `calls()` is the signed-in person's own history. `chat` is the RAW
575
+ * call: it takes the provider's own request body exactly what you would
576
+ * POST to OpenAI's /v1/chat/completions, Anthropic's /v1/messages or
577
+ * Google's generateContent and answers with the fetch `Response`
578
+ * untouched, streaming intact (SSE stays SSE). Raw calls are off by
579
+ * default for every provider key (`raw_calls_off`, 403) until the founder
580
+ * switches them on for that key on the AI tools page. Gemmein spends a
581
+ * credit, adds the owner's key, forwards, and passes status and bytes
582
+ * back. Pass `tool` (W9.3b) to a raw call to price and gate it as a named
583
+ * tool instead of the implicit default (one credit, any allowed model, no
584
+ * gate). Response headers: `x-gemmein-credits-remaining` on every answer
585
+ * that passed the spend; `x-gemmein-credit: refunded` when the provider
586
+ * failed before its first byte.
446
587
  */
447
588
  export declare class AiClient {
448
589
  private readonly config;
@@ -452,7 +593,10 @@ export declare class AiClient {
452
593
  * for await (const chunk of res.body) { … }
453
594
  *
454
595
  * Browser sessions only — a server key is refused (`scope_denied`, 403).
455
- * Refusals, all `GemmeinError`: `session_required` (401) ·
596
+ * Refusals, all `GemmeinError`: `raw_calls_off` (403 — raw calls are off
597
+ * for this provider until the founder switches them on for its key on
598
+ * the AI tools page; call a named tool with `run` instead) ·
599
+ * `session_required` (401) ·
456
600
  * `credits_exhausted` (402 — the message carries the balance; show your
457
601
  * own "buy more" door, which is a product checkout) · `ai_not_configured`
458
602
  * (409 — the owner has set no key) · `provider_required` (400) ·
@@ -472,6 +616,63 @@ export declare class AiClient {
472
616
  * on the implicit default.
473
617
  */
474
618
  chat(body: Record<string, unknown>, options?: AiChatOptions): Promise<Response>;
619
+ /**
620
+ * W9.6: run a named tool with INPUTS — the server composes the provider
621
+ * request from the tool's own instructions and template (never the
622
+ * browser), gates it, spends its credits and streams the answer back.
623
+ * The answer is the provider's own shape for the tool's provider (SSE
624
+ * when `stream`), so read it as you would `chat()`'s.
625
+ *
626
+ * const res = await g.ai.run("summarise", { text }, { stream: true });
627
+ *
628
+ * Browser sessions only — a server key is refused (`scope_denied`, 403).
629
+ * Refusals, all `GemmeinError`: `session_required` (401 — sign in
630
+ * first) · `ai_capped` (429 — 20 calls a minute per person;
631
+ * `err.resetAt`) · `unknown_tool` (404 — no tool by that name in this
632
+ * environment) · `tool_disabled` (403 — the owner switched it off) ·
633
+ * `entitlement_required` (403 — the message names the plan or product
634
+ * it needs) · `payload_too_large` (413 — inputs over 64 KB) ·
635
+ * `invalid_body` (400 — the body must be a JSON object
636
+ * `{ inputs, stream? }`) · `invalid_inputs` (400 — the message names the
637
+ * input and the rule) · `tool_incomplete` (409 — the tool composes
638
+ * nothing; a founder's fix) · `ai_not_configured` (409 — the tool's
639
+ * provider has no key set; the owner pastes one) · `credits_exhausted`
640
+ * (402 — the message names the tool, its price and the balance) ·
641
+ * `provider_unreachable` (502 — no answer before the first byte; the
642
+ * tool's credits are refunded, header `x-gemmein-credit: refunded`).
643
+ * The provider's own answer — 2xx or not — is returned as it came; read
644
+ * `res.ok` yourself. `x-gemmein-tool` names the tool.
645
+ */
646
+ run(tool: string, inputs?: Record<string, string | number | boolean>, options?: {
647
+ stream?: boolean;
648
+ signal?: AbortSignal;
649
+ }): Promise<Response>;
650
+ /**
651
+ * W9.6: `run()` without a stream, as one string — the text lifted out
652
+ * of the tool's provider's answer (the same readers `text()` uses).
653
+ * `run()`'s refusals, plus: a provider's own non-2xx throws
654
+ * `provider_error` with the provider's status and message; an answer
655
+ * with no text to lift out throws `invalid_response` (status 0).
656
+ *
657
+ * const summary = await g.ai.runText("summarise", { text });
658
+ */
659
+ runText(tool: string, inputs?: Record<string, string | number | boolean>, options?: {
660
+ signal?: AbortSignal;
661
+ }): Promise<string>;
662
+ /**
663
+ * W9.6 §16: the signed-in person's OWN AI calls, newest first — what they
664
+ * ran, when, what it cost, how it ended; the prompt and answer only where
665
+ * the tool keeps them. Session required.
666
+ *
667
+ * const { calls, nextCursor } = await g.ai.calls();
668
+ */
669
+ calls(options?: {
670
+ limit?: number;
671
+ before?: string | null;
672
+ }): Promise<{
673
+ calls: AiCallRecord[];
674
+ nextCursor: string | null;
675
+ }>;
475
676
  /**
476
677
  * The non-streaming convenience: one call, one string. Pass a body that
477
678
  * does NOT stream (`stream` unset or false); the provider's JSON answer is
@@ -600,7 +801,7 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
600
801
  }): Promise<GemmeinRecord<T>>;
601
802
  delete(id: string): Promise<void>;
602
803
  /**
603
- * Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
804
+ * Upload a file and get back a REFERENCE — `file:<uuid>` — not a URL.
604
805
  *
605
806
  * Store the reference. It never expires, it is safe to log and export, and
606
807
  * it grants nothing on its own. To show or download the file, call
@@ -627,7 +828,7 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
627
828
  * There is deliberately no `url` here. A URL that outlives a refund is the
628
829
  * bug this replaced.
629
830
  */
630
- upload(file: Blob | File, options?: {
831
+ upload(file: UploadInput, options?: {
631
832
  name?: string;
632
833
  contentType?: string;
633
834
  for?: string;
@@ -642,6 +843,11 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
642
843
  export type GemmeinServerOptions = {
643
844
  secretKey: string;
644
845
  apiUrl?: string;
846
+ /** The same seam the client has: a `fetch` to use instead of the global
847
+ * one. Every request this class makes goes through `resolveFetch`, so a
848
+ * transport failure is a `GemmeinError` `network_unreachable` here too
849
+ * (W10 review row 14a — the parity law: one seam, both rails). */
850
+ fetch?: typeof fetch;
645
851
  };
646
852
  /**
647
853
  * Where a grant came from — the KIND only. The gate never returns the
@@ -680,6 +886,13 @@ export type Holdings = {
680
886
  balance: number;
681
887
  } | null;
682
888
  };
889
+ /**
890
+ * The person behind a token or an id. The role you read here is the EFFECTIVE
891
+ * one — account membership (owner/admin) wins over the stored role, so
892
+ * a founder signed into their own app reads `owner` here and not the `member`
893
+ * every person is born as. `verifySession` and `holdings` answer with
894
+ * the same one — one person has one role, whichever door asked for it.
895
+ */
683
896
  export type GatePerson = {
684
897
  id: string;
685
898
  email: string;
@@ -698,6 +911,12 @@ export type InvitedPerson = GatePerson & {
698
911
  export declare class GemmeinServer {
699
912
  private readonly apiUrl;
700
913
  private readonly secretKey;
914
+ /** W10 review row 14a: THE ONE TRANSPORT SEAM, on this rail too. Every
915
+ * call below goes through it, so a refused connection is a typed
916
+ * `network_unreachable` and never a raw `TypeError` — the same law the
917
+ * client rail has held since row 9, and what makes "everything the
918
+ * package throws is a GemmeinError" true of the whole package. */
919
+ private readonly fetch;
701
920
  constructor(options: GemmeinServerOptions);
702
921
  collection(name: string): ServerCollectionClient;
703
922
  /**
@@ -929,7 +1148,10 @@ declare class ServerCollectionClient {
929
1148
  private readonly apiUrl;
930
1149
  private readonly secretKey;
931
1150
  private readonly name;
932
- constructor(apiUrl: string, secretKey: string, name: string);
1151
+ /** Handed down from `GemmeinServer` — never resolved again here, so one
1152
+ * client has one transport and a test's injected fetch reaches it. */
1153
+ private readonly fetch;
1154
+ constructor(apiUrl: string, secretKey: string, name: string, fetchImpl: typeof fetch);
933
1155
  get(id: string): Promise<unknown>;
934
1156
  list(options?: {
935
1157
  limit?: number;