@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/expo.cjs ADDED
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+ // W10 §1 A — `@gemmein/sdk/expo`: the SAME package, a second entry point.
3
+ //
4
+ // The core entry (`@gemmein/sdk`) stays platform-neutral and keeps ZERO
5
+ // dependency on Expo or React Native: every platform seam it needs is an
6
+ // option it already takes — `tokenStore`, `visibility`, `fetch`,
7
+ // `platform`. This file is nothing but those four options filled in with
8
+ // a phone's answers, plus the whole core surface re-exported so an Expo
9
+ // app never has to import from two places.
10
+ //
11
+ // Three rules this file keeps:
12
+ //
13
+ // 1. NOTHING is imported from Expo or React Native at module load. The
14
+ // modules are resolved lazily, inside a try, the first time a method
15
+ // that needs one runs — so importing this entry in Node (the tests
16
+ // below, an SSR pass, a script) resolves and runs without any of them
17
+ // installed. The specifiers are static strings so Metro can still see
18
+ // and bundle them in a real app.
19
+ // 2. Every module is INJECTABLE. Tests pass fakes; nothing here reaches
20
+ // for a global to decide what platform it is on.
21
+ // 3. A failure to READ the secure store degrades to "signed out" — the
22
+ // same law `BrowserTokenStore` keeps for a browser with storage
23
+ // blocked. A crash on a locked keychain would lock the app out. A
24
+ // failure to WRITE throws `secure_store_unavailable` (W10 row 9c): a
25
+ // store that cannot keep the session has broken the promise it exists
26
+ // for, and silence there signs the person out on the next launch with
27
+ // nothing anywhere saying why.
28
+ //
29
+ // One thing beyond those four seams: `upload()`'s picker shape. Expo's
30
+ // fetch refuses a bare `{ uri, name, type, size }` part, so the collections
31
+ // this entry hands out convert it to an `expo-file-system` `File` first —
32
+ // see `expoUploadPart`.
33
+ //
34
+ // Install in the app, not here (they are optional peer dependencies):
35
+ // npx expo install expo-secure-store expo-file-system
36
+ //
37
+ // import { createExpoGemmein } from "@gemmein/sdk/expo";
38
+ // import { Platform } from "react-native";
39
+ // export const g = createExpoGemmein({ appKey: "pk_live_..." }, { Platform });
40
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
41
+ if (k2 === undefined) k2 = k;
42
+ var desc = Object.getOwnPropertyDescriptor(m, k);
43
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
44
+ desc = { enumerable: true, get: function() { return m[k]; } };
45
+ }
46
+ Object.defineProperty(o, k2, desc);
47
+ }) : (function(o, m, k, k2) {
48
+ if (k2 === undefined) k2 = k;
49
+ o[k2] = m[k];
50
+ }));
51
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
52
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
53
+ };
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.SecureStoreTokenStore = exports.SECURE_STORE_VALUE_LIMIT = exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AiClient = exports.CreditsClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.FilesClient = exports.PurchasesClient = exports.AuthClient = exports.GemmeinError = exports.Gemmein = void 0;
56
+ exports.appStateVisibility = appStateVisibility;
57
+ exports.expoFetch = expoFetch;
58
+ exports.expoPlatformTag = expoPlatformTag;
59
+ exports.expoUploadPart = expoUploadPart;
60
+ exports.createExpoGemmein = createExpoGemmein;
61
+ const index_js_1 = require("./index.cjs");
62
+ // The full core surface, re-exported. `export *` carries the types and the
63
+ // helpers; the explicit list below names every client class the SDK
64
+ // registry knows about (ops/docs/facts.ts SDK_CLASSES), which is what
65
+ // tests/security/sdkParity.test.ts reads to prove this entry is not a
66
+ // smaller SDK wearing the same name.
67
+ __exportStar(require("./index.cjs"), exports);
68
+ var index_js_2 = require("./index.cjs");
69
+ Object.defineProperty(exports, "Gemmein", { enumerable: true, get: function () { return index_js_2.Gemmein; } });
70
+ Object.defineProperty(exports, "GemmeinError", { enumerable: true, get: function () { return index_js_2.GemmeinError; } });
71
+ Object.defineProperty(exports, "AuthClient", { enumerable: true, get: function () { return index_js_2.AuthClient; } });
72
+ Object.defineProperty(exports, "PurchasesClient", { enumerable: true, get: function () { return index_js_2.PurchasesClient; } });
73
+ Object.defineProperty(exports, "FilesClient", { enumerable: true, get: function () { return index_js_2.FilesClient; } });
74
+ Object.defineProperty(exports, "SubscriptionsClient", { enumerable: true, get: function () { return index_js_2.SubscriptionsClient; } });
75
+ Object.defineProperty(exports, "PaymentsClient", { enumerable: true, get: function () { return index_js_2.PaymentsClient; } });
76
+ Object.defineProperty(exports, "AccountClient", { enumerable: true, get: function () { return index_js_2.AccountClient; } });
77
+ Object.defineProperty(exports, "CreditsClient", { enumerable: true, get: function () { return index_js_2.CreditsClient; } });
78
+ Object.defineProperty(exports, "AiClient", { enumerable: true, get: function () { return index_js_2.AiClient; } });
79
+ Object.defineProperty(exports, "StorageClient", { enumerable: true, get: function () { return index_js_2.StorageClient; } });
80
+ Object.defineProperty(exports, "CollectionClient", { enumerable: true, get: function () { return index_js_2.CollectionClient; } });
81
+ Object.defineProperty(exports, "GemmeinServer", { enumerable: true, get: function () { return index_js_2.GemmeinServer; } });
82
+ /**
83
+ * The lazy resolver. Static specifiers so Metro bundles them; a `catch`
84
+ * so Node — where none of them exist — gets `undefined` rather than an
85
+ * unhandled rejection. Resolved once per specifier and remembered.
86
+ */
87
+ const resolved = new Map();
88
+ function loadModule(name) {
89
+ const cached = resolved.get(name);
90
+ if (cached)
91
+ return cached;
92
+ const attempt = (async () => {
93
+ try {
94
+ switch (name) {
95
+ // @ts-ignore optional peer dependency — present only in an Expo app
96
+ case "expo-secure-store": return (await Promise.resolve().then(() => require("expo-secure-store")));
97
+ // @ts-ignore optional peer dependency — present only in a React Native app
98
+ case "react-native": return (await Promise.resolve().then(() => require("react-native")));
99
+ // @ts-ignore optional peer dependency — present only in an Expo app
100
+ case "expo/fetch": return (await Promise.resolve().then(() => require("expo/fetch")));
101
+ // @ts-ignore optional peer dependency — present only in an Expo app
102
+ case "expo-file-system": return (await Promise.resolve().then(() => require("expo-file-system")));
103
+ default: return undefined;
104
+ }
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ })();
110
+ resolved.set(name, attempt);
111
+ return attempt;
112
+ }
113
+ // ═══════════════════════════════════════════════════════════════════════
114
+ // SecureStoreTokenStore — the session in the Keychain / Keystore
115
+ // ═══════════════════════════════════════════════════════════════════════
116
+ /**
117
+ * iOS refuses a Keychain value much over 2 KB through expo-secure-store,
118
+ * and the failure is a native throw the app cannot read. A Gemmein session
119
+ * token is a short opaque string — two orders of magnitude under this — so
120
+ * anything at this size is a caller storing the wrong thing. It is refused
121
+ * HERE, with a sentence, rather than on a device the author cannot see.
122
+ */
123
+ exports.SECURE_STORE_VALUE_LIMIT = 2048;
124
+ /**
125
+ * The one sentence a refused WRITE reads as (W10 row 9c). The device's own
126
+ * throw travels on `cause`, so an app that logs it keeps the native message
127
+ * — `errSecMissingEntitlement`, a locked keychain, a Keystore that is not
128
+ * there — instead of a Gemmein sentence standing in for it.
129
+ */
130
+ function secureStoreUnavailable(cause) {
131
+ const detail = cause instanceof Error ? cause.message : String(cause);
132
+ return new index_js_1.GemmeinError({
133
+ status: 0,
134
+ code: "secure_store_unavailable",
135
+ message: `The session could not be stored: expo-secure-store is not available or refused the write (${detail}) ` +
136
+ "— install expo-secure-store, or pass a tokenStore",
137
+ cause,
138
+ });
139
+ }
140
+ /**
141
+ * The session, kept in the iOS Keychain / Android Keystore through
142
+ * `expo-secure-store`. Keyed per app key exactly as `BrowserTokenStore`
143
+ * keys localStorage, so two Gemmein apps in one binary never share a
144
+ * token.
145
+ *
146
+ * `keychainAccessible: WHEN_UNLOCKED_THIS_DEVICE_ONLY` — the token is
147
+ * readable only while the device is unlocked AND never travels in an
148
+ * iCloud/iTunes backup or to a restored device. A session that survives a
149
+ * device restore is a session the person cannot revoke by losing the
150
+ * phone.
151
+ *
152
+ * Reads and clears are guarded: a locked keychain, a wiped entry, or a
153
+ * runtime with no secure store at all degrades to "signed out", never to
154
+ * a crash. A WRITE the device refuses throws `secure_store_unavailable` —
155
+ * see the note on `set`.
156
+ */
157
+ class SecureStoreTokenStore {
158
+ constructor(appKey, secureStore) {
159
+ // The same derivation as BrowserTokenStore (packages/sdk/src/index.ts):
160
+ // one law, two platforms. Every character a pk_ key can carry is legal
161
+ // in a SecureStore key.
162
+ this.key = `gemmein_session_${appKey.slice(0, 20)}`;
163
+ this.injected = secureStore;
164
+ }
165
+ async store() {
166
+ if (this.injected)
167
+ return this.injected;
168
+ const mod = await loadModule("expo-secure-store");
169
+ if (!mod)
170
+ return undefined;
171
+ const candidate = (mod.default ?? mod);
172
+ return typeof candidate?.getItemAsync === "function" ? candidate : undefined;
173
+ }
174
+ /** The accessibility class, read off the module so a version that renames
175
+ * the constant does not silently fall back to the iCloud-backed default. */
176
+ options(mod) {
177
+ const accessible = mod.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
178
+ return accessible === undefined ? {} : { keychainAccessible: accessible };
179
+ }
180
+ async get() {
181
+ try {
182
+ const mod = await this.store();
183
+ if (!mod)
184
+ return undefined;
185
+ return (await mod.getItemAsync(this.key, this.options(mod))) ?? undefined;
186
+ }
187
+ catch {
188
+ return undefined; // locked, missing, or unavailable — signed out
189
+ }
190
+ }
191
+ async set(token) {
192
+ // Measured in BYTES: the limit is the platform's, not JavaScript's.
193
+ const bytes = new TextEncoder().encode(token).length;
194
+ if (bytes > exports.SECURE_STORE_VALUE_LIMIT) {
195
+ throw new index_js_1.GemmeinError({
196
+ status: 0,
197
+ code: "token_too_large",
198
+ message: `This value is ${bytes} bytes; the device secure store holds ${exports.SECURE_STORE_VALUE_LIMIT}. ` +
199
+ "A Gemmein session token is far smaller — store the token, not the session payload.",
200
+ });
201
+ }
202
+ // W10 row 9c. This used to return silently on both arms below, so an app
203
+ // signed in, stored nothing, and reported itself signed out on the next
204
+ // launch with nothing anywhere saying why — the same silence the Swift
205
+ // `KeychainTokenStore` kept, found by driving the reference app on an
206
+ // unsigned simulator build. A store that cannot PERSIST the session has
207
+ // broken the one promise it exists for, so it says so. Reads and clears
208
+ // stay lenient: unreadable means signed out, which every app handles.
209
+ //
210
+ // The session is real either way — it was minted server-side and
211
+ // `verifyEmailCode` never gets to return it — so an app that would rather
212
+ // run than stop can catch this one code and rebuild its client with a
213
+ // `MemoryTokenStore`: sign-in works, and ends with the process.
214
+ let mod;
215
+ try {
216
+ mod = await this.store();
217
+ }
218
+ catch (cause) {
219
+ throw secureStoreUnavailable(cause);
220
+ }
221
+ if (!mod)
222
+ throw secureStoreUnavailable(new Error("expo-secure-store did not resolve"));
223
+ try {
224
+ await mod.setItemAsync(this.key, token, this.options(mod));
225
+ }
226
+ catch (cause) {
227
+ throw secureStoreUnavailable(cause);
228
+ }
229
+ }
230
+ async clear() {
231
+ try {
232
+ const mod = await this.store();
233
+ if (!mod)
234
+ return;
235
+ await mod.deleteItemAsync(this.key, this.options(mod));
236
+ }
237
+ catch {
238
+ /* nothing to clear */
239
+ }
240
+ }
241
+ }
242
+ exports.SecureStoreTokenStore = SecureStoreTokenStore;
243
+ // ═══════════════════════════════════════════════════════════════════════
244
+ // appStateVisibility — "is the app in front of the person?"
245
+ // ═══════════════════════════════════════════════════════════════════════
246
+ /**
247
+ * The `VisibilityHook` `watch()` sleeps on, answered by React Native's
248
+ * `AppState` instead of the browser's `document.visibilityState`.
249
+ *
250
+ * `AppState.currentState` is `"active" | "background" | "inactive"` (and
251
+ * `null` before the first read). Only `"active"` is in front of the
252
+ * person: `"inactive"` is the app-switcher card, a phone call, the
253
+ * Control Center pulled down — a screen the person is not reading, so
254
+ * polling there burns battery for nothing.
255
+ *
256
+ * Before the module has resolved the app is treated as VISIBLE, which is
257
+ * the browser default and the safe one: a watcher that wrongly believes
258
+ * it is hidden goes quiet, and quiet is the failure nobody notices.
259
+ */
260
+ function appStateVisibility(appState) {
261
+ let mod = appState;
262
+ const waiting = [];
263
+ if (!mod) {
264
+ void loadModule("react-native").then((rn) => {
265
+ const candidate = rn?.AppState;
266
+ if (!candidate || typeof candidate.addEventListener !== "function")
267
+ return;
268
+ mod = candidate;
269
+ for (const attach of waiting.splice(0))
270
+ attach(candidate);
271
+ });
272
+ }
273
+ return {
274
+ isHidden() {
275
+ return mod ? mod.currentState !== "active" : false;
276
+ },
277
+ onChange(cb) {
278
+ let remove;
279
+ let cancelled = false;
280
+ const attach = (m) => {
281
+ if (cancelled)
282
+ return;
283
+ // RN ≥ 0.65 returns a subscription with `.remove()`; older builds
284
+ // returned nothing and were removed by `removeEventListener`. Both
285
+ // shapes collapse to one remover here.
286
+ const subscription = m.addEventListener("change", () => cb());
287
+ remove = typeof subscription === "function"
288
+ ? subscription
289
+ : subscription && typeof subscription.remove === "function"
290
+ ? () => subscription.remove()
291
+ : undefined;
292
+ };
293
+ if (mod)
294
+ attach(mod);
295
+ else
296
+ waiting.push(attach);
297
+ return () => {
298
+ cancelled = true;
299
+ const off = remove;
300
+ remove = undefined;
301
+ off?.();
302
+ };
303
+ },
304
+ };
305
+ }
306
+ // ═══════════════════════════════════════════════════════════════════════
307
+ // The fetch and the platform tag
308
+ // ═══════════════════════════════════════════════════════════════════════
309
+ /**
310
+ * `expo/fetch` — the WinterCG fetch Expo ships. React Native's own fetch
311
+ * is XHR-backed and cannot give a streaming body, which is what
312
+ * `g.ai.chat({ stream: true })` and `g.ai.run(..., { stream: true })`
313
+ * return. Resolved on the first request; `globalThis.fetch` if Expo's is
314
+ * not there, so a bare React Native app still works for everything that
315
+ * is not a stream.
316
+ */
317
+ function expoFetch(injected) {
318
+ let impl = injected;
319
+ return (async (input, init) => {
320
+ if (!impl) {
321
+ const mod = await loadModule("expo/fetch");
322
+ const candidate = mod?.fetch;
323
+ impl = typeof candidate === "function" ? candidate : (i, n) => globalThis.fetch(i, n);
324
+ }
325
+ return impl(input, init);
326
+ });
327
+ }
328
+ /**
329
+ * The `x-client-info` platform tag: `expo-ios`, `expo-android`, `expo-web`.
330
+ *
331
+ * It is baked into the header when the client is CONSTRUCTED, so it cannot
332
+ * wait on a dynamic import. Pass `Platform` from `react-native` (a static
333
+ * import that costs an Expo app nothing) and the tag names the OS; without
334
+ * it the tag is the honest `expo`. Either way it is a report, never a
335
+ * proof — the ledger stores it as one.
336
+ */
337
+ function expoPlatformTag(platform) {
338
+ const os = typeof platform?.OS === "string" ? platform.OS : "";
339
+ return os ? `expo-${os}` : "expo";
340
+ }
341
+ // ═══════════════════════════════════════════════════════════════════════
342
+ // The upload part — the picker's `{ uri }`, turned into bytes
343
+ // ═══════════════════════════════════════════════════════════════════════
344
+ /**
345
+ * W10 row 9, found by driving the Expo app: `upload()` takes the picker's
346
+ * `{ uri, name, type, size }`, but `expo/fetch` — the fetch this entry
347
+ * hands the client, and the only one that can stream an AI answer — refuses
348
+ * that part when its `FormData` serialises it:
349
+ * `Unsupported FormDataPart implementation`. React Native's own fetch reads
350
+ * the bytes at `uri`; Expo's will not.
351
+ *
352
+ * So the shape is converted here, before it ever reaches core `upload()`:
353
+ * `new File(uri)` from `expo-file-system` is a real `Blob` that carries
354
+ * `uri`, `name`, `type` and `size` — the three fields the presign reads,
355
+ * plus bytes any FormData can serialise. The picker shape therefore works
356
+ * through `@gemmein/sdk/expo` exactly as the docs say it does.
357
+ *
358
+ * Anything that already carries its own bytes — a `Blob`, a browser `File`,
359
+ * an `expo-file-system` `File` — is passed through untouched.
360
+ */
361
+ async function expoUploadPart(file, modules = {}) {
362
+ const part = file;
363
+ // Already byte-bearing: a Blob, a File, an expo-file-system File. Duck-typed
364
+ // rather than `instanceof Blob`, because the class a phone's runtime has is
365
+ // not always the one this module can see.
366
+ const carriesBytes = typeof part.arrayBuffer === "function" || typeof part.stream === "function" ||
367
+ typeof part.slice === "function" || typeof part.bytes === "function";
368
+ if (carriesBytes || typeof part.uri !== "string")
369
+ return file;
370
+ const injected = modules.FileSystem;
371
+ const mod = injected ?? (await loadModule("expo-file-system"));
372
+ const FileClass = mod?.File;
373
+ if (typeof FileClass !== "function") {
374
+ throw new index_js_1.GemmeinError({
375
+ status: 0,
376
+ code: "upload_input_unsupported",
377
+ message: "This upload input cannot be read on Expo — pass a Blob or an expo-file-system File; " +
378
+ "the picker's { uri } needs expo-file-system installed (npx expo install expo-file-system)",
379
+ });
380
+ }
381
+ return new FileClass(part.uri);
382
+ }
383
+ /**
384
+ * Every collection this client hands out gets the conversion above in front
385
+ * of its `upload`. It is wrapped on the ONE factory both spellings go
386
+ * through — `g.collection(name)` delegates to `g.storage.collection(name)`
387
+ * — so there is one seam, not two, and every other method is the core
388
+ * client's own, untouched.
389
+ */
390
+ function wrapUploads(client, modules) {
391
+ const storage = client.storage;
392
+ const core = storage.collection.bind(client.storage);
393
+ storage.collection = (name, options) => {
394
+ const collection = core(name, options);
395
+ const upload = collection.upload.bind(collection);
396
+ collection.upload = async (file, uploadOptions) => upload(await expoUploadPart(file, modules), uploadOptions);
397
+ return collection;
398
+ };
399
+ return client;
400
+ }
401
+ // ═══════════════════════════════════════════════════════════════════════
402
+ // createExpoGemmein — the four options, filled in
403
+ // ═══════════════════════════════════════════════════════════════════════
404
+ /**
405
+ * The Expo client. Identical to `gemmein()` in every method, because it IS
406
+ * `Gemmein` — the only difference is where the session is kept, how "is
407
+ * the app in front?" is answered, which fetch carries the bytes, and what
408
+ * the client tag says.
409
+ *
410
+ * const g = createExpoGemmein({ appKey: "pk_live_..." }, { Platform });
411
+ *
412
+ * Every default is overridable: pass your own `tokenStore`, `visibility`,
413
+ * `fetch` or `platform` in `options` and it wins.
414
+ */
415
+ function createExpoGemmein(options, modules = {}) {
416
+ return wrapUploads(new index_js_1.Gemmein({
417
+ tokenStore: new SecureStoreTokenStore(options.appKey, modules.SecureStore),
418
+ visibility: appStateVisibility(modules.AppState),
419
+ fetch: expoFetch(modules.fetch),
420
+ platform: expoPlatformTag(modules.Platform),
421
+ ...options,
422
+ }), modules);
423
+ }
@@ -0,0 +1,152 @@
1
+ import { Gemmein } from "./index.js";
2
+ import type { GemmeinOptions, TokenStore, UploadInput, VisibilityHook } from "./index.js";
3
+ export * from "./index.js";
4
+ export { Gemmein, GemmeinError, AuthClient, PurchasesClient, FilesClient, SubscriptionsClient, PaymentsClient, AccountClient, CreditsClient, AiClient, StorageClient, CollectionClient, GemmeinServer, } from "./index.js";
5
+ /** The slice of `expo-secure-store` this entry calls. */
6
+ export type SecureStoreModule = {
7
+ getItemAsync(key: string, options?: Record<string, unknown>): Promise<string | null>;
8
+ setItemAsync(key: string, value: string, options?: Record<string, unknown>): Promise<void>;
9
+ deleteItemAsync(key: string, options?: Record<string, unknown>): Promise<void>;
10
+ /** `WHEN_UNLOCKED_THIS_DEVICE_ONLY` — read off the module when present. */
11
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY?: unknown;
12
+ };
13
+ /** The slice of React Native's `AppState` this entry subscribes to. */
14
+ export type AppStateModule = {
15
+ currentState: string | null;
16
+ addEventListener(type: string, handler: (state: string) => void): {
17
+ remove(): void;
18
+ } | (() => void);
19
+ };
20
+ /** The slice of React Native's `Platform` this entry reads. */
21
+ export type PlatformModule = {
22
+ OS: string;
23
+ };
24
+ /**
25
+ * The slice of `expo-file-system` this entry calls: its `File` class.
26
+ *
27
+ * `new File(uri)` — the constructor takes the `file:///` URI a picker or a
28
+ * camera handed back (it accepts more segments, and a `Directory`/`File`
29
+ * first argument, which this entry never needs). The instance implements
30
+ * `Blob` and carries `uri`, `name`, `type` and `size`, which is exactly
31
+ * what `upload()` reads.
32
+ */
33
+ export type FileSystemModule = {
34
+ File: new (uri: string) => Blob & {
35
+ uri?: string;
36
+ name?: string;
37
+ type?: string | null;
38
+ size?: number | null;
39
+ };
40
+ };
41
+ /** Everything an Expo app can hand in instead of letting this file resolve
42
+ * it. Tests pass fakes here; an app may pass its own static imports, which
43
+ * is the fastest path (no dynamic resolution at all). */
44
+ export type ExpoModules = {
45
+ SecureStore?: SecureStoreModule;
46
+ AppState?: AppStateModule;
47
+ Platform?: PlatformModule;
48
+ /** `expo-file-system`, for `upload()`'s picker shape — see `expoUploadPart`. */
49
+ FileSystem?: FileSystemModule;
50
+ fetch?: typeof fetch;
51
+ };
52
+ /**
53
+ * iOS refuses a Keychain value much over 2 KB through expo-secure-store,
54
+ * and the failure is a native throw the app cannot read. A Gemmein session
55
+ * token is a short opaque string — two orders of magnitude under this — so
56
+ * anything at this size is a caller storing the wrong thing. It is refused
57
+ * HERE, with a sentence, rather than on a device the author cannot see.
58
+ */
59
+ export declare const SECURE_STORE_VALUE_LIMIT = 2048;
60
+ /**
61
+ * The session, kept in the iOS Keychain / Android Keystore through
62
+ * `expo-secure-store`. Keyed per app key exactly as `BrowserTokenStore`
63
+ * keys localStorage, so two Gemmein apps in one binary never share a
64
+ * token.
65
+ *
66
+ * `keychainAccessible: WHEN_UNLOCKED_THIS_DEVICE_ONLY` — the token is
67
+ * readable only while the device is unlocked AND never travels in an
68
+ * iCloud/iTunes backup or to a restored device. A session that survives a
69
+ * device restore is a session the person cannot revoke by losing the
70
+ * phone.
71
+ *
72
+ * Reads and clears are guarded: a locked keychain, a wiped entry, or a
73
+ * runtime with no secure store at all degrades to "signed out", never to
74
+ * a crash. A WRITE the device refuses throws `secure_store_unavailable` —
75
+ * see the note on `set`.
76
+ */
77
+ export declare class SecureStoreTokenStore implements TokenStore {
78
+ private readonly key;
79
+ private readonly injected?;
80
+ constructor(appKey: string, secureStore?: SecureStoreModule);
81
+ private store;
82
+ /** The accessibility class, read off the module so a version that renames
83
+ * the constant does not silently fall back to the iCloud-backed default. */
84
+ private options;
85
+ get(): Promise<string | undefined>;
86
+ set(token: string): Promise<void>;
87
+ clear(): Promise<void>;
88
+ }
89
+ /**
90
+ * The `VisibilityHook` `watch()` sleeps on, answered by React Native's
91
+ * `AppState` instead of the browser's `document.visibilityState`.
92
+ *
93
+ * `AppState.currentState` is `"active" | "background" | "inactive"` (and
94
+ * `null` before the first read). Only `"active"` is in front of the
95
+ * person: `"inactive"` is the app-switcher card, a phone call, the
96
+ * Control Center pulled down — a screen the person is not reading, so
97
+ * polling there burns battery for nothing.
98
+ *
99
+ * Before the module has resolved the app is treated as VISIBLE, which is
100
+ * the browser default and the safe one: a watcher that wrongly believes
101
+ * it is hidden goes quiet, and quiet is the failure nobody notices.
102
+ */
103
+ export declare function appStateVisibility(appState?: AppStateModule): VisibilityHook;
104
+ /**
105
+ * `expo/fetch` — the WinterCG fetch Expo ships. React Native's own fetch
106
+ * is XHR-backed and cannot give a streaming body, which is what
107
+ * `g.ai.chat({ stream: true })` and `g.ai.run(..., { stream: true })`
108
+ * return. Resolved on the first request; `globalThis.fetch` if Expo's is
109
+ * not there, so a bare React Native app still works for everything that
110
+ * is not a stream.
111
+ */
112
+ export declare function expoFetch(injected?: typeof fetch): typeof fetch;
113
+ /**
114
+ * The `x-client-info` platform tag: `expo-ios`, `expo-android`, `expo-web`.
115
+ *
116
+ * It is baked into the header when the client is CONSTRUCTED, so it cannot
117
+ * wait on a dynamic import. Pass `Platform` from `react-native` (a static
118
+ * import that costs an Expo app nothing) and the tag names the OS; without
119
+ * it the tag is the honest `expo`. Either way it is a report, never a
120
+ * proof — the ledger stores it as one.
121
+ */
122
+ export declare function expoPlatformTag(platform?: PlatformModule): string;
123
+ /**
124
+ * W10 row 9, found by driving the Expo app: `upload()` takes the picker's
125
+ * `{ uri, name, type, size }`, but `expo/fetch` — the fetch this entry
126
+ * hands the client, and the only one that can stream an AI answer — refuses
127
+ * that part when its `FormData` serialises it:
128
+ * `Unsupported FormDataPart implementation`. React Native's own fetch reads
129
+ * the bytes at `uri`; Expo's will not.
130
+ *
131
+ * So the shape is converted here, before it ever reaches core `upload()`:
132
+ * `new File(uri)` from `expo-file-system` is a real `Blob` that carries
133
+ * `uri`, `name`, `type` and `size` — the three fields the presign reads,
134
+ * plus bytes any FormData can serialise. The picker shape therefore works
135
+ * through `@gemmein/sdk/expo` exactly as the docs say it does.
136
+ *
137
+ * Anything that already carries its own bytes — a `Blob`, a browser `File`,
138
+ * an `expo-file-system` `File` — is passed through untouched.
139
+ */
140
+ export declare function expoUploadPart(file: UploadInput, modules?: ExpoModules): Promise<UploadInput>;
141
+ /**
142
+ * The Expo client. Identical to `gemmein()` in every method, because it IS
143
+ * `Gemmein` — the only difference is where the session is kept, how "is
144
+ * the app in front?" is answered, which fetch carries the bytes, and what
145
+ * the client tag says.
146
+ *
147
+ * const g = createExpoGemmein({ appKey: "pk_live_..." }, { Platform });
148
+ *
149
+ * Every default is overridable: pass your own `tokenStore`, `visibility`,
150
+ * `fetch` or `platform` in `options` and it wins.
151
+ */
152
+ export declare function createExpoGemmein(options: GemmeinOptions, modules?: ExpoModules): Gemmein;