@gemmein/sdk 0.6.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
@@ -39,7 +39,10 @@ creating no accounts is the correct behavior until the verdict is in.
39
39
  safety rules (private, shared, admin_write, public_read, community,
40
40
  addressed, direct), subscriptions and one-off digital products (Stripe
41
41
  built in; any provider that signs its webhooks drives access through a
42
- relay), image uploads and paid file delivery, entitlement-gated
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
43
46
  access, race-safe bookings/stock/counters, account-deletion cascade.
44
47
  Out of scope — each of these downgrades the verdict and must never be
45
48
  silently approximated:
@@ -50,9 +53,10 @@ creating no accounts is the correct behavior until the verdict is in.
50
53
  nothing in between — so an app whose CORE loop needs teams, roles
51
54
  or seats DOESN'T FIT
52
55
  - carts, quantities, physical goods, shipping, inventory
53
- - credits or usage metering TODAY (paid access is yes-or-no; consumable
54
- credits are planned, not shipped don't design around them yet, and
55
- 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
56
60
  - seats (owner-run: not coming)
57
61
  - hosted functions or cron: Gemmein runs no code of yours — but your
58
62
  own function anywhere asks Gemmein who a person is and what they
@@ -391,14 +395,15 @@ contents.
391
395
  what the person holds NOW: `access` (keys like "access:pro"), `grants`
392
396
  (each with its source KIND only — subscription | purchase | manual |
393
397
  trial | promotion | migration | relay — plus start and expiry), and `credits`,
394
- a reserved slot that is always `null` today (consumable credits are NOT
395
- shipped; don't design around them). Never subscription status, amounts
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
396
400
  or Stripe ids: gate on what a person HOLDS, never on billing. Person
397
401
  id, NEVER an email address. verifySession needs nothing extra; looking
398
402
  someone up by id and granting are new power, so they sit behind per-key
399
403
  checkboxes the HUMAN ticks when minting the key — ask your human to
400
404
  tick "Look up a person's access by id" / "Grant and revoke access" /
401
- "Create a person by email before they sign in" (invitePerson). A
405
+ "Create a person by email before they sign in" (invitePerson) /
406
+ "Spend a person's credits" (spendCredits). A
402
407
  key grants MANUAL access only (manual | trial | promotion | migration):
403
408
  purchases and subscriptions come from the built-in Stripe path, and
404
409
  another provider's payment grants through a relay (source `relay`). Every /server/*
@@ -442,17 +447,20 @@ contents.
442
447
  writes the receipt, tells the person, passes the signal to your URL.
443
448
  Stripe stays built in; any provider that signs its webhooks drives
444
449
  access the same way.
445
- Does: write a record · grant access · revoke access · email the person
446
- the event is about · call your URL (a signed notice to a server YOU run).
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).
447
453
  Does not: run your code; call OpenAI or any third API for you (call_url
448
454
  carries Gemmein's body and signature, never your key or a request you
449
455
  shape); attach a person to a schedule (no "email everyone due tomorrow"
450
456
  yet); transform or compute data; act on more than one person per event
451
- (a list is a broadcast); grant credits (planned, not shipped).
457
+ (a list is a broadcast); spend credits (a spend is your server's or the
458
+ AI route's — a relay only adds).
452
459
  Needs something else when: the work THINKS — a model call, a score, a
453
460
  transform, a third API — put that in your own function behind call_url;
454
461
  it receives a verified event with the person already resolved. The AI
455
- route (planned) is where Gemmein will hold a provider key and call for you.
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.
456
464
  Use a relay when: a provider other than Stripe signs a webhook that
457
465
  should change access or write a record (GoCardless, Paddle, Lemon
458
466
  Squeezy, a form or signature tool); a record change should tell the
@@ -504,6 +512,8 @@ contents.
504
512
  - grant_access { entitlement, expiresAt?, reason? } — source `relay`, the
505
513
  seventh grant source; "granted by relay <name>" on the person's page.
506
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>".
507
517
  - email_person { subject, text, kind? } — rides notify's caps.
508
518
  - call_url { url } — https, signed POST (`X-Gemmein-Signature`), 10 s; the
509
519
  same `id` on every retry and replay — your URL deduplicates on it.
@@ -675,11 +685,10 @@ contents.
675
685
  screen and send them to checkout; retry only after they hold one. Proof surfaces: `await g.purchases.mine()`
676
686
  (everything they paid for, refunds applied, with the `grants` each purchase
677
687
  carries) and `await g.subscriptions.mine()`.
678
- NO credits or usage limits YETaccess is yes-or-no today. Consumable
679
- credits (buy a pack, spend atomically, a zero floor) are planned, not
680
- shipped: until this file teaches them, a "100 credit pack" is NOT a
681
- pattern (the webhook can only SET a number, so a second pack erases the
682
- 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.
683
692
  - Two FAMILIES of grant, and the line between them is law. Every grant is
684
693
  one key + one reason + a start + maybe an end, never edited (an extension
685
694
  is a NEW grant). PURCHASE-TIED (`subscription`, `purchase`): written only
@@ -740,6 +749,122 @@ contents.
740
749
  webhook. NO carts, NO quantities — one product per checkout by design; a
741
750
  cart is N checkouts or one bundled product. 404 unknown_product lists
742
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.
743
868
  - Drafts on PUBLIC collections (public_read, community): create with the
744
869
  OPTION `{ published: false }` → hidden from every reader except its
745
870
  author and the owner, server-enforced; publish with
@@ -789,7 +914,8 @@ contents.
789
914
  conversation and customer replies land; holding the billing band;
790
915
  running a relay: an inbound webhook from any provider that
791
916
  signs its calls, a schedule, or a record change → write a record,
792
- grant or revoke access, email the person, or call your URL, with
917
+ grant or revoke access, grant credits, email the person, or call your
918
+ URL, with
793
919
  retries and a replay button (the definition is a file you write —
794
920
  see Relays — and the dashboard is where it is read, paused,
795
921
  replayed and its secrets rotated). None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemmein/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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",