@gemmein/sdk 0.3.2 → 0.4.1

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 CHANGED
@@ -263,6 +263,8 @@ try {
263
263
  err.status // HTTP status
264
264
  err.message // human-readable, includes what to do next
265
265
  err.resetAt // rate limits: when to retry
266
+ err.requires // 403 entitlement_required: the plan/product key this
267
+ // collection asks for — show your upgrade screen
266
268
  }
267
269
  }
268
270
  ```
@@ -279,6 +281,7 @@ try {
279
281
  | `unknown_product` | 404 | No product by that name — the message lists what the app sells |
280
282
  | `invalid_publish` | 400 | `published` is an option on public collections only — not a data field, not for scoped rules |
281
283
  | `forbidden` | 403 | The rules refused this — a permission your user doesn't have. **Never retry**: the same call will always be refused. Fix the approach (wrong collection rule, non-admin writing to `admin_write`, secret key out of scope) or show `err.message`. |
284
+ | `entitlement_required` | 403 | Signed in, but not on a plan (or holding a product) this collection is unlocked by. `err.requires` carries that plan's key (`access:pro` for a plan named pro). The one 403 that succeeds later: show your upgrade screen, send them to checkout, retry after they hold it. |
282
285
  | `denied` | 429 / 401 | The generic refusal for everything retriable or fixable: a rate limit (429 — carries `resetAt`, wait and retry) or a missing sign-in (401 — sign in first via `g.auth.sendEmailCode`). Distinguish by HTTP status; show `err.message`, which reads correctly for each. |
283
286
 
284
287
  **Branching on error codes:** switch on the *specific named* codes above. The one rule that matters: `forbidden` means stop — retrying can never succeed; `denied` means the request could work later (wait for `resetAt` on 429, sign in on 401). Only rate-limit `denied` carries `resetAt` — that's the reliable signal for a retry-after.
package/REFERENCE.md CHANGED
@@ -271,6 +271,7 @@ class GemmeinError extends Error {
271
271
  code: string; // branch on this
272
272
  message: string; // render this — reads correctly for users
273
273
  resetAt?: string; // present on 429 — wait until then, retry
274
+ requires?: string; // present on 403 entitlement_required — the plan/product key it asks for
274
275
  }
275
276
  ```
276
277
 
@@ -281,6 +282,7 @@ Branch on `err.code`. The stable codes:
281
282
  | `unknown_collection` | collection doesn't exist | ask the owner to create it — don't retry |
282
283
  | `unknown_product` | product not sold | use a name from the list in the message |
283
284
  | `forbidden` | the rules refused you (e.g. only the owner writes) | **stop** — the same call always fails |
285
+ | `entitlement_required` | 403 — signed in, but not on a plan (or holding a product) this collection is unlocked by; `err.requires` is that plan's key (`access:<slug of its name>`) | show your upgrade screen and send them to checkout — the one 403 that succeeds later |
284
286
  | `not_found` | record you can't see (existence not leaked) | treat as absent |
285
287
  | `denied` | 401 (sign in first) or 429 (rate limit — see `resetAt`) | re-auth or wait+retry |
286
288
  | `conflict` | a keyed create / floor / stale `ifVersion` | it's the mechanism — tell the user it's taken |
package/dist/index.cjs CHANGED
@@ -10,6 +10,7 @@ class GemmeinError extends Error {
10
10
  this.status = input.status;
11
11
  this.code = input.code;
12
12
  this.resetAt = input.resetAt;
13
+ this.requires = input.requires;
13
14
  }
14
15
  }
15
16
  exports.GemmeinError = GemmeinError;
