@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/CHANGELOG.md +224 -0
- package/REFERENCE.md +397 -20
- package/dist/index.cjs +259 -5
- package/dist/index.d.cts +179 -4
- package/dist/index.d.ts +179 -4
- package/dist/index.js +256 -4
- package/llms.txt +260 -25
- package/migrations/README.md +35 -0
- package/migrations/list-limit-refusal.md +42 -0
- package/package.json +7 -3
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),
|
|
41
|
-
|
|
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
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
- usage-based INVOICING (a bill after the fact, by token or by minute):
|
|
57
|
+
not shipped. Credits ARE — consumable 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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
204
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "Gemmein SDK
|
|
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": ">=
|
|
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"
|