@gemmein/sdk 0.5.0 → 0.7.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/llms.txt CHANGED
@@ -37,8 +37,12 @@ creating no accounts is the correct behavior until the verdict is in.
37
37
  2. Map every requirement against this file. In scope: web apps
38
38
  (TypeScript/JavaScript), passwordless email sign-in, records under seven
39
39
  safety rules (private, shared, admin_write, public_read, community,
40
- addressed, direct), Stripe-run subscriptions and one-off digital
41
- products, image uploads and paid file delivery, entitlement-gated
40
+ addressed, direct), subscriptions and one-off digital products (Stripe
41
+ built in; any provider that signs its webhooks drives access through a
42
+ relay), consumable credits (buy a pack, spend atomically, a zero
43
+ floor — see Credits), a chat call to OpenAI, Anthropic or Google on
44
+ the founder's own key at one credit per call (see The AI route),
45
+ image uploads and paid file delivery, entitlement-gated
42
46
  access, race-safe bookings/stock/counters, account-deletion cascade.
43
47
  Out of scope — each of these downgrades the verdict and must never be
44
48
  silently approximated:
@@ -49,18 +53,24 @@ creating no accounts is the correct behavior until the verdict is in.
49
53
  nothing in between — so an app whose CORE loop needs teams, roles
50
54
  or seats DOESN'T FIT
51
55
  - carts, quantities, physical goods, shipping, inventory
52
- - credits or usage metering TODAY (paid access is yes-or-no; consumable
53
- credits are planned, not shipped don't design around them yet, and
54
- never sell a "credit pack" until this file says the ledger exists)
56
+ - usage-based INVOICING (a bill after the fact, by token or by minute):
57
+ not shipped. Credits AREconsumable credits: buy a pack, spend
58
+ atomically, a zero floor — see Credits; a "100 credit pack" is a
59
+ product with Grants credits = 100, and a model call is one credit
55
60
  - seats (owner-run: not coming)
56
61
  - hosted functions or cron: Gemmein runs no code of yours — but your
57
62
  own function anywhere asks Gemmein who a person is and what they
58
- hold; read the server-side note closing this section before
59
- counting that a gap
60
- - custody of money (Stripe holds all funds; your human brings or
61
- creates their own Stripe account when the app charges)
63
+ hold, and a relay runs Gemmein's OWN verbs (write a record,
64
+ grant access, email the person, call your URL) on a webhook, a
65
+ schedule or a record change with no code; read the server-side
66
+ note closing this section before counting that a gap
67
+ - custody of money (Gemmein never holds funds; your human's own payment
68
+ provider does. Stripe is the built-in path because it is what Gemmein
69
+ itself uses; GoCardless, Paddle, Lemon Squeezy or any provider that
70
+ signs its webhooks drives access through a relay — the founder keeps
71
+ their provider)
62
72
  - multi-seller payouts or revenue splits: every payment lands whole in
63
- your human's ONE Stripe account. A marketplace where sellers get
73
+ your human's ONE provider account. A marketplace where sellers get
64
74
  paid out means your human pays sellers themselves, outside Gemmein —
65
75
  say that out loud in the verdict
66
76
  3. Deliver one of three verdicts to your human BEFORE installing anything:
@@ -87,7 +97,9 @@ look a person up by id and grant or revoke access by hand.)
87
97
  the verdict downgrades when the server piece needs a call a secret key
88
98
  cannot make, or when the app needs compute your human has nowhere to
89
99
  run — a cron schedule or an always-on worker with no host of its own —
90
- because Gemmein never runs your code.
100
+ because Gemmein never runs your code. A schedule whose work is one of
101
+ the relay verbs (write a record, call your URL) is not that gap:
102
+ see Relays.
91
103
 
92
104
  ## Getting started (AI-run, human-answered)
93
105
 
@@ -200,8 +212,10 @@ contents.
200
212
  nothing in between. No team seats, no staff accounts, no workspaces, on
201
213
  purpose: one person holds the dashboard, and everyone else is a customer.
202
214
  - The platform holds identity, data, and subscription state. It never holds,