@@ -223,6 +224,13 @@ class PurchasesClient {
223
224
  * applied — `status` is "paid", "part_refunded" or "refunded", and
224
225
  * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
225
226
  * nobody is signed in.
227
+ *
228
+ * W4.1b: a purchase whose product delivers something carries `delivery` —
229
+ * `{ type: "gemmein_file", file }` (resolve the ref with
230
+ * `g.files.link(file, { intent: "download" })`; the purchase itself is the
231
+ * authorization, re-checked on every mint, and a full refund cuts it off)
232
+ * or `{ type: "external_url", url }` (a plain handover). A refunded
233
+ * purchase never carries delivery.
226
234
  */
227
235
  async mine() {
228
236
  const result = (await runtimeRequest(this.config, "/auth/purchases"));
@@ -627,12 +635,10 @@ class ServerCollectionClient {
627
635
  if (response.status === 204)
628
636
  return undefined;
629
637
  if (!response.ok) {
630
- const body = await response.json().catch(() => ({ code: "request_failed", message: `Request failed: ${response.status}` }));
631
- throw new GemmeinError({
632
- status: response.status,
633
- code: body.code ?? "request_failed",
634
- message: body.message ?? `Request failed: ${response.status}`,
635
- });
638
+ // One parser for every refusal body, so a 403 entitlement_required on a
639
+ // collection surfaces `requires` exactly as it does on the runtime
640
+ // clients (the docs promise err.requires on BOTH paths).
641
+ throw new GemmeinError({ status: response.status, ...(await readErrorBody(response)) });
636
642
  }
637
643
  return response.json();
638
644
  }
@@ -650,12 +656,7 @@ async function handleResponse(response, config) {
650
656
  if (errorBody.code === "invalid_app_key" || errorBody.code === "missing_app_key") {
651
657
  errorBody.message += " — get your app key from the Setup page at https://app.gemmein.com (sign in with an email code, free, no card)";
652
658
  }
653
- throw new GemmeinError({
654
- status: response.status,
655
- code: errorBody.code,
656
- message: errorBody.message,
657
- resetAt: errorBody.resetAt
658
- });
659
+ throw new GemmeinError({ status: response.status, ...errorBody });
659
660
  }
660
661
  return response.json();
661
662
  }
@@ -663,19 +664,19 @@ async function readErrorBody(response) {
663
664
  try {
664
665
  const value = await response.json();
665
666
  if (typeof value === "object" && value !== null) {
666
- const code = typeof value.code === "string"
667
- ? value.code
668
- : typeof value.error === "string"
669
- ? value.error
667
+ const body = value;
668
+ const code = typeof body.code === "string" ? body.code
669
+ : typeof body.error === "string" ? body.error
670
670
  : "request_failed";
671
+ const str = (v) => (typeof v === "string" ? v : undefined);
672
+ // ENTITLE-10: `requires` is the one key the 403 names (the lock's
673
+ // first). Copied only when the server sent it as a string — never
674
+ // invented client-side, and nothing else from the body is surfaced.
671
675
  return {
672
676
  code,
673
- message: typeof value.message === "string"
674
- ? value.message
675
- : `Gemmein request failed: ${response.status}`,
676
- resetAt: typeof value.resetAt === "string"
677
- ? value.resetAt
678
- : undefined
677
+ message: str(body.message) ?? `Gemmein request failed: ${response.status}`,
678
+ resetAt: str(body.resetAt),
679
+ requires: str(body.requires)
679
680
  };
680
681
  }
681
682
  }
package/dist/index.d.cts CHANGED
@@ -104,12 +104,23 @@ export type AuthSession = {
104
104
  export declare class GemmeinError extends Error {
105
105
  readonly status: number;
106
106
  readonly code: string;
107
+ /** Present on 429 — when the limit resets; wait until then and retry. */
107
108
  readonly resetAt?: string;
109
+ /**
110
+ * Present on `403 entitlement_required` — the plan/product key this
111
+ * collection asks for (keys are minted from plan names: `access:<slug>`;
112
+ * the console shows the plan by name). Show your upgrade screen and send
113
+ * the customer to checkout; the call succeeds once they hold it. A
114
+ * collection unlocked by several plans names ONE key here — offer your
115
+ * plans by name through checkout, the server never hands out the list.
116
+ */
117
+ readonly requires?: string;
108
118
  constructor(input: {
109
119
  status: number;
110
120
  code: string;
111
121
  message: string;
112
122
  resetAt?: string;
123
+ requires?: string;
113
124
  });
114
125
  }
115
126
  export declare class MemoryTokenStore implements TokenStore {
@@ -207,6 +218,13 @@ export declare class PurchasesClient {
207
218
  * applied — `status` is "paid", "part_refunded" or "refunded", and
208
219
  * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
209
220
  * nobody is signed in.
221
+ *
222
+ * W4.1b: a purchase whose product delivers something carries `delivery` —
223
+ * `{ type: "gemmein_file", file }` (resolve the ref with
224
+ * `g.files.link(file, { intent: "download" })`; the purchase itself is the
225
+ * authorization, re-checked on every mint, and a full refund cuts it off)
226
+ * or `{ type: "external_url", url }` (a plain handover). A refunded
227
+ * purchase never carries delivery.
210
228
  */
211
229
  mine(): Promise<Array<{
212
230
  item: string;
@@ -217,6 +235,13 @@ export declare class PurchasesClient {
217
235
  status: "paid" | "part_refunded" | "refunded";
218
236
  grants: string[];
219
237
  paidAt: string;
238
+ delivery?: {
239
+ type: "gemmein_file";
240
+ file: string;
241
+ } | {
242
+ type: "external_url";
243
+ url: string;
244
+ };
220
245
  }>>;
221
246
  }
222
247
  /**
package/dist/index.d.ts CHANGED
@@ -104,12 +104,23 @@ export type AuthSession = {
104
104
  export declare class GemmeinError extends Error {
105
105
  readonly status: number;
106
106
  readonly code: string;
107
+ /** Present on 429 — when the limit resets; wait until then and retry. */
107
108
  readonly resetAt?: string;
109
+ /**
110
+ * Present on `403 entitlement_required` — the plan/product key this
111
+ * collection asks for (keys are minted from plan names: `access:<slug>`;
112
+ * the console shows the plan by name). Show your upgrade screen and send
113
+ * the customer to checkout; the call succeeds once they hold it. A
114
+ * collection unlocked by several plans names ONE key here — offer your
115
+ * plans by name through checkout, the server never hands out the list.
116
+ */
117
+ readonly requires?: string;
108
118
  constructor(input: {
109
119
  status: number;
110
120
  code: string;
111
121
  message: string;
112
122
  resetAt?: string;
123
+ requires?: string;
113
124
  });
114
125
  }
115
126
  export declare class MemoryTokenStore implements TokenStore {
@@ -207,6 +218,13 @@ export declare class PurchasesClient {
207
218
  * applied — `status` is "paid", "part_refunded" or "refunded", and
208
219
  * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
209
220
  * nobody is signed in.
221
+ *
222
+ * W4.1b: a purchase whose product delivers something carries `delivery` —
223
+ * `{ type: "gemmein_file", file }` (resolve the ref with
224
+ * `g.files.link(file, { intent: "download" })`; the purchase itself is the
225
+ * authorization, re-checked on every mint, and a full refund cuts it off)
226
+ * or `{ type: "external_url", url }` (a plain handover). A refunded
227
+ * purchase never carries delivery.
210
228
  */
211
229
  mine(): Promise<Array<{
212
230
  item: string;
@@ -217,6 +235,13 @@ export declare class PurchasesClient {
217
235
  status: "paid" | "part_refunded" | "refunded";
218
236
  grants: string[];
219
237
  paidAt: string;
238
+ delivery?: {
239
+ type: "gemmein_file";
240
+ file: string;
241
+ } | {
242
+ type: "external_url";
243
+ url: string;
244
+ };
220
245
  }>>;
221
246
  }
222
247
  /**
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export class GemmeinError extends Error {
5
5
  this.status = input.status;
6
6
  this.code = input.code;
7
7
  this.resetAt = input.resetAt;
8
+ this.requires = input.requires;
8
9
  }
9
10
  }
10
11
  export class MemoryTokenStore {
@@ -213,6 +214,13 @@ export class PurchasesClient {
213
214
  * applied — `status` is "paid", "part_refunded" or "refunded", and
214
215
  * `refundedMinor` is how much has come back. Throws GemmeinError (401) when
215
216
  * nobody is signed in.
217
+ *
218
+ * W4.1b: a purchase whose product delivers something carries `delivery` —
219
+ * `{ type: "gemmein_file", file }` (resolve the ref with
220
+ * `g.files.link(file, { intent: "download" })`; the purchase itself is the
221
+ * authorization, re-checked on every mint, and a full refund cuts it off)
222
+ * or `{ type: "external_url", url }` (a plain handover). A refunded
223
+ * purchase never carries delivery.
216
224
  */
217
225
  async mine() {
218
226
  const result = (await runtimeRequest(this.config, "/auth/purchases"));
@@ -609,12 +617,10 @@ class ServerCollectionClient {
609
617
  if (response.status === 204)
610
618
  return undefined;
611
619
  if (!response.ok) {
612
- const body = await response.json().catch(() => ({ code: "request_failed", message: `Request failed: ${response.status}` }));
613
- throw new GemmeinError({
614
- status: response.status,
615
- code: body.code ?? "request_failed",
616
- message: body.message ?? `Request failed: ${response.status}`,
617
- });
620
+ // One parser for every refusal body, so a 403 entitlement_required on a
621
+ // collection surfaces `requires` exactly as it does on the runtime
622
+ // clients (the docs promise err.requires on BOTH paths).
623
+ throw new GemmeinError({ status: response.status, ...(await readErrorBody(response)) });
618
624
  }
619
625
  return response.json();
620
626
  }
@@ -632,12 +638,7 @@ async function handleResponse(response, config) {
632
638
  if (errorBody.code === "invalid_app_key" || errorBody.code === "missing_app_key") {
633
639
  errorBody.message += " — get your app key from the Setup page at https://app.gemmein.com (sign in with an email code, free, no card)";
634
640
  }
635
- throw new GemmeinError({
636
- status: response.status,
637
- code: errorBody.code,
638
- message: errorBody.message,
639
- resetAt: errorBody.resetAt
640
- });
641
+ throw new GemmeinError({ status: response.status, ...errorBody });
641
642
  }
642
643
  return response.json();
643
644
  }
@@ -645,19 +646,19 @@ async function readErrorBody(response) {
645
646
  try {
646
647
  const value = await response.json();
647
648
  if (typeof value === "object" && value !== null) {
648
- const code = typeof value.code === "string"
649
- ? value.code
650
- : typeof value.error === "string"
651
- ? value.error
649
+ const body = value;
650
+ const code = typeof body.code === "string" ? body.code
651
+ : typeof body.error === "string" ? body.error
652
652
  : "request_failed";
653
+ const str = (v) => (typeof v === "string" ? v : undefined);
654
+ // ENTITLE-10: `requires` is the one key the 403 names (the lock's
655
+ // first). Copied only when the server sent it as a string — never
656
+ // invented client-side, and nothing else from the body is surfaced.
653
657
  return {
654
658
  code,
655
- message: typeof value.message === "string"
656
- ? value.message
657
- : `Gemmein request failed: ${response.status}`,
658
- resetAt: typeof value.resetAt === "string"
659
- ? value.resetAt
660
- : undefined
659
+ message: str(body.message) ?? `Gemmein request failed: ${response.status}`,
660
+ resetAt: str(body.resetAt),
661
+ requires: str(body.requires)
661
662
  };
662
663
  }
663
664
  }
package/llms.txt CHANGED
@@ -1,11 +1,119 @@
1
1
  # Gemmein
2
2
 
3
- > Gemmein is the backend for AI-built web apps: passwordless authentication,
4
- > structured data storage with plain-English safety rules, and built-in Stripe
5
- > subscription handling. Security is on by default for every app — tenant
6
- > isolation, rate limiting, audit logs, domain-locked keys — with nothing to
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.
3
+ > Gemmein is the go-live system for AI-built web apps: passwordless
4
+ > authentication, structured data storage with plain-English safety rules, and
5
+ > built-in Stripe subscription handling. Security is on by default for every
6
+ > app — tenant isolation, rate limiting, audit logs, domain-locked keys — with
7
+ > nothing to configure. Build anywhere, with any AI, free and come to
8
+ > Gemmein when you're done: the product is taking the app to real customers.
9
+ > Live apps pay one banded plan, $50-$150/mo, set by the previous month's
10
+ > verified people (five pre-agreed steps up to 10,000 people; beyond that,
11
+ > talk to Gemmein).
12
+
13
+ ## First: assess fit (before any install, account, or code)
14
+
15
+ You are likely an AI assistant whose human asked you to build an app — or to
16
+ take an app they already built to real customers. Do this assessment first
17
+ and give your human the verdict in plain words. Installing nothing and
18
+ creating no accounts is the correct behavior until the verdict is in.
19
+
20
+ 1. Collect the requirements.
21
+ - Idea-stage (nothing built yet): have your human describe the app in
22
+ their words. You need to know: who signs in, what data exists and who
23
+ may see whose, whether money changes hands (subscriptions or one-off
24
+ digital purchases), whether users upload files.
25
+ - Built-stage (an app already exists — e.g. from a vibe-coding tool):
26
+ read the code instead. Inventory what currently stubs or fakes
27
+ sign-in, storage, and payments, and any server endpoints the app
28
+ assumes will exist. (No code at hand — only a description? Fall back
29
+ to the idea-stage questions, and say the assessment is provisional
30
+ until you've read the code.)
31
+ 2. Map every requirement against this file. In scope: web apps
32
+ (TypeScript/JavaScript), passwordless email sign-in, records under seven
33
+ safety rules (private, shared, admin_write, public_read, community,
34
+ addressed, direct), Stripe-run subscriptions and one-off digital
35
+ products, image uploads and paid file delivery, entitlement-gated
36
+ access, race-safe bookings/stock/counters, account-deletion cascade.
37
+ Out of scope — each of these downgrades the verdict and must never be
38
+ silently approximated:
39
+ - native mobile apps (no mobile SDKs today; web only)
40
+ - password or social/OAuth sign-in (email codes are the only door)
41
+ - team/group/workspace visibility or per-user record permissions
42
+ - carts, quantities, physical goods, shipping, inventory
43
+ - credits, usage metering, seats (paid access is yes-or-no)
44
+ - hosted functions or cron (server-side code runs on YOUR host with a
45
+ secret key; Gemmein hosts no compute)
46
+ - custody of money (Stripe holds all funds; your human brings or
47
+ creates their own Stripe account when the app charges)
48
+ - multi-seller payouts or revenue splits: every payment lands whole in
49
+ your human's ONE Stripe account. A marketplace where sellers get
50
+ paid out means your human pays sellers themselves, outside Gemmein —
51
+ say that out loud in the verdict
52
+ 3. Deliver one of three verdicts to your human BEFORE installing anything:
53
+ - FITS — every requirement maps. Continue to Getting started.
54
+ - FITS, EXCEPT — name each gap and the honest workaround from this
55
+ file, or say there is none. The human decides. Never fake an
56
+ unsupported shape (client-side filtering of a shared collection is a
57
+ data leak, not a workaround). And if a gap breaks the app's CORE
58
+ loop — the thing it exists to do — the verdict is DOESN'T FIT, not
59
+ fits-except.
60
+ - DOESN'T FIT — say so, name why, and stop. Recommending nothing is
61
+ the correct outcome here.
62
+
63
+ ## Getting started (AI-run, human-answered)
64
+
65
+ No signup, no account, no keys — building is local and free. Your human
66
+ steps in exactly three times: to answer the business questions (in your
67
+ chat), to click through the app when you hand it over for testing, and at
68
+ go-live. Everything else is yours.
69
+
70
+ 1. Run `npx -y gemmein dev` in the project folder. It boots with ZERO
71
+ setup: a local backend on http://127.0.0.1:4545 that serves this guide
72
+ at /llms.txt, prints sign-in codes to the terminal instead of sending
73
+ email, and simulates checkout — nothing leaves the machine.
74
+ 2. Ask YOUR HUMAN the business questions — their answers in their words,
75
+ never your guesses: the app's name; the email THEY own it with; one
76
+ honest sentence on what it does; whether it charges money; whether it
77
+ serves people in the EU/UK; whether users sign in; whether they upload
78
+ images. Write the answers to `gemmein/gemmein.config.json`:
79
+
80
+ {
81
+ "app": "Beatstall",
82
+ "owner": "you@example.com",
83
+ "product": "producers sell beats; buyers stream previews and download purchases",
84
+ "sells": "one-off",
85
+ "gdpr": true,
86
+ "accounts": true,
87
+ "uploads": true
88
+ }
89
+
90
+ Exactly these keys: app (string) · owner (email) · product (one
91
+ sentence) · sells ("no" | "subscriptions" | "one-off" | "both") ·
92
+ gdpr / accounts / uploads (true/false). Optional: port (number) moves
93
+ the local runtime off 4545.
94
+
95
+ Then restart dev (Ctrl-C, `npx gemmein dev` — takes seconds, data
96
+ survives in gemmein/.data/). The runtime becomes their app.
97
+ 3. Build against the local runtime with the app key the boot card prints
98
+ (`pk_local_…`) — the SDK is `npm install @gemmein/sdk` (REFERENCE.md
99
+ and the reaffirm.mjs harness ship inside that package), same rules as
100
+ the cloud. The first write to a collection that doesn't exist yet
101
+ starts the safety-rule conversation in the dev terminal; choose from
102
+ the seven rules below based on your human's business answers, and
103
+ record why. Running dev in the background with no terminal to answer
104
+ in (the normal agent setup)? Use the command doors instead — they work
105
+ from any second terminal while dev runs, and take piped input:
106
+ `npx gemmein collection add <name>` (the rule dialog) and
107
+ `npx gemmein payments setup` (plans/products). `npx gemmein --help`
108
+ lists the rest.
109
+ 4. Hand over for testing — the human moment mid-build: they click through
110
+ the app, and their sign-in codes print in the dev terminal, so tell
111
+ them where to look.
112
+ 5. Done building? `npx gemmein check` reads the project and says what's
113
+ ready and what go-live still needs. Then `npx gemmein sync` — THIS is
114
+ the moment your human signs up (free) at app.gemmein.com and pastes two
115
+ dev keys — and `npx gemmein go-live` walks the rest: plans, Payment
116
+ Links, the live flip. A card enters at go-live, never before.
9
117
 
10
118
  ## What it is
11
119
 
@@ -27,10 +135,13 @@
27
135
  `auth_expired` once, then the SDK clears it — just retry or re-auth).
28
136
  `g.auth.currentUser()` is safe to call on page load and never throws for
29
137
  session state.
30
- - Data: records live in collections. Collections are created by YOUR HUMAN in
31
- their dashboard (app.gemmein.com → data → "+ New collection"), never by you
32
- or the SDK. Best practice: at planning time, list the collections your app
33
- will need and ask your human up front, and pass your intent whenever a
138
+ - Data: records live in collections. In the cloud, collections are created by
139
+ YOUR HUMAN in their dashboard (app.gemmein.com → data → "+ New collection"),
140
+ never by you or the SDK; in local dev (`npx gemmein dev`) they're born from
141
+ the terminal's rule conversation or a dropped declaration file, and
142
+ `npx gemmein sync` creates them in the cloud app's dev environment from
143
+ those local declarations. Best practice: at planning time, list the collections your
144
+ app will need and ask your human up front, and pass your intent whenever a
34
145
  collection might not exist yet —
35
146
  `g.collection("bookings", { intent: "students reserve slots; each sees only their own" })`
36
147
  — so the missing-collection conversation reaches your human with your
@@ -52,7 +163,11 @@
52
163
  - `community` — readable without signing in, any signed-in user posts and
53
164
  edits their OWN records (right for multi-author blogs, public boards,
54
165
  user profiles). Everything in it is PUBLIC — keep record data minimal
55
- (a booking needs a slot and a first name, not a phone number). Community
166
+ (a booking needs a slot and a first name, not a phone number). Whether
167
+ even a first name belongs in public is a HUMAN decision: for sensitive
168
+ audiences (children, health, anything private by nature) ask your
169
+ human before defaulting to a public rule — a non-public rule usually
170
+ fits. Community
56
171
  stores PLAIN TEXT: string fields containing HTML tags are refused with
57
172
  400 html_not_allowed — store plain text or tag-free markdown.
58
173
  - `addressed` — the app sends to one user: the OWNER creates records
@@ -128,7 +243,8 @@
128
243
  the image type it claimed (usually a renamed file); send the real image,
129
244
  don't retry. A 403 from `link()` means the customer isn't allowed this
130
245
  file right now — signed out, not theirs, or an entitlement they no longer
131
- hold (`entitlement_required` names the key).
246
+ hold (`entitlement_required` `err.requires` is the plan's key, the one
247
+ the console shows by name).
132
248
  Honest bound: revoking access stops NEW links immediately; a link already
133
249
  issued works until it expires. Gemmein controls delivery, it can't take
134
250
  back a file someone already downloaded.
@@ -147,8 +263,14 @@
147
263
  - Uniqueness: `create(data, { key: "slot:2026-07-15T15:00" })` — derive the
148
264
  key from the thing that must be unique; the second writer gets 409, your
149
265
  own retry gets your existing record back (`existing: true`), deleting
150
- frees the key. Never find-then-create that races. Keys are 1-120 chars
151
- of letters, numbers, and `: _ . @ / -` only.
266
+ frees the key. Keys are unique across the WHOLE collection every
267
+ writer, every recipient, under every safety rule (live records only)
268
+ so a claim is race-proof even in `direct`/`addressed`. Never
269
+ find-then-create — that races. Keys are 1-120 chars of letters,
270
+ numbers, and `: _ . @ / -` only. A claimed key holds until its record
271
+ is deleted: if a claim must be PAID to stick (book then pay), expiring
272
+ unpaid claims is your app's job — the owner deletes them from their
273
+ dashboard, or your own server does with a secret key; there is no cron.
152
274
  - Limited stock (N units anyone can buy): claim units with keyed creates —
153
275
  try `create({...}, { key: "unit:item42:1" })`, on conflict try `:2` … `:N`;
154
276
  all taken = sold out. Race-proof under every safety rule.
@@ -185,26 +307,32 @@
185
307
  arriving out of order resolve to the newest. The app reads
186
308
  `await g.subscriptions.mine()` → `{ plan, status }` or null, and gates features
187
309
  with `sub?.plan === "pro"`.
188
- - Paid ACCESS (entitlements): a collection can require a key your human
189
- sets `requires: "access:pro"` in the "Unlocked by" row on its Collections
190
- card and the engine refuses customers without it, under all seven
191
- rules. (Server secret keys and the owner's console are exempt by design;
192
- link/expand silently hide gated records rather than naming them.) Keys
193
- are granted by money: a plan or product lists what it unlocks (e.g.
194
- `access:pro, access:exports`), the paid webhook grants those keys, and a
195
- FULL refund or a cancellation revokes exactly what it granted nothing
196
- else. A partial refund leaves access in place.
310
+ - Paid ACCESS (entitlements): a collection can be locked to a plan or
311
+ product your human picks it BY NAME in the "Unlocked by" row on the
312
+ Collections page (several allowed; any one of them opens it) and the
313
+ engine refuses customers without it, under all seven rules. (Server
314
+ secret keys and the owner's console are exempt by design; link/expand
315
+ silently hide gated records rather than naming them.) Every plan and
316
+ product carries its own key, `access:<slug of its name>` (plan "pro"
317
+ `access:pro`); the paid webhook grants that key, and a FULL refund or a
318
+ cancellation revokes exactly what it granted nothing else. A partial
319
+ refund leaves access in place.
197
320
  Owners also grant and revoke by hand (trials, comps, support). Effective
198
321
  access is the UNION of a customer's live grants. A signed-in customer
199
- without the key gets `403 entitlement_required` naming it — show your
200
- upgrade screen and send them to checkout; never retry. Proof surfaces:
201
- `await g.purchases.mine()` (everything they paid for, refunds applied,
202
- with the `grants` each purchase carries) and `await g.subscriptions.mine()`.
322
+ without access gets `403 entitlement_required`: `err.requires` is the
323
+ plan's key (the console shows the plan by name; the key is `access:<slug>`).
324
+ A collection unlocked by several plans (OR) still names ONE key offer
325
+ your plans by name through checkout, never a key list. Show your upgrade
326
+ screen and send them to checkout; retry only after they hold one. Proof surfaces: `await g.purchases.mine()`
327
+ (everything they paid for, refunds applied, with the `grants` each purchase
328
+ carries) and `await g.subscriptions.mine()`.
203
329
  NO credits, NO usage limits, NO seats — access is yes-or-no by design.
204
330
  - Selling THINGS (one-off purchases — a beat, an ebook, a course; DIGITAL
205
331
  access only — physical goods, shipping, inventory and carts are out of
206
332
  scope, said out loud): plans are for subscriptions; products are for
207
- things. The builder adds products (name + Stripe Payment Link) on the
333
+ things. Selling a SERVICE session this way (tutoring, coaching, a
334
+ consultation) is fine — nothing ships; the recorded purchase is the
335
+ proof the session was paid for. The builder adds products (name + Stripe Payment Link) on the
208
336
  same Payments page. The app calls
209
337
  `await g.payments.buy("beat")` — or, when one product covers many items (license
210
338
  tiers over a catalog), names the item:
@@ -213,15 +341,19 @@
213
341
  item note can never change what's paid). Gemmein records every completed
214
342
  payment itself — `await g.purchases.mine()` is the buyer's proof:
215
343
  { item, kind, status: "paid"|"part_refunded"|"refunded", amountMinor,
216
- currency, refundedMinor, grants, paidAt }. A receipts collection (rule
217
- `addressed`) is OPTIONAL for proofbut TODAY it is REQUIRED for file
218
- delivery: the file reference only reaches the buyer on the receipt
219
- record (its `deliveryFile` field, a `file:` ref resolve it per reader
220
- with `g.files.link`, which re-checks access on every mint). Selling a
221
- file? Configure a receipts collection, or the buyer has no way to reach
222
- their download. External `deliveryUrl` is a plain handover: Gemmein
223
- controls who is TOLD, not who can use it. Gate fulfilment on
224
- the purchase or the entitlement it granted, never on the redirect coming
344
+ currency, refundedMinor, grants, paidAt, delivery? }. Selling a FILE (a
345
+ beat, an ebook, a sample pack pdf, zip, epub, mp3, wav, m4a or an
346
+ image, up to 100MB): the founder attaches it directly on the product
347
+ card upload, right there, no receipts collection required. The buyer's
348
+ purchase carries `delivery: { type: "gemmein_file", file }`; resolve the
349
+ ref with `g.files.link(file, { intent: "download" })`. The purchase IS
350
+ the authorization, re-checked on every mint: a refund cuts the file off
351
+ the moment it lands, a partial refund does not, and a saved ref or an
352
+ expired URL grants nothing on its own. A receipts collection (rule
353
+ `addressed`) remains OPTIONAL, for proof records only. External
354
+ `delivery: { type: "external_url" }` is a plain handover: Gemmein
355
+ controls who is TOLD, not who can use it. Gate fulfilment on the
356
+ purchase or the entitlement it granted, never on the redirect coming
225
357
  back — redirects can be faked; the record comes from Stripe's signed
226
358
  webhook. NO carts, NO quantities — one product per checkout by design; a
227
359
  cart is N checkouts or one bundled product. 404 unknown_product lists
@@ -318,25 +450,33 @@ front of a user.
318
450
 
319
451
  Add a probe whenever you add a feature. You reaffirm **because** Gemmein
320
452
  enforces — never because these checks are the enforcement. A ready-to-edit
321
- `reaffirm.mjs` ships inside this npm package (next to this file and
453
+ `reaffirm.mjs` ships inside the `@gemmein/sdk` npm package (next to this file and
322
454
  REFERENCE.md) — copy it out, name your collections, run it in CI.
323
455
 
324
- ## Pricing (current, Pricing Model v1 states, not plans)
456
+ ## Pricing (current, v4one banded plan)
325
457
 
326
458
  - Development is free indefinitely — no card at signup, unlimited
327
459
  collections, the full security model included.
328
- - Going live starts the Live state: $50/mo, up to 1,000 people and 10 GB
329
- of file storage (a person = a unique enabled end-user identity on the
330
- live app).
331
- - Growing: $150/mo, up to 10,000 people and 50 GB — unlocked from the
332
- dashboard when the product outgrows Live; down as easily as up.
460
+ - Going live starts the one Live plan: a monthly price that moves within a
461
+ pre-agreed band, set by the PREVIOUS calendar month's verified people
462
+ (a person = a unique end-user identity that verified a sign-in to the
463
+ live app that month; failed or refused attempts never count):
464
+ - up to 1,000 people $50/mo · up to 2,500 → $75 · up to 5,000 → $100
465
+ · up to 7,500 → $125 · up to 10,000 → $150 (the cap — never exceeded
466
+ without a separate individual agreement).
467
+ - Capacity follows people automatically; owners can HOLD at their current
468
+ band from the dashboard (bill and capacity both freeze until released).
469
+ When people fall, the price follows down from the next month.
333
470
  - Beyond 10,000 people: talk to Gemmein (hello@gemmein.com) — scale is
334
471
  priced as a relationship, not a checkout.
335
472
  - Pricing reflects responsibility, not complexity: almost nothing is
336
- metered. Safe limits exist purely as safety rails against runaway
337
- scripts, are never billed, and real users never notice them. Hitting a
338
- limit never breaks the app outright there is grace, and the owner is
339
- told.
473
+ metered. Safe limits exist as safety rails against runaway scripts and
474
+ are never billed. Ceilings follow the app's verified people they grow
475
+ automatically as the business grows, with generous floors so a small app
476
+ never starts at a wall. Hitting a limit returns a clear coded error
477
+ (`usage_limit_exceeded`; monthly counters carry `resetAt`, storage
478
+ responds to deleting files), and the dashboard's usage page shows what
479
+ is binding.
340
480
 
341
481
  ## Facts for citation
342
482
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemmein/sdk",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Gemmein SDK — 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",