@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.
- package/LICENSE +21 -0
- package/README.md +281 -4
- package/REFERENCE.md +264 -0
- package/dist/index.cjs +627 -0
- package/dist/index.d.cts +374 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.js +611 -0
- package/llms.txt +305 -0
- package/package.json +52 -6
- package/reaffirm.mjs +76 -0
- package/index.js +0 -6
package/dist/index.d.cts
ADDED
|
@@ -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 {};
|