203
- moves, or processes money — Stripe handles all payments end-to-end; Gemmein
204
- only receives Stripe's webhooks and records who is on which plan. App
215
+ moves, or processes money — Stripe is built in end-to-end because it is
216
+ what Gemmein itself uses, and any provider that signs its webhooks drives
217
+ access the same way through a relay; Gemmein only receives the webhooks
218
+ and records who holds what. App
205
219
  compute/hosting
206
220
  belongs anywhere (Vercel, Cloudflare, any static host). There are no hosted
207
221
  functions — server-side needs use a secret key from any server.
@@ -295,7 +309,8 @@ contents.
295
309
  read-only: never store your own userId/role/owner fields inside `data`.
296
310
  `.list()` returns `{ records, hasMore }` (an object, not an array) and
297
311
  accepts `{ limit, sort: "newest"|"oldest"|"updated", where, search, cursor,
298
- since }`.
312
+ since }`. limit is 25 by default and 100 at most; a larger ask is refused
313
+ with invalid_limit — page with cursor.
299
314
  - Live data (dashboards, feeds, inboxes): nothing is pushed — BY DESIGN
300
315
  (pushed data is where other platforms leak; every Gemmein read passes the
301
316
  permission check). Instead, polling is built in and cheap:
@@ -365,21 +380,33 @@ contents.
365
380
  await g.grantAccess(personId, { entitlement: "access:pro",
366
381
  source: "trial", expiresAt, reason: "7-day trial" });
367
382
  await g.revokeAccess(personId, grantId, { reason: "trial ended" });
383
+ const { person, created } = await g.invitePerson("client@example.com");
368
384
 
385
+ A secret key with the capability may create a person by email BEFORE
386
+ they sign in — the envelope, the invoice, the client portal, the
387
+ booking-winner email: create the person, hand them the file, address
388
+ the record, notify them; their first sign-in lands on a ready account.
389
+ `invitePerson(email)` is create-or-fetch (created: true the first time,
390
+ false after; one id, case-insensitive) and is the ONE server call that
391
+ takes an email; everything else takes the person id it returns. The
392
+ owner sees them as "invited · hasn't signed in yet" until they sign in.
369
393
  ONE verifySession per request answers identity AND holdings — don't
370
394
  call it twice, don't cache the answer past the request. `holdings` is
371
395
  what the person holds NOW: `access` (keys like "access:pro"), `grants`
372
396
  (each with its source KIND only — subscription | purchase | manual |
373
- trial | promotion | migration — plus start and expiry), and `credits`,
374
- a reserved slot that is always `null` today (consumable credits are NOT
375
- shipped; don't design around them). Never subscription status, amounts
397
+ trial | promotion | migration | relay — plus start and expiry), and `credits`,
398
+ `{ balance }` how many credits the person holds now (engine 0.8.0 and
399
+ later; `null` from an older local engine). Never subscription status, amounts
376
400
  or Stripe ids: gate on what a person HOLDS, never on billing. Person
377
401
  id, NEVER an email address. verifySession needs nothing extra; looking
378
402
  someone up by id and granting are new power, so they sit behind per-key
379
403
  checkboxes the HUMAN ticks when minting the key — ask your human to
380
- tick "Look up a person's access by id" / "Grant and revoke access". A
404
+ tick "Look up a person's access by id" / "Grant and revoke access" /
405
+ "Create a person by email before they sign in" (invitePerson) /
406
+ "Spend a person's credits" (spendCredits). A
381
407
  key grants MANUAL access only (manual | trial | promotion | migration):
382
- purchases and subscriptions still come only from Stripe. Every /server/*
408
+ purchases and subscriptions come from the built-in Stripe path, and
409
+ another provider's payment grants through a relay (source `relay`). Every /server/*
383
410
  call a resolved secret key makes, ok or refused, lands in that key's
384
411
  usage ledger on the owner's Keys page (a rejected or publishable key
385
412
  can't be attributed, so it reaches only the request log); refusals also
@@ -408,6 +435,92 @@ contents.
408
435
  verifySession or the dashboard, never a name
409
436
  invalid_body one malformed field (token, expiresAt, reason)
410
437
  — the message names which one and what it needs
438
+ invalid_email invitePerson: must look like name@domain
439
+ invite_capped invitePerson: 500 invite calls per app per day (a
440
+ fetch of an existing person counts) — the message
441
+ says where to write; resetAt says when
442
+ - Relays — route, map, authorise, never compute.
443
+ What it is: a rule in one JSON file — when this happens, Gemmein does
444
+ these things, keeps a row per event, retries, and shows every run in the
445
+ owner's dashboard with a replay button. A relay takes a verified signal
446
+ and closes a circuit: GoCardless says paid, the relay opens access,
447
+ writes the receipt, tells the person, passes the signal to your URL.
448
+ Stripe stays built in; any provider that signs its webhooks drives
449
+ access the same way.
450
+ Does: write a record · grant access · revoke access · grant credits ·
451
+ email the person the event is about · call your URL (a signed notice to
452
+ a server YOU run).
453
+ Does not: run your code; call OpenAI or any third API for you (call_url
454
+ carries Gemmein's body and signature, never your key or a request you
455
+ shape); attach a person to a schedule (no "email everyone due tomorrow"
456
+ yet); transform or compute data; act on more than one person per event
457
+ (a list is a broadcast); spend credits (a spend is your server's or the
458
+ AI route's — a relay only adds).
459
+ Needs something else when: the work THINKS — a model call, a score, a
460
+ transform, a third API — put that in your own function behind call_url;
461
+ it receives a verified event with the person already resolved. The AI
462
+ route is where Gemmein holds your provider key and makes a chat call for
463
+ a signed-in person at one credit each — see The AI route.
464
+ Use a relay when: a provider other than Stripe signs a webhook that
465
+ should change access or write a record (GoCardless, Paddle, Lemon
466
+ Squeezy, a form or signature tool); a record change should tell the
467
+ person or open access; something should run on a clock and write a
468
+ record or ping your URL.
469
+ Not when: the work computes or reshapes data (that is your function);
470
+ one event should email many people (a broadcast, planned); a schedule
471
+ must address a person (not yet); you already use Stripe for the money —
472
+ the built-in Stripe path already grants access and writes receipts, and
473
+ a relay adds nothing there.
474
+ The GoCardless case end to end — a payment confirms, the payer gets Pro,
475
+ an email, and your function hears about it. The file is
476
+ `gemmein/relays/<name>.json`; the owner's dashboard shows it read-only
477
+ (Setup → Relays) with its receiver URL, its secrets (shown once), every
478
+ event with each action's result, and a replay button:
479
+
480
+ {
481
+ "name": "gocardless-paid",
482
+ "trigger": {
483
+ "kind": "receiver",
484
+ "verify": { "scheme": "hmac_sha256_header", "header": "Webhook-Signature" },
485
+ "map": {
486
+ "event_id": "events.0.id",
487
+ "event_type": "events.0.action",
488
+ "person_email": "events.0.details.customer_email"
489
+ },
490
+ "when": { "event_type": "confirmed" }
491
+ },
492
+ "actions": [
493
+ { "type": "grant_access", "entitlement": "Pro", "reason": "GoCardless payment {{mapped.event_id}}" },
494
+ { "type": "email_person", "subject": "Your payment is confirmed", "text": "Thanks. Pro is open on your account." },
495
+ { "type": "call_url", "url": "https://hooks.example.com/paid" }
496
+ ]
497
+ }
498
+
499
+ Triggers (exactly one per relay):
500
+ - receiver — a provider's signed webhook at `POST /hooks/<appId>/<name>`;
501
+ `verify` names the scheme (hmac_sha256_header | stripe | svix |
502
+ shared_token), `map` names dotted paths into the body (`event_id`
503
+ dedupes, `person_email` resolves the person, the rest ride as
504
+ `{{mapped.<name>}}`), `when` is an exact match on mapped fields.
505
+ - schedule — `every` 15m | 30m | 1h | 6h | 12h | 1d, `at: "HH:MM"` UTC
506
+ with 1d; one tick per period; no person, so person actions are refused.
507
+ - data_change — `collection`, `on` ⊆ created | updated | deleted, optional
508
+ `where` on the record's data; never for the runner's own writes.
509
+ Authorise on fields the signed-in user cannot set, or from a receiver.
510
+ Actions (1 to 10, run in order, each idempotent per event):
511
+ - write_record { collection, data, to?: "person" } — keyed per event.
512
+ - grant_access { entitlement, expiresAt?, reason? } — source `relay`, the
513
+ seventh grant source; "granted by relay <name>" on the person's page.
514
+ - revoke_access { entitlement } — ends every live grant of it.
515
+ - grant_credits { amount (1..10,000), reason? } — adds to the person's
516
+ balance, once per event; the ledger line reads "relay: <reason>".
517
+ - email_person { subject, text, kind? } — rides notify's caps.
518
+ - call_url { url } — https, signed POST (`X-Gemmein-Signature`), 10 s; the
519
+ same `id` on every retry and replay — your URL deduplicates on it.
520
+ Full depth — every verify scheme, the map and when grammar, the call_url
521
+ contract and signature, every refusal code, the limits and both rails:
522
+ https://docs.gemmein.com/relays (the same chapter is REFERENCE.md
523
+ "## Relays", which the MCP `reference` tool serves).
411
524
  - Linking records (author on a post, product on an order): store the other
412
525
  record's id in a field (`authorProfileId: profile.id`) — in collections
413
526
  users write (community, shared, direct) the server learns it's a link;
@@ -572,11 +685,10 @@ contents.
572
685
  screen and send them to checkout; retry only after they hold one. Proof surfaces: `await g.purchases.mine()`
573
686
  (everything they paid for, refunds applied, with the `grants` each purchase
574
687
  carries) and `await g.subscriptions.mine()`.
575
- NO credits or usage limits YETaccess is yes-or-no today. Consumable
576
- credits (buy a pack, spend atomically, a zero floor) are planned, not
577
- shipped: until this file teaches them, a "100 credit pack" is NOT a
578
- pattern (the webhook can only SET a number, so a second pack erases the
579
- first). NO seats, by design — not coming.
688
+ Access is yes-or-no. A QUANTITY beside it is credits buy a pack, spend
689
+ atomically, a zero floor (see Credits): a "100 credit pack" is a product
690
+ with Grants credits = 100, and the ledger ADDS at every purchase, so a
691
+ second pack makes 200. NO seats, by design not coming.
580
692
  - Two FAMILIES of grant, and the line between them is law. Every grant is
581
693
  one key + one reason + a start + maybe an end, never edited (an extension
582
694
  is a NEW grant). PURCHASE-TIED (`subscription`, `purchase`): written only
@@ -637,6 +749,122 @@ contents.
637
749
  webhook. NO carts, NO quantities — one product per checkout by design; a
638
750
  cart is N checkouts or one bundled product. 404 unknown_product lists
639
751
  what the app actually sells — use those names.
752
+ - Credits — a quantity beside access. See https://docs.gemmein.com/credits
753
+ What it is: a balance your customers hold and your product spends — a
754
+ pack they buy, a comp you give, a call that costs one.
755
+ Does: a product grants credits at purchase (its Grants credits field);
756
+ your server spends them with a reason; the AI route spends one per call;
757
+ a relay grants them; the owner's dashboard shows the balance and the
758
+ ledger and comps by hand. Every movement is one ledger line.
759
+ Does not: expire; go negative (floor 0 — a spend past the balance is
760
+ refused whole, never partly); price by token; open access (that is an
761
+ entitlement — a person with 500 credits and no plan is still refused by
762
+ a locked collection); count seats or quotas; spend from the browser.
763
+ Needs something else when: access should switch on with a plan (an
764
+ entitlement — see Paid ACCESS); you bill by usage after the fact (not
765
+ shipped — sell packs up front); the spend must happen in the browser
766
+ (it never does: your server, a relay or the AI route moves credits).
767
+ The case: "sell a 100-credit pack and let each chat cost one". The pack
768
+ is a product on the Payments page with Grants credits = 100 (the
769
+ product's `grantsCredits`); each purchase ADDS 100 to the buyer's
770
+ balance (two packs make 200), and a FULL refund claws back what is still
771
+ unspent, floor 0. The app reads the balance to show it:
772
+
773
+ const { balance } = await g.credits.balance() // browser, signed in
774
+
775
+ Your server spends by person id, with a reason and a key, so a retry
776
+ never spends twice (the key needs "Spend a person's credits" ticked):
777
+
778
+ const r = await gemmeinServer(sk).spendCredits(person.id, {
779
+ amount: 1, reason: "export", key: `export:${jobId}`
780
+ })
781
+ // { ok: true, spent: 1, deduped: false, balance: { before: 12, after: 11 },
782
+ // event: { id: "cev_…", reason: "export", actor: "<the key's name>" } }
783
+ // the same key again: spent: 0, deduped: true, the same event, nothing moved
784
+ // 402 credits_exhausted — "this person has 0 credits — the spend needs 1"
785
+
786
+ `holdings.credits` is `{ balance }` on verifySession and holdings, so one
787
+ verify answers who, what they hold AND how many. Numbers: no expiry ·
788
+ floor 0 · a balance carries at most 1,000,000,000 · a pack 1..1,000,000 ·
789
+ a server spend 1..10,000 per call · a comp ≤100,000 · a relay grant
790
+ 1..10,000 · spend lines kept 90 days on the cloud rail (purchases, grants
791
+ and clawbacks are kept). Facts: a purchase by an email that has never
792
+ signed in creates the person and credits them; your `key` is scoped to
793
+ the person and namespaced (`srv:`), so one order id reused for two people
794
+ charges both and rotating the secret key never re-charges; a full refund
795
+ claws back what THAT purchase's ledger row granted, whatever the product
796
+ says today; the owner's page lists the newest 50 non-spend lines and the
797
+ credits the person spent in the last 30 days. Codes: `credits_exhausted`
798
+ (402 — carries the balance; show the pack, never retry the spend) ·
799
+ `capability_required` (403) · `person_not_found` (404) · `invalid_amount`
800
+ / `invalid_reason` / `invalid_key` (400) · `dedupe_conflict` (409 — the
801
+ key already names a different movement) · `credits_ceiling` (409 — the
802
+ balance would pass 1,000,000,000; nothing added).
803
+ - The AI route — your app talks to OpenAI, Anthropic or Google through
804
+ Gemmein, on YOUR provider key, which never reaches the browser.
805
+ See https://docs.gemmein.com/ai
806
+ What it is: your human pastes their provider key once in the dashboard's
807
+ Keys room (write-only: never shown again, never in the app's code); a
808
+ signed-in person's chat request reaches the provider through Gemmein and
809
+ costs one credit.
810
+ Does: takes the provider's OWN request body from a signed-in person,
811
+ spends one credit, forwards the body as sent — minus the `provider`
812
+ field, and for Google minus `model` and `stream`, which ride the URL —
813
+ with the key, content-type and accept added, passes the provider's
814
+ status and bytes straight back (a stream stays a stream), and refunds
815
+ the credit only when the provider fails before its first byte.
816
+ Does not: choose models, cache, summarise, moderate, or reshape the
817
+ request or the answer; run from a server key (`sk_` is refused); reach
818
+ any host but the three; price by token (one credit per call, whatever
819
+ the length); refund a call that dies mid-stream, or one the caller
820
+ abandons before the headers (hanging up early does not refund).
821
+ Needs something else when: your SERVER makes the model call (call the
822
+ provider directly with your key; the gate still answers who they are and
823
+ what they hold); the call is not a chat call — embeddings, images, audio
824
+ (the provider directly, from your server); the provider is not on the
825
+ list (write to hello@gemmein.com).
826
+ The case: "a chat app where each message costs one credit and my OpenAI
827
+ key stays private". The body is exactly what the provider documents for
828
+ its chat endpoint; `g.ai.chat` returns the fetch Response untouched:
829
+
830
+ const res = await g.ai.chat({
831
+ model: "gpt-4o-mini", stream: true,
832
+ messages: [{ role: "user", content: text }]
833
+ }, { provider: "openai" }) // provider is optional when one key is set
834
+ // res.status and res.body are the provider's own (SSE stays SSE) —
835
+ // a provider 4xx/5xx comes back the same way: read res.ok; only
836
+ // Gemmein's own refusals throw
837
+ // res.headers: x-gemmein-credits-remaining: 41
838
+ // x-gemmein-credit: refunded (a failure before the first byte)
839
+
840
+ For a non-stream answer as one string, whichever provider answered (a
841
+ provider non-2xx throws `provider_error` with its status and message):
842
+
843
+ const answer = await g.ai.text({ model: "gpt-4o-mini",
844
+ messages: [{ role: "user", content: text }] })
845
+
846
+ Branch on `err.code`: credits_exhausted (402 — the message carries the
847
+ balance; show the pack) · ai_not_configured (409 — no key set; the owner
848
+ pastes one) · provider_required (400 — more than one key set; name
849
+ `provider`) · model_not_allowed (403 — the owner's allowlist names what
850
+ is allowed) · ai_capped (429 — 20 per person per minute; wait for
851
+ `resetAt`) · payload_too_large (413 — 256 KB) · invalid_body (400 — the
852
+ body must be the provider's JSON object, nested at most 32 levels) ·
853
+ session_required (401 — sign in first) · scope_denied (403 — a server
854
+ key; the route is for the browser) · provider_unreachable (502 — no
855
+ answer before the first byte; the credit is refunded; retry) ·
856
+ provider_error (`g.ai.text` only — the provider's own non-2xx, its
857
+ status and message). Numbers: 1 credit per call · 20/min/person · 256 KB
858
+ · 170 s in all, and on a stream 10 s to the first response headers.
859
+ Facts: `?provider=` and `?stream=1` on the URL do what the body fields
860
+ do; the owner may list up to 20 allowed models (the Keys room's test
861
+ call uses the first); every `/ai/chat` call counts toward the app's
862
+ api_requests band like any other request; a provider that echoes the key
863
+ in a refusal reaches you as `***<hint>`. The owner's Usage room counts
864
+ the calls; the provider bills the tokens on the owner's own account.
865
+ `gemmein dev` answers a fake provider without a key (header
866
+ `x-gemmein-ai: fake`), so the loop runs locally; set GEMMEIN_AI_KEY_OPENAI
867
+ / _ANTHROPIC / _GOOGLE there for a real call.
640
868
  - Drafts on PUBLIC collections (public_read, community): create with the
641
869
  OPTION `{ published: false }` → hidden from every reader except its
642
870
  author and the owner, server-enforced; publish with
@@ -683,7 +911,14 @@ contents.
683
911
  moderation and status flips on other users' records; scoping a secret
684
912
  key to collections and actions; verifying the sending and receiving
685
913
  domain; usage and logs; the Inbox, where every notify() send is a
686
- conversation and customer replies land; holding the billing band. None
914
+ conversation and customer replies land; holding the billing band;
915
+ running a relay: an inbound webhook from any provider that
916
+ signs its calls, a schedule, or a record change → write a record,
917
+ grant or revoke access, grant credits, email the person, or call your
918
+ URL, with
919
+ retries and a replay button (the definition is a file you write —
920
+ see Relays — and the dashboard is where it is read, paused,
921
+ replayed and its secrets rotated). None
687
922
  of them has an SDK equivalent, so an owner screen you build for one is
688
923
  a page that already exists, without the enforcement.
689
924
  - Secret keys (`sk_`) must never appear in browser code; app keys (`pk_`)
@@ -0,0 +1,35 @@
1
+ # Migration notes — @gemmein/sdk
2
+
3
+ One file per behaviour change that may ask something of an app already
4
+ built on the SDK. `CHANGELOG.md` says what changed in each version; a note
5
+ here says what, if anything, your app has to do about it, and by when.
6
+
7
+ ## Format
8
+
9
+ Each note is a Markdown file named for the change (`list-limit-refusal.md`),
10
+ and opens with two header lines:
11
+
12
+ ```
13
+ Since: <version> — the SDK version the change arrived in
14
+ Action required by: <version> — the version by which an app must have
15
+ acted, or "none" when nothing is required
16
+ ```
17
+
18
+ Then, in this order:
19
+
20
+ 1. **What changed** — the old behaviour and the new one, as facts.
21
+ 2. **Who is affected** — the call shapes that see a difference. An app that
22
+ never made those calls can stop reading.
23
+ 3. **What to do** — the smallest change that keeps the app correct, with
24
+ the code.
25
+ 4. **How to tell** — how the change shows up at runtime (the error code,
26
+ the message) so it can be recognised in logs.
27
+
28
+ A note is never edited after the version it names ships; a later change to
29
+ the same surface gets its own file.
30
+
31
+ ## Notes
32
+
33
+ | File | Since | Action required by |
34
+ |------|-------|--------------------|
35
+ | [`list-limit-refusal.md`](./list-limit-refusal.md) | 0.6.0 | 0.6.0 |
@@ -0,0 +1,42 @@
1
+ # `list({ limit })` over 100 is refused
2
+
3
+ ```
4
+ Since: 0.6.0
5
+ Action required by: 0.6.0
6
+ ```
7
+
8
+ ## What changed
9
+
10
+ Before 0.6.0 the server clamped a list `limit` above 100 to 100 and
11
+ answered a normal page (`hasMore: true`). From 0.6.0 a `limit` outside 1..100
12
+ is refused: the call throws `GemmeinError` with code `invalid_limit` and the
13
+ message "limit must be between 1 and 100". The default (no `limit`) is 25 and
14
+ the maximum is 100, as before. The same applies on the local engine
15
+ (`gemmein dev`) from 0.7.0 — it runs the same server code.
16
+
17
+ ## Who is affected
18
+
19
+ Apps that call `collection.list({ limit: N })` with `N > 100`, `N < 1`, or a
20
+ non-integer `N`, on either the browser client or `gemmeinServer(sk)`.
21
+ `watch()` is unaffected (it asks for 100). An app that passes no `limit`, or
22
+ 1..100, sees no difference.
23
+
24
+ ## What to do
25
+
26
+ Ask for at most 100 and page with `cursor`:
27
+
28
+ ```ts
29
+ const all = [];
30
+ let cursor: string | undefined;
31
+ do {
32
+ const page = await g.collection("orders").list({ limit: 100, cursor });
33
+ all.push(...page.records);
34
+ cursor = page.hasMore ? page.cursor : undefined;
35
+ } while (cursor);
36
+ ```
37
+
38
+ ## How to tell
39
+
40
+ A 400 with `code: "invalid_limit"` in the response, `err.code ===
41
+ "invalid_limit"` in the SDK, and the row `invalid_limit` under the key's
42
+ refusals on the Keys page when a secret key made the call.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gemmein/sdk",
3
- "version": "0.5.0",
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.",
3
+ "version": "0.7.0",
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",
7
7
  "main": "./dist/index.cjs",
@@ -22,11 +22,13 @@
22
22
  "dist",
23
23
  "llms.txt",
24
24
  "REFERENCE.md",
25
+ "CHANGELOG.md",
26
+ "migrations",
25
27
  "reaffirm.mjs"
26
28
  ],
27
29
  "sideEffects": false,
28
30
  "engines": {
29
- "node": ">=18"
31
+ "node": ">=20"
30
32
  },
31
33
  "keywords": [
32
34
  "auth",
@@ -45,6 +47,8 @@
45
47
  "access": "public"
46
48
  },
47
49
  "scripts": {
50
+ "version:sync": "node scripts/sync-version.mjs",
51
+ "prebuild": "node scripts/sync-version.mjs",
48
52
  "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json && mv dist/cjs/index.js dist/index.cjs && rm -rf dist/cjs && cp dist/index.d.ts dist/index.d.cts",
49
53
  "prepack": "npm run build",
50
54
  "prepublishOnly": "npm run build"