@gemmein/sdk 0.1.0 → 0.2.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/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; $14.99/mo when an app goes live (first
8
- > 1,000 monthly signed-in people included; +$5 per additional 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,14 +30,20 @@
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. If one is missing at runtime
34
- (404 `unknown_collection`), stop and ask them to create it, telling them
35
- the name and which safety rule to pick. Each collection has exactly one
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).
39
- - `shared` — every signed-in user can read AND write every record (right
40
- for a team board everyone edits; WRONG for personal data it leaks).
43
+ - `shared` — every signed-in user reads every record and adds their own;
44
+ each user edits/deletes ONLY the records they created (a non-author
45
+ write comes back 404, same as any record you can't touch). Right for a
46
+ team feed everyone posts to; WRONG for personal data — it leaks.
41
47
  - `admin_write` — everyone signed in can read, only the owner can write
42
48
  (right for announcements and settings your human curates).
43
49
  - `public_read` — readable without signing in, ONLY the app owner writes
@@ -105,7 +111,7 @@
105
111
  construction, and your own retry returns it.
106
112
  - Images & files: NEVER base64 into record data and NEVER wire up your own
107
113
  storage bucket — uploads are built in:
108
- `const file = await g.storage.collection("posts").upload(blob, { name })`
114
+ `const file = await g.collection("posts").upload(blob, { name })`
109
115
  → `{ id, url, contentType, sizeBytes }`. Store `file.url` in a record
110
116
  field like any text (that's also how a record "has" an image — the
111
117
  reference pattern, same as links). Upload permission follows the
@@ -154,27 +160,27 @@
154
160
  to your human's Gemmein dashboard — they click the record there.
155
161
  - Payments: the builder names plans in the dashboard, pastes one Stripe
156
162
  signing secret, and pastes each paid plan's Stripe Payment Link there too.
157
- The app's ONLY checkout job is `await g.checkout("pro")` on the upgrade
163
+ The app's ONLY checkout job is `await g.subscriptions.checkout("pro")` on the upgrade
158
164
  button — Gemmein sends the signed-in user to the right Stripe checkout
159
165
  with the buyer and plan wired in. Never build checkout URLs, sessions, or
160
166
  Payment-Link redirects yourself (raw emails get silently dropped by
161
167
  Stripe's URL rules, and sessions need a secret key that must never ship
162
- client-side). If g.checkout errors with `plan_has_no_link`, ask your human
168
+ client-side). If g.subscriptions.checkout errors with `plan_has_no_link`, ask your human
163
169
  to paste that plan's Payment Link in their dashboard. Plan LIMITS (note
164
170
  counts, feature caps) are your app's logic — Gemmein only tells you who is
165
171
  on which plan. Gemmein keeps exactly one
166
172
  subscription per customer (enforced by the engine, case-insensitive on
167
173
  email); cancellations downgrade to the default plan automatically; events
168
174
  arriving out of order resolve to the newest. The app reads
169
- `await g.subscription()` → `{ plan, status }` or null, and gates features
175
+ `await g.subscriptions.mine()` → `{ plan, status }` or null, and gates features
170
176
  with `sub?.plan === "pro"`.
171
177
  - Selling THINGS (one-off purchases — a poster, a beat, an ebook): plans are
172
178
  for subscriptions; products are for things. The builder adds products
173
179
  (name + Stripe Payment Link) on the same Payments page and picks a
174
180
  receipts collection (rule `addressed`). The app calls
