@homie-rocks/studio 0.33.1 → 0.35.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 +53 -1
- package/README.md +18 -14
- package/bin/homie-studio.mjs +4 -4
- package/lib/build.mjs +3 -3
- package/lib/cloudflare.mjs +3 -2
- package/lib/doctor.mjs +11 -11
- package/lib/mcp-tools.mjs +15 -0
- package/lib/mcp.mjs +1 -0
- package/lib/scaffold.mjs +45 -39
- package/lib/shop-catalog.mjs +4 -4
- package/lib/shop-links.mjs +91 -0
- package/lib/shop.mjs +28 -20
- package/lib/stripe-connect.mjs +135 -0
- package/lib/template-history.json +84 -0
- package/package.json +1 -1
- package/shop/SELLING.md +15 -91
- package/shop/SHOP.md +185 -274
- package/shop/shop.ts +20 -5
- package/worker/index.mjs +7 -1
- package/worker/office.mjs +1 -1
- package/worker/players.mjs +1 -1
- package/worker/referrals.mjs +9 -12
- package/worker/shop-links.mjs +46 -0
- package/worker/shop-page.mjs +93 -31
- package/worker/shop-rules.mjs +39 -58
- package/worker/shop-store.mjs +101 -9
- package/worker/shop.mjs +638 -223
- package/worker/stripe.mjs +22 -13
- package/worker/version.mjs +1 -1
package/shop/SHOP.md
CHANGED
|
@@ -1,292 +1,203 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Your shop, your choices
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
is the seller; homie.rocks never holds, routes or settles its money, is not a Stripe Connect platform, and takes
|
|
6
|
-
no cut. There is no shared currency between studios. `SELLING.md` beside this file is the owner's plain-words
|
|
7
|
-
note on what selling means for them (refunds, disputes, tax, kids; not legal advice).
|
|
3
|
+
You sell through your own Stripe account on your own Cloudflare account. Homie takes no cut.
|
|
4
|
+
The studio is the seller and is responsible for the law where it sells and for Stripe's terms.
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
At the studio's root, reviewed in git. Nothing secret is ever in it.
|
|
6
|
+
A minimal `shop.json`:
|
|
12
7
|
|
|
13
8
|
```json
|
|
14
9
|
{
|
|
15
|
-
"till": "stripe",
|
|
16
|
-
"currency": "usd",
|
|
17
|
-
"policy": { "preset": "protective" },
|
|
18
10
|
"items": [
|
|
19
|
-
{ "id": "
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ "id": "season-1", "kind": "pass", "name": "Season 1", "price": 800, "ends": "2027-01-31", "gives": ["track:season-1"] },
|
|
23
|
-
{ "id": "tip", "kind": "tip", "name": "Buy the studio a coffee", "price": "choose", "min": 200, "max": 5000 }
|
|
24
|
-
],
|
|
25
|
-
"referrals": { "rate": 0.10, "windowDays": 30, "capPerPlayer": 1000, "holdDays": 30, "minimumInvoice": 2500 }
|
|
11
|
+
{ "id": "badge", "name": "Studio badge", "price": 500, "gives": ["badge:studio"] },
|
|
12
|
+
{ "id": "gift", "kind": "tip", "name": "A gift", "price": "choose" }
|
|
13
|
+
]
|
|
26
14
|
}
|
|
27
15
|
```
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
17
|
+
The default is an open shop: guests can buy, repeatedly, from any page, without an age question.
|
|
18
|
+
Guest purchases belong to their browser session; signing in keeps them. Clearing the guest cookie loses
|
|
19
|
+
access, so adding a passkey is useful. Checkout checks browser cookies and explains how to enable them if blocked. Item kinds and wording are yours; nothing scans them.
|
|
20
|
+
A kind is a label, not a recurring-billing, wallet, random-reward or fulfillment implementation.
|
|
21
|
+
|
|
22
|
+
## Settings you can choose
|
|
23
|
+
|
|
24
|
+
- `till`: `stripe` (default), `stripe-managed`, or `off`; `currency`: `usd` by default.
|
|
25
|
+
`automaticTax: true` enables Stripe Tax. Managed Payments uses Stripe's eligibility and tax settings.
|
|
26
|
+
- Items: unique `id`, `name`, `price`, optional `kind` (default `item`), `blurb`, `gives`, `badge`,
|
|
27
|
+
`game` (catalog placement), `advantage`, `taxCode`, `starts`, `ends`, and `days`.
|
|
28
|
+
Any item can use `price: "choose"`; a `tip` uses it when price is omitted, or can have a fixed price.
|
|
29
|
+
Variable amounts have optional `min` (zero if omitted) and `max` (unbounded if omitted).
|
|
30
|
+
Dates and durations apply only when written.
|
|
31
|
+
- `capPerPlayerMonth`: optional spending cap. A cart reserves its whole pre-tax total atomically.
|
|
32
|
+
- `checkoutMinutes`: optional session lifetime, 30 to 1440 minutes (values below 31 use 31 to leave a transport margin); Stripe defaults to 1440 (24 hours).
|
|
33
|
+
Returning from checkout or starting another checkout asks Stripe to expire your open session.
|
|
34
|
+
Its reservation releases only when Stripe confirms expiry or another terminal outcome.
|
|
35
|
+
- `refundDays`: optional self-service refund window for items, including used items by default.
|
|
36
|
+
A tip is never refunded by the player. The studio can refund any paid order or line from its office; disputed charges wait for Stripe to resolve the dispute.
|
|
37
|
+
- `referrals`: optional `rate`, `windowDays`, `capPerPlayer`, `holdDays`, `minimumInvoice`,
|
|
38
|
+
`billingEmail` and `accept`. `rate` defaults to zero until written;
|
|
39
|
+
omitted limits do not apply. `referralNewPlayersOnly: true` limits attribution to new players.
|
|
40
|
+
- `catalog`: modes (`test`, `live`) populated by `homie-studio shop catalog` for named Stripe Products.
|
|
41
|
+
Prices always come from your file, even when a Stripe Product exists.
|
|
42
|
+
- Flood protection: `purchaseAttemptsPerMinute` (6 per buyer),
|
|
43
|
+
`purchaseAttemptsPerAddressPerMinute` (600), `guestBuyersPerAddressPerHour` (600).
|
|
44
|
+
These are configurable positive integers, per Worker instance. `PLAYER_LIMIT_DAILY` also applies.
|
|
45
|
+
Referral delivery uses `REFERRAL_STATEMENTS_PER_MINUTE`, `REFERRAL_STATEMENTS_GLOBAL_PER_MINUTE`
|
|
46
|
+
(both 30), and `REFERRAL_STATEMENT_BYTES` (65536). `requestBytes` overrides the catalog-sized shop request
|
|
47
|
+
allowance; `SHOP_WEBHOOK_BYTES` (524288) controls the webhook byte allowance.
|
|
48
|
+
|
|
49
|
+
No toolkit ceiling applies to prices, quantities, tips, durations, refund windows, caps or referral terms.
|
|
50
|
+
Money is integer Stripe currency units, with checked safe arithmetic. Stripe's currency rules and
|
|
51
|
+
[Checkout's maximum of 100 lines](https://docs.stripe.com/api/checkout/sessions/create#line_items) apply.
|
|
52
|
+
Free and paid lines can share a cart. An entirely free cart grants locally without a Stripe call. Product text limits and currency-unit requirements come from Stripe;
|
|
53
|
+
[Stripe decides settlement minimums and payment-method limits](https://docs.stripe.com/currencies).
|
|
54
|
+
|
|
55
|
+
## Optional policy bundles
|
|
56
|
+
|
|
57
|
+
An omitted `policy` or omitted `preset` uses the open settings (`custom`).
|
|
58
|
+
Write `"policy": { "preset": "protective" }` to choose the earlier account and age behavior.
|
|
59
|
+
Existing files with no preset now use the open default. Explicit settings remain in effect.
|
|
60
|
+
Each field can be overridden independently.
|
|
61
|
+
|
|
62
|
+
| Policy | Open / custom | protective | adults-only |
|
|
64
63
|
|---|---|---|---|
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `paidRandomRewards`, `countdownOffers`, `virtualCurrency` | false | false | true |
|
|
74
|
-
| `supporterAdvantages`, `repeatPurchases`, `refundUsedItems`, `televisionCheckout` | false | false | true |
|
|
75
|
-
|
|
76
|
-
`children` and `teens` each accept `deny`, `parent`, or `allow`. The neutral age question stores only a band,
|
|
77
|
-
never the birth year. `childAge` and `adultAge` set the band thresholds (13 and 18); the neutral picker spans 120 years. A parent's link lasts a week as a security expiry. Account-free purchases use a guest
|
|
78
|
-
session in this browser; clearing its cookie loses access, so buyers can add a passkey to keep their purchases.
|
|
79
|
-
The protective preset hides the shop on kids servers and kids studios, blocks purchases under 13, requires
|
|
80
|
-
parent checkout for teens, hides game advantages on beginner and kids servers, refuses paid randomness and
|
|
81
|
-
countdowns, and sends TV shoppers to a phone. Nothing relaxes those protections without a studio setting.
|
|
82
|
-
|
|
83
|
-
For example, `{ "policy": { "preset": "protective", "repeatPurchases": true } }` changes only repeat purchases.
|
|
84
|
-
The custom preset deliberately enables all policies; choose it only as a considered studio decision.
|
|
85
|
-
`kind` is descriptive, not a whitelist: services and subscription labels still use the existing one-time checkout.
|
|
86
|
-
Virtual-currency keys and random-reward metadata do not implement a wallet or random-reward fulfillment engine.
|
|
64
|
+
| `requireAccount`, `ageQuestion` | false | true | true |
|
|
65
|
+
| `children` | allow | deny | deny |
|
|
66
|
+
| `teens` | allow | parent | deny |
|
|
67
|
+
| `kidsStudio`, `kidsServer` | true | false | false |
|
|
68
|
+
| `kidsAdvantages`, `beginnerAdvantages` | true | false | false |
|
|
69
|
+
| `repeatPurchases`, `refundUsedItems`, `televisionCheckout` | true | false | false |
|
|
70
|
+
| `withdrawalAcknowledgement` | false | true | true |
|
|
71
|
+
| `childAge`, `adultAge` | 13, 18 (inactive without age policy) | 13, 18 | 13, 18 |
|
|
87
72
|
|
|
88
|
-
|
|
73
|
+
Age-band choices are `allow`, `deny`, or `parent`. The question retains a band, not a birth year.
|
|
74
|
+
Parent links expire after a week for security. The TV option can show a phone code instead of checkout.
|
|
75
|
+
Content scans do not exist in any preset.
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
representation but require multiples of 100. BHD, JOD, KWD, OMR and TND use scale 1000. Stripe's current currency documentation does not state a divisible-by-ten charge rule; the toolkit does not refuse those amounts and shows Stripe's error if Stripe refuses a checkout. The check warns about the documented settlement-currency minimum where
|
|
92
|
-
known; Stripe decides the actual minimum after conversion and any payment-method maximum at checkout.
|
|
93
|
-
Errors name Stripe as the source. See [Stripe currencies](https://docs.stripe.com/currencies).
|
|
94
|
-
Zero-price Checkout orders are supported and can be revoked without a Stripe refund. Every amount setting accepts only nonnegative whole minor units (numbers or decimal numeric strings); a fractional amount stops the build, names the setting and says what to write. No amount is rounded. All amounts must fit JavaScript safe integer arithmetic. Homie does not impose its own price ceiling or fee floor.
|
|
95
|
-
|
|
96
|
-
`purchaseAttemptsPerMinute` defaults to 6 per account and `purchaseAttemptsPerAddressPerMinute` to 600 per
|
|
97
|
-
address. Both accept positive whole numbers (including decimal numeric strings). The larger address rate
|
|
98
|
-
allows households, schools and venues to share a connection; rotating addresses still hits the account rate.
|
|
99
|
-
`guestBuyersPerAddressPerHour` defaults to 600 new guest buyers per address per hour, allowing a venue or school to arrive together. It must be a positive whole number; 0 cannot disable it accidentally. The existing `PLAYER_LIMIT_DAILY` also applies.
|
|
100
|
-
Checkout, parent links and parent payments have separate counts, per Worker instance. Referral statements use
|
|
101
|
-
`REFERRAL_STATEMENTS_PER_MINUTE` (30 per address), `REFERRAL_STATEMENTS_GLOBAL_PER_MINUTE` (30 globally), and
|
|
102
|
-
`REFERRAL_STATEMENT_BYTES` (65536). Written values must be positive whole decimal numbers; invalid values produce a named settings error. Security
|
|
103
|
-
checks for origins, sessions, signed webhooks and request sizes remain. They protect the studio's money.
|
|
104
|
-
|
|
105
|
-
## In a game
|
|
77
|
+
## Game API and carts
|
|
106
78
|
|
|
107
79
|
```ts
|
|
108
80
|
import { createShop } from '@homie-rocks/studio/shop';
|
|
109
81
|
const shop = createShop();
|
|
110
|
-
await shop.ready;
|
|
111
|
-
|
|
112
|
-
shop.on('change', (
|
|
113
|
-
|
|
114
|
-
shop.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
The play shell around the game answers these calls (the game never sees a card, an email, an order or a
|
|
122
|
-
session). The room button's sheet has a **Shop** button too. A purchase opens Stripe's page in a new tab, the game
|
|
123
|
-
keeps running, and the shell looks again when the tab comes back: `change` fires once it is paid.
|
|
124
|
-
|
|
125
|
-
**Badges in rooms.** A player whose account owns a `badge:` key carries it on their seat: `net.players()` gives
|
|
126
|
-
`peer.badge` ("Supporter"), set by the studio's Worker from what the account owns when the player connects (a
|
|
127
|
-
hello can never claim one). Kids-server badges follow `policy.kidsServer`; AI seats have no purchases.
|
|
128
|
-
|
|
129
|
-
## The money path
|
|
130
|
-
|
|
82
|
+
await shop.ready;
|
|
83
|
+
shop.has('badge:studio');
|
|
84
|
+
shop.on('change', () => redraw());
|
|
85
|
+
shop.open(); // show the shop
|
|
86
|
+
shop.add('badge', 2); // quantity
|
|
87
|
+
shop.add('gift', 1, 500); // chosen tip amount in currency units
|
|
88
|
+
await shop.checkout(); // one Stripe Checkout Session
|
|
89
|
+
await shop.buy('badge'); // direct purchase stays one call
|
|
90
|
+
shop.cart(); shop.clear();
|
|
91
|
+
shop.used('badge:studio'); // used-item policy applies only if chosen
|
|
131
92
|
```
|
|
132
|
-
phone (signed in) ─ POST /api/shop/buy ─► studio Worker ─ Checkout Session (the studio's restricted key) ─► Stripe
|
|
133
|
-
◄──────────────── Stripe's hosted page (new tab): the player pays ─────────────────────────────────┘
|
|
134
|
-
Stripe ─ signed webhook ─► POST /api/shop/hook: order paid → entitlements → referral line (held)
|
|
135
|
-
homie.rocks: not involved at any step.
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
- The order row atomically reserves against the studio monthly cap before Stripe is called. Reservations are
|
|
139
|
-
released on confirmed expiry, failure, missing session or refund, or an explicit owner release, never merely because a webhook is late; the Checkout Session carries the order, the player and the
|
|
140
|
-
item in its metadata; the hook checks the session matches the order (player, price) before it grants anything.
|
|
141
|
-
- A sessionless reservation stops counting after its 31-minute creation window plus a one-minute margin.
|
|
142
|
-
A session with no final result, including a bank payment still processing, keeps counting even past expiry.
|
|
143
|
-
Shop GETs never call Stripe. With a cap configured, only a purchase refused by the atomic cap check waits
|
|
144
|
-
for reconciliation: up to three rows in parallel, each with a two-second timeout. Successful purchases continue
|
|
145
|
-
reconciliation in `waitUntil` (the shop has no scheduled handler). Rows are atomically claimed before reading
|
|
146
|
-
[Stripe's session and expanded payment](https://docs.stripe.com/api/checkout/sessions/retrieve).
|
|
147
|
-
Successful reads set a per-row interval of one minute (five minutes for processing bank payments),
|
|
148
|
-
growing with age to one hour. Failed reads retry after a short backoff of about one minute and log the order and reason. With more than three lost rows, a refusal may need another purchase attempt.
|
|
149
|
-
Paid counts in the month of [the Stripe charge's creation time](https://docs.stripe.com/api/charges/object#charge_object-created);
|
|
150
|
-
missing payment time, pending or unreachable keeps counting. Confirmed expiry or failure releases it.
|
|
151
|
-
Stripe's [404 resource_missing](https://docs.stripe.com/error-codes#resource-missing) means this key's account
|
|
152
|
-
has no such session, not that no payment can follow: the row stops reserving the cap and is shown as `missing` in the office.
|
|
153
|
-
The office lists unresolved orders first, with Next orders for more. Release reservation records the confirming
|
|
154
|
-
owner's account (or a fingerprint of the owner's office sign-in session) and time in the office-only note, and asks Stripe to
|
|
155
|
-
[expire an open Checkout Session](https://docs.stripe.com/api/checkout/sessions/expire). It does not refund a payment.
|
|
156
|
-
A verified payment still grants the item and counts toward spending, including after a missing mark or release,
|
|
157
|
-
and can be refunded from the office. Duplicate events change nothing. Players see their previous unfinished
|
|
158
|
-
order state, never the internal mark or note.
|
|
159
|
-
An owner release or missing mark can free space for another purchase before a late payment counts again;
|
|
160
|
-
new reservations still obey the configured cap. Test orders never count toward a live
|
|
161
|
-
cap. Free orders remain available if an owner lowers a cap below prior spending. Partial refunds keep the whole
|
|
162
|
-
price counted; full refunds and lost disputes release it. No spending SUM runs when the cap is absent.
|
|
163
|
-
Deploy applies `0010_shop_reservations.sql` and `0011_shop_statements.sql`; readiness names these migrations
|
|
164
|
-
if the required schema is absent.
|
|
165
|
-
- The webhook's `Stripe-Signature` is checked (HMAC-SHA256 of `<t>.<payload>` with the endpoint's `whsec_`
|
|
166
|
-
secret, within five minutes); each event id is handled once; a test event never touches a live shop.
|
|
167
|
-
- Paid: the entitlements are granted. Refunded: that one order's entitlements are revoked. A dispute: nothing
|
|
168
|
-
changes until it is decided; lost: that one item goes, as a refund would; won: it stays. **The account is never
|
|
169
|
-
locked or deleted over a dispute.**
|
|
170
|
-
- Receipts come from Stripe by email (the buyer types it on Stripe's page, never on the studio's). The account
|
|
171
|
-
page lists the player's orders and badges; `/api/player/export` includes them; deleting an account asks first
|
|
172
|
-
when it owns things, and keeps the order rows without the player.
|
|
173
|
-
- Each Checkout Session says beside the pay button that the item is delivered at once (the EU and UK withdrawal
|
|
174
|
-
acknowledgement) and the studio's configured refund terms. `policy.withdrawalAcknowledgement` is true in every preset, restoring the existing wording, and is editable. Stripe caps product names at 5000 characters, descriptions at 40000 and submit wording at 1200; the build reports these provider constraints without truncation.
|
|
175
93
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
The
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
## Testing without money
|
|
287
|
-
|
|
288
|
-
The kit's tests run the Worker against `stripe-mock` (Stripe's open-source API mock, which checks every parameter
|
|
289
|
-
against Stripe's own API description) and sign webhooks with a test secret. stripe-mock also takes every write
|
|
290
|
-
`shop catalog` lists, a checkout that names its Product, and the webhook the connect page makes. Stripe's MCP is
|
|
291
|
-
never called by the tests: `shop catalog --have` reads an answer in the shapes Stripe's API documents. `STRIPE_API_BASE` (a loopback address
|
|
292
|
-
only) points a Worker at such a mock; it is never honoured for anything but 127.0.0.1 or localhost.
|
|
94
|
+
The studio chooses where to place or open its shop. The shell verifies game messages and the Worker verifies
|
|
95
|
+
sessions, ownership, origins and payments. `GET /api/player/owns` includes quantity in each detail row.
|
|
96
|
+
The shop page has quantities, Add to cart, Remove and Checkout cart. The game API opens checkout in another tab, keeping the game and room running. If another tab cannot open, checkout uses the current tab and returns to the game or TV page after payment or cancellation. The cart stays in memory and is lost when its game page closes.
|
|
97
|
+
|
|
98
|
+
`POST /api/shop/buy` accepts `{ "lines": [{ "item": "badge", "quantity": 2 }] }` or
|
|
99
|
+
`{ "item": "badge" }`. Tip lines also take `amount`. Client-supplied fixed prices are ignored.
|
|
100
|
+
One order stores every line and its item snapshot. `/api/shop/mine` lists the lines;
|
|
101
|
+
`POST /api/shop/refund` takes `order` and optionally `line`. The office offers both.
|
|
102
|
+
|
|
103
|
+
## Connecting and operating
|
|
104
|
+
|
|
105
|
+
`homie-studio shop init --supporter` writes an example item and `SELLING.md`, without a policy, cap or refund
|
|
106
|
+
window. `shop check` validates settings; the build uses the same validation.
|
|
107
|
+
`shop connect` (MCP `stripe_login`) starts Stripe's official browser approval, sandbox/test first.
|
|
108
|
+
If Stripe CLI is missing, the result gives `npm install -g @stripe/cli@latest` for the AI to run.
|
|
109
|
+
The person never supplies a key to the AI. A studio-specific CLI profile avoids borrowing another studio's login.
|
|
110
|
+
If the studio is not deployed yet, approval is retained: deploy with `studio_deploy` (and
|
|
111
|
+
`cloudflare_login` first if needed), then rerun connect. Webhooks require the deployed HTTPS address.
|
|
112
|
+
|
|
113
|
+
**The default is keyless.** Connect syncs one Product and Price for each shop.json item and one
|
|
114
|
+
Payment Link for each paid item. Tip Prices use custom amounts; free orders stay local. It creates
|
|
115
|
+
an account webhook at `<site>/api/shop/hook`, captures its signing secret without displaying it,
|
|
116
|
+
and installs `STRIPE_WEBHOOK_SECRET` and `STRIPE_SHOP_LINKS` through Wrangler stdin. No Stripe
|
|
117
|
+
API key goes into the Worker. Repeat connect after editing shop.json: changed prices get new
|
|
118
|
+
Price versions, changed links get new versions, and removed objects are archived. Old paid orders
|
|
119
|
+
keep their frozen item definitions. Receipts contain no credentials. Unchanged runs create nothing.
|
|
120
|
+
|
|
121
|
+
With an existing account and deployed studio, the person approves Stripe CLI once; an already
|
|
122
|
+
connected profile needs zero new approvals. An MCP-only grant is separate and is never extracted.
|
|
123
|
+
The AI installs tooling, creates/syncs objects and stores the secret. New account verification,
|
|
124
|
+
activation, administrator access settings and Cloudflare authorization are separate prerequisites.
|
|
125
|
+
`shop connect --live` requires the owner's go-live request. `--renew` retries browser authorization.
|
|
126
|
+
Never request a key from the person for the default flow.
|
|
127
|
+
|
|
128
|
+
At purchase time, the Worker saves an opaque order reference and sends it as the link's
|
|
129
|
+
`client_reference_id`. The buyer confirms quantity or tip amount on Stripe. A signed snapshot
|
|
130
|
+
session must match the frozen link, revision, currency and allowed amount before granting. Reusing
|
|
131
|
+
a link creates a distinct order for each Session. Returns without a webhook never grant goods.
|
|
132
|
+
Refunds and disputes use durable financial snapshots in the existing meta table, without buyer/card
|
|
133
|
+
data. They tolerate retries and arrivals before the payment event. Fully successful refunds revoke
|
|
134
|
+
items; failed refunds restore the financial booking. A lost dispute revokes goods, never the account.
|
|
135
|
+
|
|
136
|
+
| Need | Keyless behavior / fuller connection |
|
|
137
|
+
|---|---|
|
|
138
|
+
| Several different items in one payment | Buy separately. Stripe supports static multi-item links (up to 20 fixed-price lines), but they are not an arbitrary buyer cart. Dynamic carts use the keyed path. |
|
|
139
|
+
| Reserve an optional spending allowance | Requires keyed Checkout; a reusable public link cannot atomically reserve this studio's local cap. No silent weakening of a configured cap. |
|
|
140
|
+
| Expire an abandoned session | Stripe controls the lifetime; deactivating a link prevents new sessions, not existing sessions. Custom expiry/cancellation requires the keyed path. |
|
|
141
|
+
| Refund in one tap / player self-refund | Owner uses Stripe Dashboard or asks their Stripe-connected AI. Signed events update books. Office shows guidance instead of a paid-order refund button. Free refunds remain local. |
|
|
142
|
+
| Lost webhook | Remains pending. Stripe retries; the AI can use `stripe events resend <event> --webhook-endpoint <endpoint>` (within Stripe's resend window). No grant from a browser return. Automatic provider-read reconciliation needs a key. |
|
|
143
|
+
| Free orders | Local, including free carts; no Stripe account, API key or webhook secret required. |
|
|
144
|
+
| Exact local eligibility/sale/repeat enforcement | Local checks run before redirect, but a public link can be reused. Sync deactivates removed links; strict pre-payment enforcement of previously issued links requires keyed sessions. |
|
|
145
|
+
| Managed Payments | Payment Links support managed_payments.enabled; connect uses it when the studio selects Stripe as seller, subject to Stripe eligibility and terms. |
|
|
146
|
+
| Currency display | Payment Links always enable Stripe Adaptive Pricing. Bookkeeping uses integration currency; presentment details describe what the customer saw. |
|
|
147
|
+
|
|
148
|
+
An existing `STRIPE_KEY` selects the fuller path automatically. Only if requested, explain:
|
|
149
|
+
“The fuller connection adds combined carts, spending limits, automatic recovery and refunds here.”
|
|
150
|
+
The explicit `shop connect --manual` path remains available, including `--live`, but requires the
|
|
151
|
+
owner's existing Dashboard key step; it does not satisfy one approval. Never choose it automatically.
|
|
152
|
+
Running default connect again saves links, removes the API key, and keeps signed-event fulfillment.
|
|
153
|
+
When switching to spending restrictions, the AI must deactivate previously issued Payment Links through
|
|
154
|
+
the approved CLI and resolve in-flight checkouts before relying on those restrictions; switching credentials
|
|
155
|
+
does not cancel old Stripe sessions. The runtime OAuth finding and sources are in `docs/stripe-connect-research.md` in the source checkout.
|
|
156
|
+
|
|
157
|
+
`shop`, `shop orders`, and `/_studio/office/shop` show readiness and sales. Stripe's optional MCP
|
|
158
|
+
is useful for owner-requested refunds and analytics; honor its approval links. Do not create a
|
|
159
|
+
secret-returning webhook through a conversation tool: the private connect process owns that call.
|
|
160
|
+
|
|
161
|
+
## Payment guarantees and schema
|
|
162
|
+
|
|
163
|
+
The schema step `0012_shop_lines.sql` follows the reservation and statement steps. Deploy applies it through
|
|
164
|
+
the existing migration mechanism. It adds tables and columns without changing any existing primary key or
|
|
165
|
+
column meaning. Released code keeps working after the step, including through rollback. The new Worker
|
|
166
|
+
without the step still reads owned items and refunds from the office; new sales wait for the named step.
|
|
167
|
+
|
|
168
|
+
A verified payment atomically grants all line snapshots and records the referral share on the order total.
|
|
169
|
+
Duplicate events, reconciliation and late payments grant exactly once, including after a missing mark,
|
|
170
|
+
owner release, expiry or failure. Refunds revoke only their own lines, retain other purchases of the same key,
|
|
171
|
+
and adjust the referral share with exact integer allocation. Stripe tax is read per line when applicable.
|
|
172
|
+
Snapshots preserve delivery after catalog edits; the schema step represents older orders as one line.
|
|
173
|
+
An unpaid older order has no historical item snapshot and needs its catalog definition to complete delivery.
|
|
174
|
+
A missing definition gives a named retryable response and office note; the office can refund the verified
|
|
175
|
+
payment. Older paid orders missing a grant are repaired once. An unknown older item is refunded by the office,
|
|
176
|
+
since its original kind cannot be recovered safely for player self-service.
|
|
177
|
+
A whole-order refund uses one Stripe refund for the remaining charge. Dashboard partial refunds reduce cap
|
|
178
|
+
usage and referral shares by their pre-tax portion, without assigning money to an unnamed line.
|
|
179
|
+
Refunds are rebuilt from [Stripe’s refunds list](https://docs.stripe.com/api/refunds/list). Only
|
|
180
|
+
[succeeded refunds](https://docs.stripe.com/api/refunds/object#refund_object-status) revoke items;
|
|
181
|
+
pending refunds keep them and failed refunds restore them. A named line loses only its own items;
|
|
182
|
+
an untagged Dashboard refund revokes items only when the whole order is refunded.
|
|
183
|
+
Unneeded signed events are acknowledged without recording. A refund or dispute naming an unrecorded payment
|
|
184
|
+
of this database recovers its Checkout Session first, regardless of event age.
|
|
185
|
+
|
|
186
|
+
**With an API key**, sessions reserve the optional cap until Stripe confirms an outcome. With the default lifetime, a sessionless
|
|
187
|
+
reservation ages out after 24 hours plus a one-minute margin; your `checkoutMinutes` changes that window.
|
|
188
|
+
[Stripe session lifetime](https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-expires_at)
|
|
189
|
+
and [expiring open sessions](https://docs.stripe.com/api/checkout/sessions/expire) define these provider bounds.
|
|
190
|
+
Replacing an open checkout expires only reservations at least a minute old; a named cancellation can expire immediately.
|
|
191
|
+
Unresolved orders become eligible for reconciliation after one minute, even with no cap. A buyer's next shop
|
|
192
|
+
or owned-items request schedules up to three reads off the response path, with atomic claims and backoff up
|
|
193
|
+
to an hour; an optional Worker cron invokes the same reconciliation across buyers. No cron is installed for you. To run it every five minutes, add `"triggers": { "crons": ["*/5 * * * *"] }` to the studio’s root `wrangler.jsonc`; deploy preserves its triggers.
|
|
194
|
+
With an API key, every recorded payment checks Stripe’s refunds and dispute state, including payments recovered by an early refund or dispute event. The thanks page can verify a payment directly with the studio's key. Webhook events require Stripe's signature.
|
|
195
|
+
Test and live books stay separate. Owner release attempts to expire an open session, records who released it
|
|
196
|
+
and when, and never prevents a later verified payment grant.
|
|
197
|
+
|
|
198
|
+
Test mode uses Stripe's test cards. Test a cart, its grants and individual refunds before choosing live mode.
|
|
199
|
+
|
|
200
|
+
Game and TV checkout use the same tab if a new tab is unavailable, then return to the page the buyer left.
|
|
201
|
+
During rollback, the released Worker grants only a cart’s first item until the new Worker repairs it; it
|
|
202
|
+
allows whole-cart player refunds even when a tip is included, and counts partially refunded carts at
|
|
203
|
+
the full amount toward a cap. The additive schema preserves its original rows and entitlement keys.
|