@gemmein/sdk 0.0.1 → 0.1.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,337 @@
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
+ export declare class Gemmein {
129
+ readonly auth: AuthClient;
130
+ readonly storage: StorageClient;
131
+ constructor(options: GemmeinOptions);
132
+ /** The signed-in user's subscription — `(await g.subscription())?.plan === "pro"`. */
133
+ subscription(): Promise<{
134
+ plan: string;
135
+ status: "active" | "cancelled";
136
+ } | null>;
137
+ /** Send the signed-in user to Stripe checkout for a plan — `g.checkout("pro")`. Redirects in browsers. */
138
+ checkout(plan?: string): Promise<{
139
+ url: string;
140
+ plan: string;
141
+ }>;
142
+ /**
143
+ * Buy a one-off product — `g.pay("poster")`. Redirects in browsers. NOT
144
+ * for plans/subscriptions (that's `g.checkout`). The optional `item` note
145
+ * names WHAT is being bought when one product covers many things (e.g. a
146
+ * license tier across a catalog): `g.pay("premium license", { item: "beat_37" })`
147
+ * — it lands on the buyer's receipt record for the owner to fulfil.
148
+ */
149
+ pay(product: string, options?: {
150
+ item?: string;
151
+ }): Promise<{
152
+ url: string;
153
+ product: string;
154
+ item?: string;
155
+ }>;
156
+ }
157
+ export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
158
+ export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
159
+ type ClientConfig = {
160
+ apiUrl: string;
161
+ appKey: string;
162
+ tokenStore: TokenStore;
163
+ };
164
+ export declare class AuthClient {
165
+ private readonly config;
166
+ constructor(config: ClientConfig);
167
+ sendEmailCode(email: string): Promise<void>;
168
+ verifyEmailCode(input: {
169
+ email: string;
170
+ code: string;
171
+ }): Promise<AuthSession>;
172
+ logout(): Promise<void>;
173
+ deleteAccount(): Promise<unknown>;
174
+ subscription(): Promise<{
175
+ plan: string;
176
+ status: "active" | "cancelled";
177
+ } | null>;
178
+ /**
179
+ * SUBS-2: start a Stripe checkout for a plan — Gemmein mints the URL with
180
+ * the signed-in buyer and the plan already wired in (never build checkout
181
+ * URLs yourself; raw emails get silently dropped by Stripe's URL rules).
182
+ * In a browser this redirects immediately; it also resolves with the URL
183
+ * (for non-browser callers or custom handling). Requires a signed-in user
184
+ * and a plan whose Payment Link the app owner has pasted in their
185
+ * dashboard; omit `plan` to buy the app's paid plan.
186
+ */
187
+ checkout(plan?: string): Promise<{
188
+ url: string;
189
+ plan: string;
190
+ }>;
191
+ /**
192
+ * PAY-1: buy a one-off product — same contract as checkout() but for
193
+ * things, not plans. A completed payment writes a receipt record addressed
194
+ * to the buyer in the owner's receipts collection; gate downloads/fulfilment
195
+ * on that receipt, never on the redirect coming back.
196
+ */
197
+ pay(product: string, options?: {
198
+ item?: string;
199
+ }): Promise<{
200
+ url: string;
201
+ product: string;
202
+ item?: string;
203
+ }>;
204
+ currentUser(): Promise<CurrentUser>;
205
+ private request;
206
+ }
207
+ export declare class StorageClient {
208
+ private readonly config;
209
+ constructor(config: ClientConfig);
210
+ /** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
211
+ collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string): CollectionClient<T>;
212
+ }
213
+ /**
214
+ * Talks to one collection. Collections themselves are created by the app
215
+ * owner in their dashboard (app.gemmein.com → data) — a 404
216
+ * `unknown_collection` means it doesn't exist yet: ask the owner to create
217
+ * it there, don't retry.
218
+ */
219
+ export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
220
+ private readonly config;
221
+ private readonly name;
222
+ constructor(config: ClientConfig, name: string);
223
+ /**
224
+ * Create a record from your fields. The signed-in user becomes its owner.
225
+ *
226
+ * For anything two users can race for (a booking slot, a unique slug, a
227
+ * limited drop), pass a deterministic `key` derived from the thing that
228
+ * must be unique: `create(data, { key: "slot:2026-07-15T15:00" })`.
229
+ * The second writer gets a 409 GemmeinError `conflict` — that error IS
230
+ * the booking system working: catch it and tell the user it's taken.
231
+ * Your own retry with the same key returns the record you already made
232
+ * (`existing: true`) instead of a duplicate. Deleting a keyed record
233
+ * frees its key.
234
+ *
235
+ * On `addressed` and `direct` collections every create names its
236
+ * recipient: `create(data, { for: userId })` — the server stamps it,
237
+ * and only that user (plus the sender/owner) will ever read the record.
238
+ * Sending to a non-user is a 400 `invalid_audience`; a 403 `reply_only`
239
+ * means this collection only allows replies to people who wrote to you
240
+ * first — tell the user, don't retry.
241
+ *
242
+ * On the PUBLIC rules (public_read, community) pass `{ published: false }`
243
+ * to save a DRAFT the public can't see (the author still sees their own;
244
+ * the owner sees all). Publish later with
245
+ * `update(id, {}, { published: true })`. This is server-enforced — never
246
+ * fake drafts with a data field + client-side filtering: on a public
247
+ * collection the data still reaches everyone.
248
+ */
249
+ create(data: T, options?: {
250
+ key?: string;
251
+ for?: string;
252
+ published?: boolean;
253
+ }): Promise<GemmeinRecord<T>>;
254
+ /**
255
+ * List records this user is allowed to see under the collection's safety
256
+ * rule (the app owner sees everyone's). Returns `{ records, hasMore }` —
257
+ * an object, not a bare array.
258
+ */
259
+ list(options?: ListOptions): Promise<ListResult<T>>;
260
+ get(id: string, options?: {
261
+ expand?: string[];
262
+ }): Promise<GemmeinRecord<T>>;
263
+ /**
264
+ * Merge-updates `data` fields; returns the full updated record.
265
+ *
266
+ * Counters that users race for (stock, seats) must never be computed
267
+ * client-side — put an atomic op in value position and the server does
268
+ * the math on current state: `update(id, { stock: { decrement: 1,
269
+ * floor: 0 } })`. Breaching the floor/ceiling → 409 `conflict` ("out of
270
+ * stock" — the limit working, not a bug). An object is treated as an op
271
+ * ONLY when its keys are exactly increment|decrement (+ optional
272
+ * floor|ceiling), all numbers.
273
+ *
274
+ * When different people can edit the same record (CMS pages, shared
275
+ * docs), pass `{ ifVersion: record.version }` — a stale save gets a 409
276
+ * `conflict` instead of clobbering; re-read, reapply, retry.
277
+ */
278
+ update(id: string, data: Partial<T> | Record<string, unknown>, options?: {
279
+ ifVersion?: number;
280
+ published?: boolean;
281
+ }): Promise<GemmeinRecord<T>>;
282
+ delete(id: string): Promise<void>;
283
+ upload(file: Blob | File, options?: {
284
+ name?: string;
285
+ }): Promise<{
286
+ id: string;
287
+ url: string;
288
+ contentType: string;
289
+ sizeBytes: number;
290
+ }>;
291
+ private request;
292
+ }
293
+ export type GemmeinServerOptions = {
294
+ secretKey: string;
295
+ apiUrl?: string;
296
+ };
297
+ export declare class GemmeinServer {
298
+ private readonly apiUrl;
299
+ private readonly secretKey;
300
+ constructor(options: GemmeinServerOptions);
301
+ collection(name: string): ServerCollectionClient;
302
+ /**
303
+ * Mint a member session for a test email WITHOUT an OTP round-trip — so a CI
304
+ * self-test ("reaffirm") can sign in as N test users and prove your app's
305
+ * isolation boundaries hold (user B genuinely can't read user A's private
306
+ * records). DEV ENVIRONMENTS ONLY: throws `test_session_forbidden_live` on an
307
+ * `sk_live` key, and the server refuses it too. Never ship this in app code.
308
+ * Pass the returned `token` to `gemmein(pk, { tokenStore })` to act as that user.
309
+ */
310
+ testSession(email: string): Promise<{
311
+ token: string;
312
+ expiresAt: string;
313
+ user: {
314
+ id: string;
315
+ email: string;
316
+ role: string;
317
+ };
318
+ }>;
319
+ }
320
+ declare class ServerCollectionClient {
321
+ private readonly apiUrl;
322
+ private readonly secretKey;
323
+ private readonly name;
324
+ constructor(apiUrl: string, secretKey: string, name: string);
325
+ get(id: string): Promise<unknown>;
326
+ list(options?: {
327
+ limit?: number;
328
+ sort?: "newest" | "oldest" | "updated";
329
+ where?: Record<string, unknown>;
330
+ cursor?: string;
331
+ search?: string;
332
+ expand?: string[];
333
+ }): Promise<unknown>;
334
+ update(id: string, data: Record<string, unknown>): Promise<unknown>;
335
+ private request;
336
+ }
337
+ export {};
@@ -0,0 +1,337 @@
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
+ export declare class Gemmein {
129
+ readonly auth: AuthClient;
130
+ readonly storage: StorageClient;
131
+ constructor(options: GemmeinOptions);
132
+ /** The signed-in user's subscription — `(await g.subscription())?.plan === "pro"`. */
133
+ subscription(): Promise<{
134
+ plan: string;
135
+ status: "active" | "cancelled";
136
+ } | null>;
137
+ /** Send the signed-in user to Stripe checkout for a plan — `g.checkout("pro")`. Redirects in browsers. */
138
+ checkout(plan?: string): Promise<{
139
+ url: string;
140
+ plan: string;
141
+ }>;
142
+ /**
143
+ * Buy a one-off product — `g.pay("poster")`. Redirects in browsers. NOT
144
+ * for plans/subscriptions (that's `g.checkout`). The optional `item` note
145
+ * names WHAT is being bought when one product covers many things (e.g. a
146
+ * license tier across a catalog): `g.pay("premium license", { item: "beat_37" })`
147
+ * — it lands on the buyer's receipt record for the owner to fulfil.
148
+ */
149
+ pay(product: string, options?: {
150
+ item?: string;
151
+ }): Promise<{
152
+ url: string;
153
+ product: string;
154
+ item?: string;
155
+ }>;
156
+ }
157
+ export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
158
+ export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
159
+ type ClientConfig = {
160
+ apiUrl: string;
161
+ appKey: string;
162
+ tokenStore: TokenStore;
163
+ };
164
+ export declare class AuthClient {
165
+ private readonly config;
166
+ constructor(config: ClientConfig);
167
+ sendEmailCode(email: string): Promise<void>;
168
+ verifyEmailCode(input: {
169
+ email: string;
170
+ code: string;
171
+ }): Promise<AuthSession>;
172
+ logout(): Promise<void>;
173
+ deleteAccount(): Promise<unknown>;
174
+ subscription(): Promise<{
175
+ plan: string;
176
+ status: "active" | "cancelled";
177
+ } | null>;
178
+ /**
179
+ * SUBS-2: start a Stripe checkout for a plan — Gemmein mints the URL with
180
+ * the signed-in buyer and the plan already wired in (never build checkout
181
+ * URLs yourself; raw emails get silently dropped by Stripe's URL rules).
182
+ * In a browser this redirects immediately; it also resolves with the URL
183
+ * (for non-browser callers or custom handling). Requires a signed-in user
184
+ * and a plan whose Payment Link the app owner has pasted in their
185
+ * dashboard; omit `plan` to buy the app's paid plan.
186
+ */
187
+ checkout(plan?: string): Promise<{
188
+ url: string;
189
+ plan: string;
190
+ }>;
191
+ /**
192
+ * PAY-1: buy a one-off product — same contract as checkout() but for
193
+ * things, not plans. A completed payment writes a receipt record addressed
194
+ * to the buyer in the owner's receipts collection; gate downloads/fulfilment
195
+ * on that receipt, never on the redirect coming back.
196
+ */
197
+ pay(product: string, options?: {
198
+ item?: string;
199
+ }): Promise<{
200
+ url: string;
201
+ product: string;
202
+ item?: string;
203
+ }>;
204
+ currentUser(): Promise<CurrentUser>;
205
+ private request;
206
+ }
207
+ export declare class StorageClient {
208
+ private readonly config;
209
+ constructor(config: ClientConfig);
210
+ /** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
211
+ collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string): CollectionClient<T>;
212
+ }
213
+ /**
214
+ * Talks to one collection. Collections themselves are created by the app
215
+ * owner in their dashboard (app.gemmein.com → data) — a 404
216
+ * `unknown_collection` means it doesn't exist yet: ask the owner to create
217
+ * it there, don't retry.
218
+ */
219
+ export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
220
+ private readonly config;
221
+ private readonly name;
222
+ constructor(config: ClientConfig, name: string);
223
+ /**
224
+ * Create a record from your fields. The signed-in user becomes its owner.
225
+ *
226
+ * For anything two users can race for (a booking slot, a unique slug, a
227
+ * limited drop), pass a deterministic `key` derived from the thing that
228
+ * must be unique: `create(data, { key: "slot:2026-07-15T15:00" })`.
229
+ * The second writer gets a 409 GemmeinError `conflict` — that error IS
230
+ * the booking system working: catch it and tell the user it's taken.
231
+ * Your own retry with the same key returns the record you already made
232
+ * (`existing: true`) instead of a duplicate. Deleting a keyed record
233
+ * frees its key.
234
+ *
235
+ * On `addressed` and `direct` collections every create names its
236
+ * recipient: `create(data, { for: userId })` — the server stamps it,
237
+ * and only that user (plus the sender/owner) will ever read the record.
238
+ * Sending to a non-user is a 400 `invalid_audience`; a 403 `reply_only`
239
+ * means this collection only allows replies to people who wrote to you
240
+ * first — tell the user, don't retry.
241
+ *
242
+ * On the PUBLIC rules (public_read, community) pass `{ published: false }`
243
+ * to save a DRAFT the public can't see (the author still sees their own;
244
+ * the owner sees all). Publish later with
245
+ * `update(id, {}, { published: true })`. This is server-enforced — never
246
+ * fake drafts with a data field + client-side filtering: on a public
247
+ * collection the data still reaches everyone.
248
+ */
249
+ create(data: T, options?: {
250
+ key?: string;
251
+ for?: string;
252
+ published?: boolean;
253
+ }): Promise<GemmeinRecord<T>>;
254
+ /**
255
+ * List records this user is allowed to see under the collection's safety
256
+ * rule (the app owner sees everyone's). Returns `{ records, hasMore }` —
257
+ * an object, not a bare array.
258
+ */
259
+ list(options?: ListOptions): Promise<ListResult<T>>;
260
+ get(id: string, options?: {
261
+ expand?: string[];
262
+ }): Promise<GemmeinRecord<T>>;
263
+ /**
264
+ * Merge-updates `data` fields; returns the full updated record.
265
+ *
266
+ * Counters that users race for (stock, seats) must never be computed
267
+ * client-side — put an atomic op in value position and the server does
268
+ * the math on current state: `update(id, { stock: { decrement: 1,
269
+ * floor: 0 } })`. Breaching the floor/ceiling → 409 `conflict` ("out of
270
+ * stock" — the limit working, not a bug). An object is treated as an op
271
+ * ONLY when its keys are exactly increment|decrement (+ optional
272
+ * floor|ceiling), all numbers.
273
+ *
274
+ * When different people can edit the same record (CMS pages, shared
275
+ * docs), pass `{ ifVersion: record.version }` — a stale save gets a 409
276
+ * `conflict` instead of clobbering; re-read, reapply, retry.
277
+ */
278
+ update(id: string, data: Partial<T> | Record<string, unknown>, options?: {
279
+ ifVersion?: number;
280
+ published?: boolean;
281
+ }): Promise<GemmeinRecord<T>>;
282
+ delete(id: string): Promise<void>;
283
+ upload(file: Blob | File, options?: {
284
+ name?: string;
285
+ }): Promise<{
286
+ id: string;
287
+ url: string;
288
+ contentType: string;
289
+ sizeBytes: number;
290
+ }>;
291
+ private request;
292
+ }
293
+ export type GemmeinServerOptions = {
294
+ secretKey: string;
295
+ apiUrl?: string;
296
+ };
297
+ export declare class GemmeinServer {
298
+ private readonly apiUrl;
299
+ private readonly secretKey;
300
+ constructor(options: GemmeinServerOptions);
301
+ collection(name: string): ServerCollectionClient;
302
+ /**
303
+ * Mint a member session for a test email WITHOUT an OTP round-trip — so a CI
304
+ * self-test ("reaffirm") can sign in as N test users and prove your app's
305
+ * isolation boundaries hold (user B genuinely can't read user A's private
306
+ * records). DEV ENVIRONMENTS ONLY: throws `test_session_forbidden_live` on an
307
+ * `sk_live` key, and the server refuses it too. Never ship this in app code.
308
+ * Pass the returned `token` to `gemmein(pk, { tokenStore })` to act as that user.
309
+ */
310
+ testSession(email: string): Promise<{
311
+ token: string;
312
+ expiresAt: string;
313
+ user: {
314
+ id: string;
315
+ email: string;
316
+ role: string;
317
+ };
318
+ }>;
319
+ }
320
+ declare class ServerCollectionClient {
321
+ private readonly apiUrl;
322
+ private readonly secretKey;
323
+ private readonly name;
324
+ constructor(apiUrl: string, secretKey: string, name: string);
325
+ get(id: string): Promise<unknown>;
326
+ list(options?: {
327
+ limit?: number;
328
+ sort?: "newest" | "oldest" | "updated";
329
+ where?: Record<string, unknown>;
330
+ cursor?: string;
331
+ search?: string;
332
+ expand?: string[];
333
+ }): Promise<unknown>;
334
+ update(id: string, data: Record<string, unknown>): Promise<unknown>;
335
+ private request;
336
+ }
337
+ export {};