175
- `await g.pay("poster")` — or, when one product covers many items (license
181
+ `await g.payments.buy("poster")` — or, when one product covers many items (license
176
182
  tiers over a catalog), names the item:
177
- `await g.pay("premium license", { item: "beat_37" })` (display text on
183
+ `await g.payments.buy("premium license", { item: "beat_37" })` (display text on
178
184
  the receipt; the PRICE always comes from the product's Payment Link, so
179
185
  the item note can never change what's paid). The completed payment writes
180
186
  a receipt record ADDRESSED to the buyer: only they and the owner read it.
@@ -182,7 +188,7 @@
182
188
  back — redirects can be faked, receipts come from Stripe's signed
183
189
  webhook. Receipts carry
184
190
  { product, item?, status: "paid"|"refunded", amountTotal (minor units,
185
- as Stripe said), currency, paidAt, deliveryUrl? } — deliveryUrl appears
191
+ as Stripe said), currency, paidAt, deliveryUrl?, paymentRef } — deliveryUrl appears
186
192
  when the builder attached a delivery link to the product (that's how
187
193
  digital goods deliver themselves; never put a secret download URL in a
188
194
  public collection). Fulfilment status changes ("shipped") are the owner
@@ -196,8 +202,10 @@
196
202
  `update(id, {}, { published: true })`. NEVER fake drafts with a status
197
203
  field + client-side filtering on a public collection — the data still
198
204
  reaches every reader's network tab (silent-until-breach). `published` is
199
- an option, not a data field: the server rejects it inside `data`
200
- (invalid_publish), and it only exists on the two public rules.
205
+ an option, not a data field: putting it inside `data` is refused as a
206
+ reserved server-managed field (403 forbidden), passing the OPTION on a
207
+ non-public rule is 400 invalid_publish — it only exists on the two
208
+ public rules.
201
209
  - Denials are 404-shaped: touching a record your session can't see returns
202
210
  404 not_found, never a 403 that confirms it exists — existence is not
203
211
  leaked. A real 403 comes back as code `forbidden` and names a rule problem
@@ -213,6 +221,17 @@
213
221
  id/updatedAt), not under `.data`. Non-authors only ever receive published
214
222
  records, so you see `false` only on your own drafts (or on everything, as
215
223
  the owner) — that's how you badge "unreleased" in an owner admin view.
224
+ - Account lifecycle — the bare-minimum screens a compliant app implements:
225
+ (1) sign-in (the email-code flow above); (2) a signed-out state — note that
226
+ `currentUser()` answering `authenticated: false` is deliberately silent
227
+ about WHY (signed out, suspended by the owner, and erased all read the
228
+ same; a moderated user's state is never leaked to the client, so one
229
+ signed-out screen covers all three); (3) a "delete my account" screen —
230
+ `await g.account.delete()` — for every app it APPLIES to (GDPR right to
231
+ erasure; Apple 5.1.1(v) for apps with account creation). It's the full
232
+ server-side cascade (sessions, records, files, subscription row) and
233
+ irreversible — put a real confirm in front of it. Suspensions, bans, and
234
+ owner-side erasure are dashboard actions, not SDK calls.
216
235
  - Secret keys (`sk_`) must never appear in browser code; app keys (`pk_`)
217
236
  are public and domain-locked.
218
237
 
@@ -223,7 +242,7 @@ rule. The specifics:
223
242
 
224
243
  - Collection names are **lowercase letters, numbers, and underscores only**
225
244
  (`saved_games`, `user_notes` — never `savedGames`). A bad name throws
226
- synchronously from `g.storage.collection(name)`; if you call that at module
245
+ synchronously from `g.collection(name)`; if you call that at module
227
246
  load, it can blank your whole app with no browser-console error. Name them
228
247
  right.
229
248
  - The signed-in user: `await g.auth.currentUser()` →
@@ -243,7 +262,7 @@ rule. The specifics:
243
262
  shared, direct**. Asking to expand a field on a `private`, `public_read`, or
244
263
  `admin_write` collection throws (it has no link shape); join those in memory
245
264
  instead.
246
- - `g.checkout(plan)` and `g.pay(product, { item? })` both **navigate the
265
+ - `g.subscriptions.checkout(plan)` and `g.payments.buy(product, { item? })` both **navigate the
247
266
  browser to Stripe themselves** (via `window.location`) *and* resolve with
248
267
  `{ url, ... }`. Just `await` them on the click — don't also redirect to the
