@gemmein/sdk 0.2.0 → 0.3.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/README.md +19 -2
- package/REFERENCE.md +44 -2
- package/dist/index.cjs +90 -6
- package/dist/index.d.cts +110 -4
- package/dist/index.d.ts +110 -4
- package/dist/index.js +87 -5
- package/llms.txt +48 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,11 +80,28 @@ const one = await tasks.get("rec_abc123")
|
|
|
80
80
|
await tasks.update("rec_abc123", { done: true })
|
|
81
81
|
await tasks.delete("rec_abc123")
|
|
82
82
|
|
|
83
|
-
// Images/files: presigned upload, returns a
|
|
83
|
+
// Images/files: presigned upload, returns a REFERENCE to store in a record
|
|
84
84
|
const file = await tasks.upload(imageBlob, { name: "avatar.png" })
|
|
85
|
-
// { id,
|
|
85
|
+
// { id, ref, contentType, sizeBytes } ref looks like "file:01K…"
|
|
86
|
+
await tasks.create({ title: "Profile", avatar: file.ref })
|
|
87
|
+
|
|
88
|
+
// To show or download it — one call, whatever the collection:
|
|
89
|
+
const { url } = await g.files.link(record.avatar)
|
|
90
|
+
// public collection → a permanent, cacheable URL
|
|
91
|
+
// anything else → a signed URL valid for a couple of minutes, re-checked
|
|
92
|
+
// against who you are and what you still hold
|
|
86
93
|
```
|
|
87
94
|
|
|
95
|
+
Store the reference, never the URL. A reference doesn't expire and grants
|
|
96
|
+
nothing on its own; `link()` is where authorization happens, every time. That
|
|
97
|
+
also means the same app code keeps working if a collection later changes from
|
|
98
|
+
public to private.
|
|
99
|
+
|
|
100
|
+
**The bound, honestly:** revoking access stops Gemmein issuing new links
|
|
101
|
+
immediately. A link already in someone's hands works until it expires. Nothing
|
|
102
|
+
can take back a file they already downloaded — this is controlled delivery, not
|
|
103
|
+
DRM.
|
|
104
|
+
|
|
88
105
|
### Collection rules
|
|
89
106
|
|
|
90
107
|
Each collection has one rule, set in the dashboard. The server enforces it — your app never implements authorization:
|
package/REFERENCE.md
CHANGED
|
@@ -72,12 +72,18 @@ deleted, subscription row removed. Irreversible: put a real confirm in front.
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
-
## Data — `g.collection<T>(name)`
|
|
75
|
+
## Data — `g.collection<T>(name, options?)`
|
|
76
76
|
|
|
77
77
|
`name` must be **lowercase letters, numbers, and underscores** (`saved_games`,
|
|
78
78
|
never `savedGames` — a bad name throws synchronously). Collections are created
|
|
79
79
|
by the app owner in the dashboard, never by the SDK.
|
|
80
80
|
|
|
81
|
+
`options.intent` — one sentence: what the collection is for and who should
|
|
82
|
+
access it. It rides every call as a hint; against a **local `gemmein dev`
|
|
83
|
+
runtime** an undeclared collection then reaches the human with your
|
|
84
|
+
suggestion attached. The cloud ignores it. Pass it whenever you aren't
|
|
85
|
+
certain the collection exists yet.
|
|
86
|
+
|
|
81
87
|
| Method | Signature | Returns |
|
|
82
88
|
|--------|-----------|---------|
|
|
83
89
|
| `create` | `(data: T, options?: { key?: string; for?: string; published?: boolean })` | `Promise<GemmeinRecord<T>>` |
|
|
@@ -85,7 +91,40 @@ by the app owner in the dashboard, never by the SDK.
|
|
|
85
91
|
| `get` | `(id: string, options?: { expand?: string[] })` | `Promise<GemmeinRecord<T>>` |
|
|
86
92
|
| `update` | `(id: string, data: Partial<T> \| { field: { increment\|decrement, floor?, ceiling? } }, options?: { ifVersion?: number; published?: boolean })` | `Promise<GemmeinRecord<T>>` |
|
|
87
93
|
| `delete` | `(id: string)` | `Promise<void>` |
|
|
88
|
-
| `upload` | `(file: Blob \| File, options?: { name?: string })` | `Promise<{ id: string;
|
|
94
|
+
| `upload` | `(file: Blob \| File, options?: { name?: string })` | `Promise<{ id: string; ref: FileRef; contentType: string; sizeBytes: number }>` |
|
|
95
|
+
|
|
96
|
+
### Files
|
|
97
|
+
|
|
98
|
+
`upload()` returns a **reference** (`file:01K…`), not a URL. Store the reference
|
|
99
|
+
in your record — it never expires and grants nothing on its own.
|
|
100
|
+
|
|
101
|
+
| Method | Signature | Returns |
|
|
102
|
+
|--------|-----------|---------|
|
|
103
|
+
| `g.files.link` | `(ref: FileRef \| string, options?: { intent?: "inline" \| "download" })` | `Promise<{ ref, url, expiresAt?, contentType, sizeBytes?, name? }>` |
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const { ref } = await g.collection("films").upload(file)
|
|
107
|
+
await g.collection("films").create({ title, poster: ref })
|
|
108
|
+
|
|
109
|
+
// later, to render or download:
|
|
110
|
+
const { url } = await g.files.link(record.poster)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One call for every file. A collection anyone can read gives a permanent,
|
|
114
|
+
cacheable URL; anything else gives one that expires in a couple of minutes and
|
|
115
|
+
is re-checked against who you are, what the collection's rule says, whether the
|
|
116
|
+
file is yours, and any entitlement the collection requires. That is why the
|
|
117
|
+
same code keeps working when a collection changes from public to private.
|
|
118
|
+
|
|
119
|
+
**Don't store what `link()` returns.** Store the reference and call it again.
|
|
120
|
+
|
|
121
|
+
`403 entitlement_required` from `link()` names the key the customer is
|
|
122
|
+
missing. `404` means the file isn't theirs, or isn't there.
|
|
123
|
+
|
|
124
|
+
**The bound, honestly:** revoking access stops Gemmein issuing *new* links
|
|
125
|
+
immediately; a link already issued works until it expires, and a download that
|
|
126
|
+
started before expiry may finish after it. Controlled delivery, not DRM —
|
|
127
|
+
nothing takes back a file someone already downloaded.
|
|
89
128
|
|
|
90
129
|
```ts
|
|
91
130
|
type GemmeinRecord<T> = {
|
|
@@ -260,5 +299,8 @@ Branch on `err.code`. The stable codes:
|
|
|
260
299
|
| `unsupported_file_type` (415) | upload isn't one of the allowed image types | send JPEG/PNG/WebP/GIF/HEIC |
|
|
261
300
|
| `invalid_key` | a keyed create's `key` breaks the charset/length law | 1-120 chars of letters, numbers, `: _ . @ / -` |
|
|
262
301
|
| `invalid_secret_key` (client-side) | `gemmeinServer()` got a missing/`pk_` key | pass the `sk_` key from a server env var |
|
|
302
|
+
| `authentication_required` (401) | checkout/subscription/pay without a signed-in user | sign the user in first |
|
|
303
|
+
| `plan_has_no_link` (409) | the paid plan has no Payment Link pasted yet | ask the owner to paste it in their dashboard |
|
|
304
|
+
| `account_suspended` (403) | the app owner's account is suspended (billing) | the owner fixes payment at app.gemmein.com |
|
|
263
305
|
|
|
264
306
|
Keys: `pk_` (public, domain-locked, browser-safe) vs `sk_` (secret, server only).
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = void 0;
|
|
3
|
+
exports.GemmeinServer = exports.CollectionClient = exports.StorageClient = exports.AccountClient = exports.PaymentsClient = exports.SubscriptionsClient = exports.FilesClient = exports.PurchasesClient = exports.AuthClient = exports.Gemmein = exports.BrowserTokenStore = exports.MemoryTokenStore = exports.GemmeinError = void 0;
|
|
4
4
|
exports.gemmein = gemmein;
|
|
5
5
|
exports.gemmeinServer = gemmeinServer;
|
|
6
6
|
class GemmeinError extends Error {
|
|
@@ -107,14 +107,22 @@ class Gemmein {
|
|
|
107
107
|
this.storage = new StorageClient(config);
|
|
108
108
|
this.subscriptions = new SubscriptionsClient(config);
|
|
109
109
|
this.payments = new PaymentsClient(config);
|
|
110
|
+
this.purchases = new PurchasesClient(config);
|
|
110
111
|
this.account = new AccountClient(config);
|
|
112
|
+
this.files = new FilesClient(config);
|
|
111
113
|
}
|
|
112
114
|
/**
|
|
113
115
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
114
116
|
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
117
|
+
*
|
|
118
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
119
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
120
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
121
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
122
|
+
* collection exists yet. The cloud ignores it.
|
|
115
123
|
*/
|
|
116
|
-
collection(name) {
|
|
117
|
-
return this.storage.collection(name);
|
|
124
|
+
collection(name, options = {}) {
|
|
125
|
+
return this.storage.collection(name, options);
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
exports.Gemmein = Gemmein;
|
|
@@ -194,6 +202,61 @@ exports.AuthClient = AuthClient;
|
|
|
194
202
|
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
195
203
|
* there is no client write path to plan or status, by design.
|
|
196
204
|
*/
|
|
205
|
+
/**
|
|
206
|
+
* The signed-in customer's own purchase history, read from Gemmein's
|
|
207
|
+
* immutable commercial record rather than from your data.
|
|
208
|
+
*
|
|
209
|
+
* This exists so a buyer keeps proof of what they paid for even when you keep
|
|
210
|
+
* no receipts collection at all — and it stays true if you later rename or
|
|
211
|
+
* delete one, because financial truth does not live in application data.
|
|
212
|
+
*
|
|
213
|
+
* Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
|
|
214
|
+
* as the payment provider reported them. Formatting is yours; rounding here
|
|
215
|
+
* would quietly lose money.
|
|
216
|
+
*/
|
|
217
|
+
class PurchasesClient {
|
|
218
|
+
constructor(config) {
|
|
219
|
+
this.config = config;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Everything this customer has paid for, newest first, with refunds already
|
|
223
|
+
* applied — `status` is "paid", "part_refunded" or "refunded", and
|
|
224
|
+
* `refundedMinor` is how much has come back. Throws GemmeinError (401) when
|
|
225
|
+
* nobody is signed in.
|
|
226
|
+
*/
|
|
227
|
+
async mine() {
|
|
228
|
+
const result = (await runtimeRequest(this.config, "/auth/purchases"));
|
|
229
|
+
return result.purchases;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.PurchasesClient = PurchasesClient;
|
|
233
|
+
/**
|
|
234
|
+
* Turn a stored file reference into a URL you can actually use.
|
|
235
|
+
*
|
|
236
|
+
* const { url } = await g.files.link(record.poster)
|
|
237
|
+
* img.src = url
|
|
238
|
+
*
|
|
239
|
+
* One call for every file, whichever collection it lives in. If the collection
|
|
240
|
+
* is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
|
|
241
|
+
* get one that works for a couple of minutes and is re-checked against who you
|
|
242
|
+
* are, what the collection's rule says, whether the file is yours, and whether
|
|
243
|
+
* you still hold whatever the collection requires.
|
|
244
|
+
*
|
|
245
|
+
* That is why the same app code keeps working when a collection later changes
|
|
246
|
+
* from public to private — and why a refund actually takes a download away.
|
|
247
|
+
*
|
|
248
|
+
* Don't store what this returns. Store the reference and call this again.
|
|
249
|
+
*/
|
|
250
|
+
class FilesClient {
|
|
251
|
+
constructor(config) {
|
|
252
|
+
this.config = config;
|
|
253
|
+
}
|
|
254
|
+
async link(ref, options = {}) {
|
|
255
|
+
const query = options.intent === "download" ? "?intent=download" : "";
|
|
256
|
+
return (await runtimeRequest(this.config, `/files/${encodeURIComponent(String(ref))}/link${query}`));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
exports.FilesClient = FilesClient;
|
|
197
260
|
class SubscriptionsClient {
|
|
198
261
|
constructor(config) {
|
|
199
262
|
this.config = config;
|
|
@@ -286,9 +349,9 @@ class StorageClient {
|
|
|
286
349
|
this.config = config;
|
|
287
350
|
}
|
|
288
351
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
289
|
-
collection(name) {
|
|
352
|
+
collection(name, options = {}) {
|
|
290
353
|
assertCollectionName(name);
|
|
291
|
-
return new CollectionClient(this.config, name);
|
|
354
|
+
return new CollectionClient(this.config, name, options);
|
|
292
355
|
}
|
|
293
356
|
}
|
|
294
357
|
exports.StorageClient = StorageClient;
|
|
@@ -299,9 +362,10 @@ exports.StorageClient = StorageClient;
|
|
|
299
362
|
* it there, don't retry.
|
|
300
363
|
*/
|
|
301
364
|
class CollectionClient {
|
|
302
|
-
constructor(config, name) {
|
|
365
|
+
constructor(config, name, options = {}) {
|
|
303
366
|
this.config = config;
|
|
304
367
|
this.name = name;
|
|
368
|
+
this.intent = options.intent;
|
|
305
369
|
}
|
|
306
370
|
/**
|
|
307
371
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
@@ -401,6 +465,22 @@ class CollectionClient {
|
|
|
401
465
|
async delete(id) {
|
|
402
466
|
await this.request(`/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
403
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
|
|
470
|
+
*
|
|
471
|
+
* Store the reference. It never expires, it is safe to log and export, and
|
|
472
|
+
* it grants nothing on its own. To show or download the file, call
|
|
473
|
+
* `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
|
|
474
|
+
* what makes revoking access actually take a download away.
|
|
475
|
+
*
|
|
476
|
+
* const { ref } = await g.collection("films").upload(file)
|
|
477
|
+
* await g.collection("films").create({ title, poster: ref })
|
|
478
|
+
* // later, to render:
|
|
479
|
+
* const { url } = await g.files.link(record.poster)
|
|
480
|
+
*
|
|
481
|
+
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
482
|
+
* bug this replaced.
|
|
483
|
+
*/
|
|
404
484
|
async upload(file, options) {
|
|
405
485
|
const name = options?.name ?? (file instanceof File ? file.name : "upload");
|
|
406
486
|
// Step 1: Get presigned upload URL
|
|
@@ -433,6 +513,10 @@ class CollectionClient {
|
|
|
433
513
|
...init,
|
|
434
514
|
headers: await runtimeHeaders(this.config, {
|
|
435
515
|
"content-type": "application/json",
|
|
516
|
+
// The intent rides every call so an undeclared collection reaches
|
|
517
|
+
// the human WITH the AI's suggestion attached (local runtime only;
|
|
518
|
+
// the cloud ignores it).
|
|
519
|
+
...(this.intent ? { "x-collection-intent": this.intent.slice(0, 200) } : {}),
|
|
436
520
|
...init.headers
|
|
437
521
|
})
|
|
438
522
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -143,13 +143,23 @@ export declare class Gemmein {
|
|
|
143
143
|
readonly storage: StorageClient;
|
|
144
144
|
readonly subscriptions: SubscriptionsClient;
|
|
145
145
|
readonly payments: PaymentsClient;
|
|
146
|
+
readonly purchases: PurchasesClient;
|
|
146
147
|
readonly account: AccountClient;
|
|
148
|
+
readonly files: FilesClient;
|
|
147
149
|
constructor(options: GemmeinOptions);
|
|
148
150
|
/**
|
|
149
151
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
150
152
|
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
153
|
+
*
|
|
154
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
155
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
156
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
157
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
158
|
+
* collection exists yet. The cloud ignores it.
|
|
151
159
|
*/
|
|
152
|
-
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string
|
|
160
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
161
|
+
intent?: string;
|
|
162
|
+
}): CollectionClient<T>;
|
|
153
163
|
}
|
|
154
164
|
export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
|
|
155
165
|
export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
|
|
@@ -177,6 +187,81 @@ export declare class AuthClient {
|
|
|
177
187
|
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
178
188
|
* there is no client write path to plan or status, by design.
|
|
179
189
|
*/
|
|
190
|
+
/**
|
|
191
|
+
* The signed-in customer's own purchase history, read from Gemmein's
|
|
192
|
+
* immutable commercial record rather than from your data.
|
|
193
|
+
*
|
|
194
|
+
* This exists so a buyer keeps proof of what they paid for even when you keep
|
|
195
|
+
* no receipts collection at all — and it stays true if you later rename or
|
|
196
|
+
* delete one, because financial truth does not live in application data.
|
|
197
|
+
*
|
|
198
|
+
* Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
|
|
199
|
+
* as the payment provider reported them. Formatting is yours; rounding here
|
|
200
|
+
* would quietly lose money.
|
|
201
|
+
*/
|
|
202
|
+
export declare class PurchasesClient {
|
|
203
|
+
private readonly config;
|
|
204
|
+
constructor(config: ClientConfig);
|
|
205
|
+
/**
|
|
206
|
+
* Everything this customer has paid for, newest first, with refunds already
|
|
207
|
+
* applied — `status` is "paid", "part_refunded" or "refunded", and
|
|
208
|
+
* `refundedMinor` is how much has come back. Throws GemmeinError (401) when
|
|
209
|
+
* nobody is signed in.
|
|
210
|
+
*/
|
|
211
|
+
mine(): Promise<Array<{
|
|
212
|
+
item: string;
|
|
213
|
+
kind: "purchase" | "subscription";
|
|
214
|
+
amountMinor: number | null;
|
|
215
|
+
currency: string | null;
|
|
216
|
+
refundedMinor: number;
|
|
217
|
+
status: "paid" | "part_refunded" | "refunded";
|
|
218
|
+
grants: string[];
|
|
219
|
+
paidAt: string;
|
|
220
|
+
}>>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A file reference — `file:01K…`. What `upload()` gives you and what your
|
|
224
|
+
* record should store.
|
|
225
|
+
*
|
|
226
|
+
* Branded so it cannot be mistaken for a URL: `<img src={record.poster}>` is a
|
|
227
|
+
* type error, which is the cheapest possible moment to learn that a reference
|
|
228
|
+
* is a name and not a location.
|
|
229
|
+
*/
|
|
230
|
+
export type FileRef = string & {
|
|
231
|
+
readonly __gemmeinFileRef: unique symbol;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Turn a stored file reference into a URL you can actually use.
|
|
235
|
+
*
|
|
236
|
+
* const { url } = await g.files.link(record.poster)
|
|
237
|
+
* img.src = url
|
|
238
|
+
*
|
|
239
|
+
* One call for every file, whichever collection it lives in. If the collection
|
|
240
|
+
* is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
|
|
241
|
+
* get one that works for a couple of minutes and is re-checked against who you
|
|
242
|
+
* are, what the collection's rule says, whether the file is yours, and whether
|
|
243
|
+
* you still hold whatever the collection requires.
|
|
244
|
+
*
|
|
245
|
+
* That is why the same app code keeps working when a collection later changes
|
|
246
|
+
* from public to private — and why a refund actually takes a download away.
|
|
247
|
+
*
|
|
248
|
+
* Don't store what this returns. Store the reference and call this again.
|
|
249
|
+
*/
|
|
250
|
+
export declare class FilesClient {
|
|
251
|
+
private readonly config;
|
|
252
|
+
constructor(config: ClientConfig);
|
|
253
|
+
link(ref: FileRef | string, options?: {
|
|
254
|
+
intent?: "inline" | "download";
|
|
255
|
+
}): Promise<{
|
|
256
|
+
ref: FileRef;
|
|
257
|
+
url: string;
|
|
258
|
+
/** Absent when the collection is public — those links don't expire. */
|
|
259
|
+
expiresAt?: string;
|
|
260
|
+
contentType: string;
|
|
261
|
+
sizeBytes?: number;
|
|
262
|
+
name?: string;
|
|
263
|
+
}>;
|
|
264
|
+
}
|
|
180
265
|
export declare class SubscriptionsClient {
|
|
181
266
|
private readonly config;
|
|
182
267
|
constructor(config: ClientConfig);
|
|
@@ -245,7 +330,9 @@ export declare class StorageClient {
|
|
|
245
330
|
private readonly config;
|
|
246
331
|
constructor(config: ClientConfig);
|
|
247
332
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
248
|
-
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string
|
|
333
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
334
|
+
intent?: string;
|
|
335
|
+
}): CollectionClient<T>;
|
|
249
336
|
}
|
|
250
337
|
/**
|
|
251
338
|
* Talks to one collection. Collections themselves are created by the app
|
|
@@ -256,7 +343,10 @@ export declare class StorageClient {
|
|
|
256
343
|
export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
257
344
|
private readonly config;
|
|
258
345
|
private readonly name;
|
|
259
|
-
|
|
346
|
+
private readonly intent?;
|
|
347
|
+
constructor(config: ClientConfig, name: string, options?: {
|
|
348
|
+
intent?: string;
|
|
349
|
+
});
|
|
260
350
|
/**
|
|
261
351
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
262
352
|
*
|
|
@@ -317,11 +407,27 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
|
|
|
317
407
|
published?: boolean;
|
|
318
408
|
}): Promise<GemmeinRecord<T>>;
|
|
319
409
|
delete(id: string): Promise<void>;
|
|
410
|
+
/**
|
|
411
|
+
* Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
|
|
412
|
+
*
|
|
413
|
+
* Store the reference. It never expires, it is safe to log and export, and
|
|
414
|
+
* it grants nothing on its own. To show or download the file, call
|
|
415
|
+
* `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
|
|
416
|
+
* what makes revoking access actually take a download away.
|
|
417
|
+
*
|
|
418
|
+
* const { ref } = await g.collection("films").upload(file)
|
|
419
|
+
* await g.collection("films").create({ title, poster: ref })
|
|
420
|
+
* // later, to render:
|
|
421
|
+
* const { url } = await g.files.link(record.poster)
|
|
422
|
+
*
|
|
423
|
+
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
424
|
+
* bug this replaced.
|
|
425
|
+
*/
|
|
320
426
|
upload(file: Blob | File, options?: {
|
|
321
427
|
name?: string;
|
|
322
428
|
}): Promise<{
|
|
323
429
|
id: string;
|
|
324
|
-
|
|
430
|
+
ref: FileRef;
|
|
325
431
|
contentType: string;
|
|
326
432
|
sizeBytes: number;
|
|
327
433
|
}>;
|
package/dist/index.d.ts
CHANGED
|
@@ -143,13 +143,23 @@ export declare class Gemmein {
|
|
|
143
143
|
readonly storage: StorageClient;
|
|
144
144
|
readonly subscriptions: SubscriptionsClient;
|
|
145
145
|
readonly payments: PaymentsClient;
|
|
146
|
+
readonly purchases: PurchasesClient;
|
|
146
147
|
readonly account: AccountClient;
|
|
148
|
+
readonly files: FilesClient;
|
|
147
149
|
constructor(options: GemmeinOptions);
|
|
148
150
|
/**
|
|
149
151
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
150
152
|
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
153
|
+
*
|
|
154
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
155
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
156
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
157
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
158
|
+
* collection exists yet. The cloud ignores it.
|
|
151
159
|
*/
|
|
152
|
-
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string
|
|
160
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
161
|
+
intent?: string;
|
|
162
|
+
}): CollectionClient<T>;
|
|
153
163
|
}
|
|
154
164
|
export declare function gemmein(appKeyOrOptions: string | GemmeinOptions, options?: Omit<GemmeinOptions, "appKey">): Gemmein;
|
|
155
165
|
export declare function gemmeinServer(secretKeyOrOptions: string | GemmeinServerOptions, options?: Omit<GemmeinServerOptions, "secretKey">): GemmeinServer;
|
|
@@ -177,6 +187,81 @@ export declare class AuthClient {
|
|
|
177
187
|
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
178
188
|
* there is no client write path to plan or status, by design.
|
|
179
189
|
*/
|
|
190
|
+
/**
|
|
191
|
+
* The signed-in customer's own purchase history, read from Gemmein's
|
|
192
|
+
* immutable commercial record rather than from your data.
|
|
193
|
+
*
|
|
194
|
+
* This exists so a buyer keeps proof of what they paid for even when you keep
|
|
195
|
+
* no receipts collection at all — and it stays true if you later rename or
|
|
196
|
+
* delete one, because financial truth does not live in application data.
|
|
197
|
+
*
|
|
198
|
+
* Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
|
|
199
|
+
* as the payment provider reported them. Formatting is yours; rounding here
|
|
200
|
+
* would quietly lose money.
|
|
201
|
+
*/
|
|
202
|
+
export declare class PurchasesClient {
|
|
203
|
+
private readonly config;
|
|
204
|
+
constructor(config: ClientConfig);
|
|
205
|
+
/**
|
|
206
|
+
* Everything this customer has paid for, newest first, with refunds already
|
|
207
|
+
* applied — `status` is "paid", "part_refunded" or "refunded", and
|
|
208
|
+
* `refundedMinor` is how much has come back. Throws GemmeinError (401) when
|
|
209
|
+
* nobody is signed in.
|
|
210
|
+
*/
|
|
211
|
+
mine(): Promise<Array<{
|
|
212
|
+
item: string;
|
|
213
|
+
kind: "purchase" | "subscription";
|
|
214
|
+
amountMinor: number | null;
|
|
215
|
+
currency: string | null;
|
|
216
|
+
refundedMinor: number;
|
|
217
|
+
status: "paid" | "part_refunded" | "refunded";
|
|
218
|
+
grants: string[];
|
|
219
|
+
paidAt: string;
|
|
220
|
+
}>>;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A file reference — `file:01K…`. What `upload()` gives you and what your
|
|
224
|
+
* record should store.
|
|
225
|
+
*
|
|
226
|
+
* Branded so it cannot be mistaken for a URL: `<img src={record.poster}>` is a
|
|
227
|
+
* type error, which is the cheapest possible moment to learn that a reference
|
|
228
|
+
* is a name and not a location.
|
|
229
|
+
*/
|
|
230
|
+
export type FileRef = string & {
|
|
231
|
+
readonly __gemmeinFileRef: unique symbol;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Turn a stored file reference into a URL you can actually use.
|
|
235
|
+
*
|
|
236
|
+
* const { url } = await g.files.link(record.poster)
|
|
237
|
+
* img.src = url
|
|
238
|
+
*
|
|
239
|
+
* One call for every file, whichever collection it lives in. If the collection
|
|
240
|
+
* is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
|
|
241
|
+
* get one that works for a couple of minutes and is re-checked against who you
|
|
242
|
+
* are, what the collection's rule says, whether the file is yours, and whether
|
|
243
|
+
* you still hold whatever the collection requires.
|
|
244
|
+
*
|
|
245
|
+
* That is why the same app code keeps working when a collection later changes
|
|
246
|
+
* from public to private — and why a refund actually takes a download away.
|
|
247
|
+
*
|
|
248
|
+
* Don't store what this returns. Store the reference and call this again.
|
|
249
|
+
*/
|
|
250
|
+
export declare class FilesClient {
|
|
251
|
+
private readonly config;
|
|
252
|
+
constructor(config: ClientConfig);
|
|
253
|
+
link(ref: FileRef | string, options?: {
|
|
254
|
+
intent?: "inline" | "download";
|
|
255
|
+
}): Promise<{
|
|
256
|
+
ref: FileRef;
|
|
257
|
+
url: string;
|
|
258
|
+
/** Absent when the collection is public — those links don't expire. */
|
|
259
|
+
expiresAt?: string;
|
|
260
|
+
contentType: string;
|
|
261
|
+
sizeBytes?: number;
|
|
262
|
+
name?: string;
|
|
263
|
+
}>;
|
|
264
|
+
}
|
|
180
265
|
export declare class SubscriptionsClient {
|
|
181
266
|
private readonly config;
|
|
182
267
|
constructor(config: ClientConfig);
|
|
@@ -245,7 +330,9 @@ export declare class StorageClient {
|
|
|
245
330
|
private readonly config;
|
|
246
331
|
constructor(config: ClientConfig);
|
|
247
332
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
248
|
-
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string
|
|
333
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: {
|
|
334
|
+
intent?: string;
|
|
335
|
+
}): CollectionClient<T>;
|
|
249
336
|
}
|
|
250
337
|
/**
|
|
251
338
|
* Talks to one collection. Collections themselves are created by the app
|
|
@@ -256,7 +343,10 @@ export declare class StorageClient {
|
|
|
256
343
|
export declare class CollectionClient<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
257
344
|
private readonly config;
|
|
258
345
|
private readonly name;
|
|
259
|
-
|
|
346
|
+
private readonly intent?;
|
|
347
|
+
constructor(config: ClientConfig, name: string, options?: {
|
|
348
|
+
intent?: string;
|
|
349
|
+
});
|
|
260
350
|
/**
|
|
261
351
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
262
352
|
*
|
|
@@ -317,11 +407,27 @@ export declare class CollectionClient<T extends Record<string, unknown> = Record
|
|
|
317
407
|
published?: boolean;
|
|
318
408
|
}): Promise<GemmeinRecord<T>>;
|
|
319
409
|
delete(id: string): Promise<void>;
|
|
410
|
+
/**
|
|
411
|
+
* Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
|
|
412
|
+
*
|
|
413
|
+
* Store the reference. It never expires, it is safe to log and export, and
|
|
414
|
+
* it grants nothing on its own. To show or download the file, call
|
|
415
|
+
* `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
|
|
416
|
+
* what makes revoking access actually take a download away.
|
|
417
|
+
*
|
|
418
|
+
* const { ref } = await g.collection("films").upload(file)
|
|
419
|
+
* await g.collection("films").create({ title, poster: ref })
|
|
420
|
+
* // later, to render:
|
|
421
|
+
* const { url } = await g.files.link(record.poster)
|
|
422
|
+
*
|
|
423
|
+
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
424
|
+
* bug this replaced.
|
|
425
|
+
*/
|
|
320
426
|
upload(file: Blob | File, options?: {
|
|
321
427
|
name?: string;
|
|
322
428
|
}): Promise<{
|
|
323
429
|
id: string;
|
|
324
|
-
|
|
430
|
+
ref: FileRef;
|
|
325
431
|
contentType: string;
|
|
326
432
|
sizeBytes: number;
|
|
327
433
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -99,14 +99,22 @@ export class Gemmein {
|
|
|
99
99
|
this.storage = new StorageClient(config);
|
|
100
100
|
this.subscriptions = new SubscriptionsClient(config);
|
|
101
101
|
this.payments = new PaymentsClient(config);
|
|
102
|
+
this.purchases = new PurchasesClient(config);
|
|
102
103
|
this.account = new AccountClient(config);
|
|
104
|
+
this.files = new FilesClient(config);
|
|
103
105
|
}
|
|
104
106
|
/**
|
|
105
107
|
* Your app's data — `g.collection<{ title: string }>("notes")`. The
|
|
106
108
|
* canonical spelling; `g.storage.collection(name)` is the same client.
|
|
109
|
+
*
|
|
110
|
+
* `intent` (one sentence: what this collection is for and who should
|
|
111
|
+
* access it) travels with every call. Against a LOCAL gemmein dev
|
|
112
|
+
* runtime, an undeclared collection then reaches the human with your
|
|
113
|
+
* suggestion attached — always pass it when you aren't certain the
|
|
114
|
+
* collection exists yet. The cloud ignores it.
|
|
107
115
|
*/
|
|
108
|
-
collection(name) {
|
|
109
|
-
return this.storage.collection(name);
|
|
116
|
+
collection(name, options = {}) {
|
|
117
|
+
return this.storage.collection(name, options);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
120
|
// Factory forms — what the copied prompts teach. `gemmein("pk_...")` reads
|
|
@@ -184,6 +192,59 @@ export class AuthClient {
|
|
|
184
192
|
* dashboard. The client surface is deliberately read-plus-checkout only:
|
|
185
193
|
* there is no client write path to plan or status, by design.
|
|
186
194
|
*/
|
|
195
|
+
/**
|
|
196
|
+
* The signed-in customer's own purchase history, read from Gemmein's
|
|
197
|
+
* immutable commercial record rather than from your data.
|
|
198
|
+
*
|
|
199
|
+
* This exists so a buyer keeps proof of what they paid for even when you keep
|
|
200
|
+
* no receipts collection at all — and it stays true if you later rename or
|
|
201
|
+
* delete one, because financial truth does not live in application data.
|
|
202
|
+
*
|
|
203
|
+
* Amounts are MINOR UNITS (pence, cents) with the currency alongside, exactly
|
|
204
|
+
* as the payment provider reported them. Formatting is yours; rounding here
|
|
205
|
+
* would quietly lose money.
|
|
206
|
+
*/
|
|
207
|
+
export class PurchasesClient {
|
|
208
|
+
constructor(config) {
|
|
209
|
+
this.config = config;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Everything this customer has paid for, newest first, with refunds already
|
|
213
|
+
* applied — `status` is "paid", "part_refunded" or "refunded", and
|
|
214
|
+
* `refundedMinor` is how much has come back. Throws GemmeinError (401) when
|
|
215
|
+
* nobody is signed in.
|
|
216
|
+
*/
|
|
217
|
+
async mine() {
|
|
218
|
+
const result = (await runtimeRequest(this.config, "/auth/purchases"));
|
|
219
|
+
return result.purchases;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Turn a stored file reference into a URL you can actually use.
|
|
224
|
+
*
|
|
225
|
+
* const { url } = await g.files.link(record.poster)
|
|
226
|
+
* img.src = url
|
|
227
|
+
*
|
|
228
|
+
* One call for every file, whichever collection it lives in. If the collection
|
|
229
|
+
* is one anyone can read, you get a permanent, cacheable URL. If it isn't, you
|
|
230
|
+
* get one that works for a couple of minutes and is re-checked against who you
|
|
231
|
+
* are, what the collection's rule says, whether the file is yours, and whether
|
|
232
|
+
* you still hold whatever the collection requires.
|
|
233
|
+
*
|
|
234
|
+
* That is why the same app code keeps working when a collection later changes
|
|
235
|
+
* from public to private — and why a refund actually takes a download away.
|
|
236
|
+
*
|
|
237
|
+
* Don't store what this returns. Store the reference and call this again.
|
|
238
|
+
*/
|
|
239
|
+
export class FilesClient {
|
|
240
|
+
constructor(config) {
|
|
241
|
+
this.config = config;
|
|
242
|
+
}
|
|
243
|
+
async link(ref, options = {}) {
|
|
244
|
+
const query = options.intent === "download" ? "?intent=download" : "";
|
|
245
|
+
return (await runtimeRequest(this.config, `/files/${encodeURIComponent(String(ref))}/link${query}`));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
187
248
|
export class SubscriptionsClient {
|
|
188
249
|
constructor(config) {
|
|
189
250
|
this.config = config;
|
|
@@ -273,9 +334,9 @@ export class StorageClient {
|
|
|
273
334
|
this.config = config;
|
|
274
335
|
}
|
|
275
336
|
/** Optionally type your fields: `g.storage.collection<{ title: string }>("notes")`. */
|
|
276
|
-
collection(name) {
|
|
337
|
+
collection(name, options = {}) {
|
|
277
338
|
assertCollectionName(name);
|
|
278
|
-
return new CollectionClient(this.config, name);
|
|
339
|
+
return new CollectionClient(this.config, name, options);
|
|
279
340
|
}
|
|
280
341
|
}
|
|
281
342
|
/**
|
|
@@ -285,9 +346,10 @@ export class StorageClient {
|
|
|
285
346
|
* it there, don't retry.
|
|
286
347
|
*/
|
|
287
348
|
export class CollectionClient {
|
|
288
|
-
constructor(config, name) {
|
|
349
|
+
constructor(config, name, options = {}) {
|
|
289
350
|
this.config = config;
|
|
290
351
|
this.name = name;
|
|
352
|
+
this.intent = options.intent;
|
|
291
353
|
}
|
|
292
354
|
/**
|
|
293
355
|
* Create a record from your fields. The signed-in user becomes its owner.
|
|
@@ -387,6 +449,22 @@ export class CollectionClient {
|
|
|
387
449
|
async delete(id) {
|
|
388
450
|
await this.request(`/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
389
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Upload a file and get back a REFERENCE — `file:01K…` — not a URL.
|
|
454
|
+
*
|
|
455
|
+
* Store the reference. It never expires, it is safe to log and export, and
|
|
456
|
+
* it grants nothing on its own. To show or download the file, call
|
|
457
|
+
* `g.files.link(ref)`; Gemmein re-checks who is asking every time, which is
|
|
458
|
+
* what makes revoking access actually take a download away.
|
|
459
|
+
*
|
|
460
|
+
* const { ref } = await g.collection("films").upload(file)
|
|
461
|
+
* await g.collection("films").create({ title, poster: ref })
|
|
462
|
+
* // later, to render:
|
|
463
|
+
* const { url } = await g.files.link(record.poster)
|
|
464
|
+
*
|
|
465
|
+
* There is deliberately no `url` here. A URL that outlives a refund is the
|
|
466
|
+
* bug this replaced.
|
|
467
|
+
*/
|
|
390
468
|
async upload(file, options) {
|
|
391
469
|
const name = options?.name ?? (file instanceof File ? file.name : "upload");
|
|
392
470
|
// Step 1: Get presigned upload URL
|
|
@@ -419,6 +497,10 @@ export class CollectionClient {
|
|
|
419
497
|
...init,
|
|
420
498
|
headers: await runtimeHeaders(this.config, {
|
|
421
499
|
"content-type": "application/json",
|
|
500
|
+
// The intent rides every call so an undeclared collection reaches
|
|
501
|
+
// the human WITH the AI's suggestion attached (local runtime only;
|
|
502
|
+
// the cloud ignores it).
|
|
503
|
+
...(this.intent ? { "x-collection-intent": this.intent.slice(0, 200) } : {}),
|
|
422
504
|
...init.headers
|
|
423
505
|
})
|
|
424
506
|
});
|
package/llms.txt
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
> structured data storage with plain-English safety rules, and built-in Stripe
|
|
5
5
|
> subscription handling. Security is on by default for every app — tenant
|
|
6
6
|
> isolation, rate limiting, audit logs, domain-locked keys — with nothing to
|
|
7
|
-
> configure. Free while building;
|
|
8
|
-
> 1,000
|
|
7
|
+
> configure. Free while building; going live starts the Live state at
|
|
8
|
+
> $50/mo (up to 1,000 people); Growing unlocks 10,000 at $150/mo.
|
|
9
9
|
|
|
10
10
|
## What it is
|
|
11
11
|
|
|
@@ -30,9 +30,13 @@
|
|
|
30
30
|
- Data: records live in collections. Collections are created by YOUR HUMAN in
|
|
31
31
|
their dashboard (app.gemmein.com → data → "+ New collection"), never by you
|
|
32
32
|
or the SDK. Best practice: at planning time, list the collections your app
|
|
33
|
-
will need and ask your human up front
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
will need and ask your human up front, and pass your intent whenever a
|
|
34
|
+
collection might not exist yet —
|
|
35
|
+
`g.collection("bookings", { intent: "students reserve slots; each sees only their own" })`
|
|
36
|
+
— so the missing-collection conversation reaches your human with your
|
|
37
|
+
suggestion attached (local dev runtimes show it; the cloud ignores it).
|
|
38
|
+
If one is missing at runtime (404 `unknown_collection`), stop and ask
|
|
39
|
+
them to create it, telling them the name and which safety rule to pick. Each collection has exactly one
|
|
36
40
|
safety rule:
|
|
37
41
|
- `private` — each signed-in user sees and edits only their own records
|
|
38
42
|
(right for notes, tasks, anything personal).
|
|
@@ -108,22 +112,33 @@
|
|
|
108
112
|
- Images & files: NEVER base64 into record data and NEVER wire up your own
|
|
109
113
|
storage bucket — uploads are built in:
|
|
110
114
|
`const file = await g.collection("posts").upload(blob, { name })`
|
|
111
|
-
→ `{ id,
|
|
112
|
-
field like any text
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
→ `{ id, ref, contentType, sizeBytes }`. Store `file.ref` (`"file:01K…"`)
|
|
116
|
+
in a record field like any text — that's how a record "has" an image.
|
|
117
|
+
Store the REFERENCE, never a URL: a reference never expires and grants
|
|
118
|
+
nothing on its own.
|
|
119
|
+
To show or download it: `const { url } = await g.files.link(record.photo)`
|
|
120
|
+
— one call for every file. Pass `{ intent: "download" }` for a download
|
|
121
|
+
rather than a preview. Files in a collection anyone can read get a
|
|
122
|
+
permanent link; every other file gets one that expires in a couple of
|
|
123
|
+
minutes, so call `link()` when you render, don't store what it returns.
|
|
124
|
+
Upload permission follows the collection's WRITE rule; images only
|
|
125
|
+
(JPEG/PNG/WebP/GIF/HEIC). Oversized files are refused loudly (413
|
|
126
|
+
file_too_large — the message says the cap). The server checks the actual
|
|
127
|
+
bytes at confirm — a 400 invalid_file_content means the file isn't really
|
|
128
|
+
the image type it claimed (usually a renamed file); send the real image,
|
|
129
|
+
don't retry. A 403 from `link()` means the customer isn't allowed this
|
|
130
|
+
file right now — signed out, not theirs, or an entitlement they no longer
|
|
131
|
+
hold (`entitlement_required` names the key).
|
|
132
|
+
Honest bound: revoking access stops NEW links immediately; a link already
|
|
133
|
+
issued works until it expires. Gemmein controls delivery, it can't take
|
|
134
|
+
back a file someone already downloaded.
|
|
121
135
|
- Shapes: collections your users write (community, shared, direct) have a
|
|
122
136
|
SHAPE — the set of fields allowed. While your human builds, the server
|
|
123
137
|
learns it from your writes automatically (nothing to do); when they go
|
|
124
138
|
live it locks. A 400 invalid_shape means the field isn't in the locked
|
|
125
|
-
shape
|
|
126
|
-
|
|
139
|
+
shape. A live shape is sealed and cannot take new fields — stop, tell
|
|
140
|
+
your human which field you needed, and send only the fields the shape
|
|
141
|
+
already has. Never rename fields to dodge it.
|
|
127
142
|
- Contention (bookings, slugs, stock, shared edits): when two users can race
|
|
128
143
|
for the same thing, a permission model can't save you — preconditions do,
|
|
129
144
|
and they're just arguments on calls you already make. A 409 `conflict` from
|
|
@@ -184,7 +199,7 @@
|
|
|
184
199
|
back — redirects can be faked, receipts come from Stripe's signed
|
|
185
200
|
webhook. Receipts carry
|
|
186
201
|
{ product, item?, status: "paid"|"refunded", amountTotal (minor units,
|
|
187
|
-
as Stripe said), currency, paidAt, deliveryUrl
|
|
202
|
+
as Stripe said), currency, paidAt, deliveryUrl?, paymentRef } — deliveryUrl appears
|
|
188
203
|
when the builder attached a delivery link to the product (that's how
|
|
189
204
|
digital goods deliver themselves; never put a secret download URL in a
|
|
190
205
|
public collection). Fulfilment status changes ("shipped") are the owner
|
|
@@ -287,14 +302,22 @@ enforces — never because these checks are the enforcement. A ready-to-edit
|
|
|
287
302
|
`reaffirm.mjs` ships inside this npm package (next to this file and
|
|
288
303
|
REFERENCE.md) — copy it out, name your collections, run it in CI.
|
|
289
304
|
|
|
290
|
-
## Pricing (current,
|
|
305
|
+
## Pricing (current, Pricing Model v1 — states, not plans)
|
|
291
306
|
|
|
292
|
-
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
307
|
+
- Development is free indefinitely — no card at signup, unlimited
|
|
308
|
+
collections, the full security model included.
|
|
309
|
+
- Going live starts the Live state: $50/mo, up to 1,000 people and 10 GB
|
|
310
|
+
of file storage (a person = a unique enabled end-user identity on the
|
|
311
|
+
live app).
|
|
312
|
+
- Growing: $150/mo, up to 10,000 people and 50 GB — unlocked from the
|
|
313
|
+
dashboard when the product outgrows Live; down as easily as up.
|
|
314
|
+
- Beyond 10,000 people: talk to Gemmein (hello@gemmein.com) — scale is
|
|
315
|
+
priced as a relationship, not a checkout.
|
|
316
|
+
- Pricing reflects responsibility, not complexity: almost nothing is
|
|
317
|
+
metered. Safe limits exist purely as safety rails against runaway
|
|
318
|
+
scripts, are never billed, and real users never notice them. Hitting a
|
|
319
|
+
limit never breaks the app outright — there is grace, and the owner is
|
|
320
|
+
told.
|
|
298
321
|
|
|
299
322
|
## Facts for citation
|
|
300
323
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gemmein/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Gemmein SDK \u2014 passwordless auth, safe storage, and Stripe-driven record flips for AI-built apps. Small enough that one prompt teaches the whole API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|