@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/README.md +21 -12
- package/REFERENCE.md +56 -18
- package/dist/index.cjs +133 -61
- package/dist/index.d.cts +85 -35
- package/dist/index.d.ts +85 -35
- package/dist/index.js +128 -59
- package/llms.txt +52 -25
- package/package.json +6 -2
- package/reaffirm.mjs +8 -8
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,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
|
|
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).
|
|
39
|
-
- `shared` — every signed-in user
|
|
40
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
200
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
294
|
+
## Pricing (current, Pricing Model v1 — states, not plans)
|
|
276
295
|
|
|
277
|
-
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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.
|
|
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.
|
|
44
|
+
() => g.collection(PRIVATE_COLLECTION).list());
|
|
45
45
|
await refuse("anon can't write the private collection", "denied",
|
|
46
|
-
() => g.
|
|
46
|
+
() => g.collection(PRIVATE_COLLECTION).create({ probe: "x" }));
|
|
47
47
|
if (PUBLIC_COLLECTION) {
|
|
48
|
-
const open = await g.
|
|
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.
|
|
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.
|
|
61
|
-
const bSees = await B.
|
|
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.
|
|
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");
|