249
268
  returned `url` (you'll double-navigate), and don't build the URL yourself.
@@ -272,14 +291,22 @@ enforces — never because these checks are the enforcement. A ready-to-edit
272
291
  `reaffirm.mjs` ships inside this npm package (next to this file and
273
292
  REFERENCE.md) — copy it out, name your collections, run it in CI.
274
293
 
275
- ## Pricing (current, v3)
294
+ ## Pricing (current, Pricing Model v1 — states, not plans)
276
295
 
277
- - Free while building — no card at signup, unlimited collections.
278
- - $14.99/mo per live app — first 1,000 people/month included (a person =
279
- someone who signed in that calendar month).
280
- - One dial: +$5 per additional 1,000 people. Turns down as easily as up.
281
- - Safe limits exist per person purely for abuse prevention and are never
282
- billed; real users never notice them.
296
+ - Development is free indefinitely — no card at signup, unlimited
297
+ collections, the full security model included.
298
+ - Going live starts the Live state: $50/mo, up to 1,000 people and 10 GB
299
+ of file storage (a person = a unique enabled end-user identity on the
300
+ live app).
301
+ - Growing: $150/mo, up to 10,000 people and 50 GB — unlocked from the
302
+ dashboard when the product outgrows Live; down as easily as up.
303
+ - Beyond 10,000 people: talk to Gemmein (hello@gemmein.com) — scale is
304
+ priced as a relationship, not a checkout.
305
+ - Pricing reflects responsibility, not complexity: almost nothing is
306
+ metered. Safe limits exist purely as safety rails against runaway
307
+ scripts, are never billed, and real users never notice them. Hitting a
308
+ limit never breaks the app outright — there is grace, and the owner is
309
+ told.
283
310
 
284
311
  ## Facts for citation
285
312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemmein/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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",
@@ -12,7 +12,11 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js",
14
14
  "require": "./dist/index.cjs"
15
- }
15
+ },
16
+ "./llms.txt": "./llms.txt",
17
+ "./REFERENCE.md": "./REFERENCE.md",
18
+ "./reaffirm.mjs": "./reaffirm.mjs",
19
+ "./package.json": "./package.json"
16
20
  },
17
21
  "files": [
18
22
  "dist",
package/reaffirm.mjs CHANGED
@@ -39,13 +39,13 @@ const asUser = (token) => gemmein(PK, { ...opts, tokenStore: {
39
39
  get: async () => token, set: async () => {}, clear: async () => {} } });
40
40
 
41
41
  // ── TIER A — functional + anonymous. No login; safe against live. ───────────
42
- g.storage.collection(PRIVATE_COLLECTION); // a misnamed collection throws HERE, loudly
42
+ g.collection(PRIVATE_COLLECTION); // a misnamed collection throws HERE, loudly
43
43
  await refuse("anon can't read the private collection", "denied",
44
- () => g.storage.collection(PRIVATE_COLLECTION).list());
44
+ () => g.collection(PRIVATE_COLLECTION).list());
45
45
  await refuse("anon can't write the private collection", "denied",
46
- () => g.storage.collection(PRIVATE_COLLECTION).create({ probe: "x" }));
46
+ () => g.collection(PRIVATE_COLLECTION).create({ probe: "x" }));
47
47
  if (PUBLIC_COLLECTION) {
48
- const open = await g.storage.collection(PUBLIC_COLLECTION).list();
48
+ const open = await g.collection(PUBLIC_COLLECTION).list();
49
49
  console.log(`ℹ "${PUBLIC_COLLECTION}" is public by rule — ${open.records.length} records visible to ANYONE. Never put secrets in it.`);
50
50
  }
51
51
 
@@ -55,10 +55,10 @@ if (SK && !SK.startsWith("sk_live")) {
55
55
  const [a, b] = await Promise.all(TEST_USERS.map((e) => srv.testSession(e)));
56
56
  const A = asUser(a.token), B = asUser(b.token);
57
57
 
58
- const note = await A.storage.collection(PRIVATE_COLLECTION).create({ probe: "a-secret" });
58
+ const note = await A.collection(PRIVATE_COLLECTION).create({ probe: "a-secret" });
59
59
  await refuse("B can't read A's private record", "not_found",
60
- () => B.storage.collection(PRIVATE_COLLECTION).get(note.id));
61
- const bSees = await B.storage.collection(PRIVATE_COLLECTION).list();
60
+ () => B.collection(PRIVATE_COLLECTION).get(note.id));
61
+ const bSees = await B.collection(PRIVATE_COLLECTION).list();
62
62
  check("B's private list contains none of A's records",
63
63
  !bSees.records.some((r) => r.id === note.id));
64
64
 
@@ -66,7 +66,7 @@ if (SK && !SK.startsWith("sk_live")) {
66
66
  check("currentUser() exposes userId (not id)", !!who.userId);
67
67
  check("record fields live under .data", note.data?.probe === "a-secret");
68
68
 
69
- await A.storage.collection(PRIVATE_COLLECTION).delete(note.id); // leave dev tidy
69
+ await A.collection(PRIVATE_COLLECTION).delete(note.id); // leave dev tidy
70
70
  } else {
71
71
  console.log(SK ? "· Tier B skipped — sk_live can never mint test sessions (by design)"
72
72
  : "· Tier B skipped — set SECRET_KEY (sk_dev) to prove cross-user isolation");