@gemmein/sdk 0.0.1 → 0.2.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.
@@ -0,0 +1,374 @@
1
+ export type GemmeinOptions = {
2
+ appKey: string;
3
+ apiUrl?: string;
4
+ tokenStore?: TokenStore;
5
+ };
6
+ export type TokenStore = {
7
+ get(): string | undefined | Promise<string | undefined>;
8
+ set(token: string): void | Promise<void>;
9
+ clear(): void | Promise<void>;
10
+ };
11
+ /**
12
+ * A stored record. Your app's fields ALWAYS live under `data`
13
+ * (`record.data.title`, never `record.title`). Everything else is
14
+ * server-derived and read-only — never store your own userId/role/owner
15
+ * fields inside `data`; the server already knows who owns what.
16
+ */
17
+ export type GemmeinRecord<T extends Record<string, unknown> = Record<string, unknown>> = {
18
+ id: string;
19
+ /** Your fields, exactly as you created them. */
20
+ data: T;
21
+ createdAt: string;
22
+ updatedAt: string;
23
+ /**
24
+ * Who owns this record — set by the server from the signed-in session.
25
+ * `null` for app-owned records that no user session created: purchase
26
+ * receipts (written by the payment webhook), and records the owner adds
27
+ * from the dashboard without assigning a user. Guard before using it.
28
+ */
29
+ ownerUserId: string | null;
30
+ collectionId: string;
31
+ appId: string;
32
+ environmentId: string;
33
+ /**
34
+ * Monotonic edit counter (+1 per update). When different people can edit
35
+ * the same record, pass the version you read back as
36
+ * `update(id, data, { ifVersion: record.version })` — a stale save gets a
37
+ * 409 `conflict` instead of silently clobbering someone else's edit.
38
+ */
39
+ version: number;
40
+ /** The create-if-absent key this record was created with, when one was used. */
41
+ key?: string;
42
+ /**
43
+ * The recipient (addressed/direct collections) — set by the server from
44
+ * the create's `{ for: userId }`, never from data. On addressed
45
+ * collections users read only records addressed to them; on direct, the
46
+ * author and the recipient read.
47
+ */
48
+ audienceUserId?: string;
49
+ /**
50
+ * Draft state on the public rules (public_read, community) — server
51
+ * column, set via create/update OPTIONS ({ published: false }), never a
52
+ * data field. Non-authors only ever receive published records, so you'll
53
+ * only see false on your own drafts (or as the owner).
54
+ */
55
+ published: boolean;
56
+ /** Linked records you asked to expand (`list({ expand: ["authorId"] })`),
57
+ * per field — only what you could read directly; unreadable or deleted
58
+ * targets are null (render as "[deleted]"). */
59
+ expand?: Record<string, GemmeinRecord | null>;
60
+ /** Present (true) only when a keyed create was YOUR OWN retry — you got the record you already made. */
61
+ existing?: boolean;
62
+ };
63
+ /** One page of records. When `hasMore` is true, pass `cursor` to `list()` for the next page. */
64
+ export type ListResult<T extends Record<string, unknown> = Record<string, unknown>> = {
65
+ records: GemmeinRecord<T>[];
66
+ cursor?: string;
67
+ hasMore: boolean;
68
+ };
69
+ export type ListOptions = {
70
+ limit?: number;
71
+ sort?: "newest" | "oldest" | "updated";
72
+ /** Exact-match filter on your `data` fields, e.g. `{ done: false }`. */
73
+ where?: Record<string, unknown>;
74
+ /** Opaque page cursor from a previous `ListResult`. */
75
+ cursor?: string;
76
+ /** Free-text search across your `data` fields. */
77
+ search?: string;
78
+ /** SHAPE-1: link fields to embed (up to 3) — each expanded record is only
79
+ * what YOU could have read directly; unreadable/deleted targets are null. */
80
+ expand?: string[];
81
+ };
82
+ /**
83
+ * Answer to "who is signed in right now?". `authenticated: false` simply
84
+ * means "no one" — this call never throws for session state, so it's safe
85
+ * unguarded on page load.
86
+ */
87
+ export type CurrentUser = {
88
+ authenticated: true;
89
+ userId: string;
90
+ email: string;
91
+ } | {
92
+ authenticated: false;
93
+ userId?: undefined;
94
+ email?: undefined;
95
+ };
96
+ export type AuthSession = {
97
+ token: string;
98
+ expiresAt: string;
99
+ user: {
100
+ id: string;
101
+ email: string;
102
+ };
103
+ };
104
+ export declare class GemmeinError extends Error {
105
+ readonly status: number;
106
+ readonly code: string;
107
+ readonly resetAt?: string;
108
+ constructor(input: {
109
+ status: number;
110
+ code: string;
111
+ message: string;
112
+ resetAt?: string;
113
+ });
114
+ }
115
+ export declare class MemoryTokenStore implements TokenStore {
116
+ private token?;
117
+ get(): string | undefined;
118
+ set(token: string): void;
119
+ clear(): void;
120
+ }
121
+ export declare class BrowserTokenStore implements TokenStore {
122
+ private readonly key;
123
+ constructor(appKey: string);
124
+ get(): string | undefined;
125
+ set(token: string): void;
126
+ clear(): void;
127
+ }
128
+ /**
129
+ * The client — two layers:
130
+ *
131
+ * - `g.collection("notes")` — YOUR app's collections. Records, files,
132
+ * safety rules. This is where your app's own data model lives.
133
+ * - Business primitives Gemmein runs for you: `g.auth` (sign-in),
134
+ * `g.subscriptions` (who's on which plan), `g.payments` (one-off
135
+ * purchases), `g.account` (the user's own account). These are
136
+ * SELF-SERVICE surfaces for the signed-in user — reads and Stripe
137
+ * hand-offs, never admin powers. Managing other people's users,
138
+ * subscriptions, or records happens in the owner's dashboard
139
+ * (app.gemmein.com), on purpose.
140
+ */
141
+ export declare class Gemmein {
142
+ readonly auth: AuthClient;
143
+ readonly storage: StorageClient;
144
+ readonly subscriptions: SubscriptionsClient;
145
+ readonly payments: PaymentsClient;
146
+ readonly account: AccountClient;
147
+ constructor(options: GemmeinOptions);
148
+ /**
149
+ * Your app's data — `g.collection<{ title: string }>("notes")`. The
150
+ * canonical spelling; `g.storage.collection(name)` is the same client.
151
+ */
152
+ collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string): CollectionClient<T>;
153
+ }
154
+ export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
155
+ export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
156
+ type ClientConfig = {
157
+ apiUrl: string;
158
+ appKey: string;
159
+ tokenStore: TokenStore;
160
+ };
161
+ export declare class AuthClient {
162
+ private readonly config;
163
+ constructor(config: ClientConfig);
164
+ sendEmailCode(email: string): Promise<void>;
165
+ verifyEmailCode(input: {
166
+ email: string;
167
+ code: string;
168
+ }): Promise<AuthSession>;
169
+ logout(): Promise<void>;
170
+ currentUser(): Promise<CurrentUser>;
171
+ private request;
172
+ }
173
+ /**
174
+ * SUBS: the subscription primitive, self-service side. Gemmein keeps
175
+ * exactly one subscription per customer — created by the payment itself,
176
+ * updated by Stripe's signed webhooks, overridable by the owner in their
177
+ * dashboard. The client surface is deliberately read-plus-checkout only:
178
+ * there is no client write path to plan or status, by design.
179
+ */
180
+ export declare class SubscriptionsClient {
181
+ private readonly config;
182
+ constructor(config: ClientConfig);
183
+ /**
184
+ * The signed-in user's subscription — gate features with
185
+ * `(await g.subscriptions.mine())?.plan === "pro"`. Null when payments
186
+ * are off or this user has never paid; throws GemmeinError (401) when
187
+ * nobody is signed in — a data route, not the never-throw current-user
188
+ * contract.
189
+ */
190
+ mine(): Promise<{
191
+ plan: string;
192
+ status: "active" | "cancelled";
193
+ } | null>;
194
+ /**
195
+ * Start a Stripe checkout for a plan — Gemmein mints the URL with the
196
+ * signed-in buyer and the plan already wired in (never build checkout
197
+ * URLs yourself; raw emails get silently dropped by Stripe's URL rules).
198
+ * In a browser this redirects immediately; it also resolves with the URL
199
+ * (for non-browser callers or custom handling). Requires a signed-in user
200
+ * and a plan whose Payment Link the app owner has pasted in their
201
+ * dashboard; omit `plan` to buy the app's paid plan.
202
+ */
203
+ checkout(plan?: string): Promise<{
204
+ url: string;
205
+ plan: string;
206
+ }>;
207
+ }
208
+ /** The one-off purchase primitive — things, not plans (plans are `g.subscriptions`). */
209
+ export declare class PaymentsClient {
210
+ private readonly config;
211
+ constructor(config: ClientConfig);
212
+ /**
213
+ * Buy a one-off product — `g.payments.buy("poster")`. Redirects in
214
+ * browsers, and resolves with the URL. The optional `item` note names
215
+ * WHAT is being bought when one product covers many things (e.g. a
216
+ * license tier across a catalog):
217
+ * `g.payments.buy("premium license", { item: "beat_37" })`.
218
+ * A completed payment writes a receipt record addressed to the buyer in
219
+ * the owner's receipts collection; gate downloads/fulfilment on that
220
+ * receipt, never on the redirect coming back.
221
+ */
222
+ buy(product: string, options?: {
223
+ item?: string;
224
+ }): Promise<{
225
+ url: string;
226
+ product: string;
227
+ item?: string;
228
+ }>;
229
+ }
230
+ /** The signed-in user's own account — self-service, one deliberate power. */
231
+ export declare class AccountClient {
232
+ private readonly config;
233
+ constructor(config: ClientConfig);
234
+ /**
235
+ * Self-service erasure — the "delete my account" screen. Every app it
236
+ * APPLIES to needs one (GDPR right to erasure; Apple 5.1.1(v) requires it
237
+ * for any app with account creation). Server-side this is the full
238
+ * cascade: sessions revoked, the user's records and files deleted, their
239
+ * subscription row removed. Irreversible — put a real confirm in front
240
+ * of it.
241
+ */
242
+ delete(): Promise<unknown>;
243
+ }
244
+ export declare class StorageClient {
245
+ private readonly config;
246
+ constructor(config: ClientConfig);
247
+ /** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
248
+ collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string): CollectionClient<T>;
249
+ }
250
+ /**
251
+ * Talks to one collection. Collections themselves are created by the app
252
+ * owner in their dashboard (app.gemmein.com → data) — a 404
253
+ * `unknown_collection` means it doesn't exist yet: ask the owner to create
254
+ * it there, don't retry.
255
+ */
256
+ export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
257
+ private readonly config;
258
+ private readonly name;
259
+ constructor(config: ClientConfig, name: string);
260
+ /**
261
+ * Create a record from your fields. The signed-in user becomes its owner.
262
+ *
263
+ * For anything two users can race for (a booking slot, a unique slug, a
264
+ * limited drop), pass a deterministic `key` derived from the thing that
265
+ * must be unique: `create(data, { key: "slot:2026-07-15T15:00" })`.
266
+ * The second writer gets a 409 GemmeinError `conflict` — that error IS
267
+ * the booking system working: catch it and tell the user it's taken.
268
+ * Your own retry with the same key returns the record you already made
269
+ * (`existing: true`) instead of a duplicate. Deleting a keyed record
270
+ * frees its key.
271
+ *
272
+ * On `addressed` and `direct` collections every create names its
273
+ * recipient: `create(data, { for: userId })` — the server stamps it,
274
+ * and only that user (plus the sender/owner) will ever read the record.
275
+ * Sending to a non-user is a 400 `invalid_audience`; a 403 `reply_only`
276
+ * means this collection only allows replies to people who wrote to you
277
+ * first — tell the user, don't retry.
278
+ *
279
+ * On the PUBLIC rules (public_read, community) pass `{ published: false }`
280
+ * to save a DRAFT the public can't see (the author still sees their own;
281
+ * the owner sees all). Publish later with
282
+ * `update(id, {}, { published: true })`. This is server-enforced — never
283
+ * fake drafts with a data field + client-side filtering: on a public
284
+ * collection the data still reaches everyone.
285
+ */
286
+ create(data: T, options?: {
287
+ key?: string;
288
+ for?: string;
289
+ published?: boolean;
290
+ }): Promise<GemmeinRecord<T>>;
291
+ /**
292
+ * List records this user is allowed to see under the collection's safety
293
+ * rule (the app owner sees everyone's). Returns `{ records, hasMore }` —
294
+ * an object, not a bare array.
295
+ */
296
+ list(options?: ListOptions): Promise<ListResult<T>>;
297
+ get(id: string, options?: {
298
+ expand?: string[];
299
+ }): Promise<GemmeinRecord<T>>;
300
+ /**
301
+ * Merge-updates `data` fields; returns the full updated record.
302
+ *
303
+ * Counters that users race for (stock, seats) must never be computed
304
+ * client-side — put an atomic op in value position and the server does
305
+ * the math on current state: `update(id, { stock: { decrement: 1,
306
+ * floor: 0 } })`. Breaching the floor/ceiling → 409 `conflict` ("out of
307
+ * stock" — the limit working, not a bug). An object is treated as an op
308
+ * ONLY when its keys are exactly increment|decrement (+ optional
309
+ * floor|ceiling), all numbers.
310
+ *
311
+ * When different people can edit the same record (CMS pages, shared
312
+ * docs), pass `{ ifVersion: record.version }` — a stale save gets a 409
313
+ * `conflict` instead of clobbering; re-read, reapply, retry.
314
+ */
315
+ update(id: string, data: Partial<T> | Record<string, unknown>, options?: {
316
+ ifVersion?: number;
317
+ published?: boolean;
318
+ }): Promise<GemmeinRecord<T>>;
319
+ delete(id: string): Promise<void>;
320
+ upload(file: Blob | File, options?: {
321
+ name?: string;
322
+ }): Promise<{
323
+ id: string;
324
+ url: string;
325
+ contentType: string;
326
+ sizeBytes: number;
327
+ }>;
328
+ private request;
329
+ }
330
+ export type GemmeinServerOptions = {
331
+ secretKey: string;
332
+ apiUrl?: string;
333
+ };
334
+ export declare class GemmeinServer {
335
+ private readonly apiUrl;
336
+ private readonly secretKey;
337
+ constructor(options: GemmeinServerOptions);
338
+ collection(name: string): ServerCollectionClient;
339
+ /**
340
+ * Mint a member session for a test email WITHOUT an OTP round-trip — so a CI
341
+ * self-test ("reaffirm") can sign in as N test users and prove your app's
342
+ * isolation boundaries hold (user B genuinely can't read user A's private
343
+ * records). DEV ENVIRONMENTS ONLY: throws `test_session_forbidden_live` on an
344
+ * `sk_live` key, and the server refuses it too. Never ship this in app code.
345
+ * Pass the returned `token` to `gemmein(pk, { tokenStore })` to act as that user.
346
+ */
347
+ testSession(email: string): Promise<{
348
+ token: string;
349
+ expiresAt: string;
350
+ user: {
351
+ id: string;
352
+ email: string;
353
+ role: string;
354
+ };
355
+ }>;
356
+ }
357
+ declare class ServerCollectionClient {
358
+ private readonly apiUrl;
359
+ private readonly secretKey;
360
+ private readonly name;
361
+ constructor(apiUrl: string, secretKey: string, name: string);
362
+ get(id: string): Promise<unknown>;
363
+ list(options?: {
364
+ limit?: number;
365
+ sort?: "newest" | "oldest" | "updated";
366
+ where?: Record<string, unknown>;
367
+ cursor?: string;
368
+ search?: string;
369
+ expand?: string[];
370
+ }): Promise<unknown>;
371
+ update(id: string, data: Record<string, unknown>): Promise<unknown>;
372
+ private request;
373
+ }
374
+ export {};