@homie-rocks/studio 0.37.1 → 0.38.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 +60 -0
- package/bin/homie-studio.mjs +7 -6
- package/lib/cloudflare.mjs +28 -3
- package/lib/dev.mjs +1 -1
- package/lib/parts-build.mjs +23 -4
- package/lib/parts-cli.mjs +54 -3
- package/lib/parts-purchase.mjs +174 -0
- package/lib/parts-store.mjs +50 -19
- package/lib/parts-tools.mjs +15 -12
- package/lib/parts-upload.mjs +43 -0
- package/lib/parts.mjs +49 -5
- package/lib/perf.mjs +17 -10
- package/lib/port-check.mjs +77 -18
- package/lib/purchase-links.mjs +42 -0
- package/lib/purchase-wallet.mjs +114 -0
- package/lib/rules-check-worker.mjs +15 -2
- package/lib/scaffold.mjs +25 -5
- package/lib/shoot.mjs +12 -6
- package/lib/shop-links.mjs +15 -6
- package/lib/shop.mjs +91 -21
- package/lib/stripe-connect.mjs +2 -2
- package/lib/template-history.json +85 -0
- package/netplay/NETPLAY.md +20 -6
- package/netplay/netplay.ts +3 -1
- package/package.json +12 -2
- package/parts/COMMERCIAL-LICENCE.md +21 -0
- package/parts/PARTS.md +238 -10
- package/port/probe.ts +2 -1
- package/rules/core.ts +3 -1
- package/rules/host.ts +3 -1
- package/rules/pack.ts +30 -3
- package/rules/view.ts +232 -40
- package/starters/coin-dash/lab.json +17 -0
- package/starters/coin-dash/src/rules.ts +3 -3
- package/starters/gem-rush/game.json +19 -4
- package/starters/gem-rush/map/main.json +50 -0
- package/starters/gem-rush/src/move.ts +23 -0
- package/starters/gem-rush/src/rules.ts +118 -0
- package/starters/gem-rush/src/view.ts +429 -0
- package/starters/gem-rush/tunables.json +98 -11
- package/worker/extensions.mjs +13 -0
- package/worker/index.mjs +3 -2
- package/worker/machine-payments.mjs +72 -0
- package/worker/office.mjs +4 -1
- package/worker/parts-protected.mjs +111 -0
- package/worker/parts-purchase-catalogue.mjs +34 -0
- package/worker/parts-resource.mjs +32 -0
- package/worker/parts-sale.mjs +59 -0
- package/worker/parts.mjs +24 -11
- package/worker/payment-capabilities.mjs +70 -0
- package/worker/payment-dry-exchange.mjs +30 -0
- package/worker/payment-manual-refund.mjs +40 -0
- package/worker/payment-policy.mjs +40 -0
- package/worker/payment-recovery.mjs +163 -0
- package/worker/payment-test-exchange.mjs +19 -0
- package/worker/purchase-core.mjs +140 -0
- package/worker/purchase-crypto.mjs +17 -0
- package/worker/purchase-delivery.mjs +108 -0
- package/worker/purchase-discovery.mjs +26 -0
- package/worker/purchase-hosted-readiness.mjs +16 -0
- package/worker/purchase-keys.mjs +20 -0
- package/worker/purchase-links.mjs +9 -0
- package/worker/purchase-machine.mjs +279 -0
- package/worker/purchase-mcp.mjs +42 -0
- package/worker/purchase-office-routes.mjs +17 -0
- package/worker/purchase-office-script.mjs +38 -0
- package/worker/purchase-office.mjs +35 -0
- package/worker/purchase-owner.mjs +243 -0
- package/worker/purchase-pricing.mjs +8 -0
- package/worker/purchase-problem.mjs +4 -0
- package/worker/purchase-reconciliation.mjs +582 -0
- package/worker/purchase-response.mjs +82 -0
- package/worker/purchase-routes.mjs +611 -0
- package/worker/purchase-schema.mjs +50 -0
- package/worker/purchase-store.mjs +148 -0
- package/worker/purchases.mjs +6 -0
- package/worker/resource-kinds.mjs +20 -0
- package/worker/selling/index.mjs +8 -0
- package/worker/selling/runtime.mjs +42 -0
- package/worker/settlement-journal.mjs +138 -0
- package/worker/shop-page.mjs +7 -4
- package/worker/shop-refunds.mjs +22 -0
- package/worker/shop-rules.mjs +1 -1
- package/worker/shop.mjs +18 -18
- package/worker/stripe.mjs +36 -3
- package/worker/version.mjs +1 -1
- package/starters/gem-rush/src/main.ts +0 -1087
package/parts/PARTS.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# Studio parts
|
|
2
2
|
|
|
3
|
-
Studios
|
|
4
|
-
making another game can use it. A creature from one game, a level from another, a bot brain from a third. That is
|
|
5
|
-
the whole idea. Without parts, a creator's AI has to come up with everything on its own.
|
|
6
|
-
|
|
7
|
-
Mashing up is not a command: it is making a game with parts from several games.
|
|
3
|
+
Studios share **parts**: reusable files another studio can find, license and install. A part may be a game mechanic, an app's quiz, menu board or waitlist screen, a music loop or complete track, or video. It can be free or priced. The licence says where it may be used, including a venue when the seller chooses that use.
|
|
8
4
|
|
|
9
5
|
## The rules
|
|
10
6
|
|
|
11
|
-
1. **
|
|
7
|
+
1. **The lifting command extracts a piece, never a whole game or app.** Music, video and app assets can be authored directly in a part folder. Remix, which handed over an entire game, is retired;
|
|
12
8
|
parts are what replaces it.
|
|
13
9
|
2. **A part can take any form** that suits the piece: code, assets, data, a JSON description, a tuned config, or a
|
|
14
10
|
mix. Form is not restricted.
|
|
@@ -89,11 +85,12 @@ named after it):
|
|
|
89
85
|
| Field | What it is |
|
|
90
86
|
| --- | --- |
|
|
91
87
|
| `id`, `name`, `kind`, `version`, `summary` | `version` is three numbers; a shared version never changes, so a change is a new version. `summary` is one sentence. |
|
|
92
|
-
| `kind` | `character`, `rig`, `clips`, `environment`, `effect`, `sound`, `ui`, `mechanic`, `shader`, `level-generator`, `bot-brain`, `audio-pack`, `set-piece`.
|
|
88
|
+
| `kind` | `character`, `rig`, `clips`, `environment`, `effect`, `sound`, `ui`, `mechanic`, `shader`, `level-generator`, `bot-brain`, `audio-pack`, `set-piece`. These are suggestions, not a whitelist. `app`, `music` and `video` are also suggested; any lowercase label of letters, digits and hyphens is accepted. |
|
|
93
89
|
| `license`, `attribution` | Needed to share: an SPDX identifier, and who a game using the part credits. |
|
|
94
90
|
| `share` | **Default `false`.** The part's own switch, and the only thing that decides whether it is shared. |
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
91
|
+
| `from` | The source and studio: existing `{game, studio}` stays valid; non-game work may use `{kind, id, studio}`. Written when a part is lifted out of a game. |
|
|
92
|
+
| `uses` | Optional intended uses: `game`, `app`, `venue`, `cause`, `music`, `video`; the licence governs permission. |
|
|
93
|
+
| App/media provenance | The source and studio: `game`, `app`, `music` or `video` with its id. Written when a part is lifted out of a game. |
|
|
97
94
|
| `entry` | For a part with code: the module a game imports. A part of assets or data has none. |
|
|
98
95
|
| `files` | Every file with its SHA-256 and size, **written by the tool, never by hand**. A file may carry `"rights"` (below). |
|
|
99
96
|
| `preview` | `page`: a self-contained HTML page shown in a sandboxed frame. `image`: a still. `aspect`: the frame's shape as `"W:H"` (whole numbers 1 to 32, no flatter than 3:1, no taller than 1:2; default `"16:9"`). `phoneAspect`: the shape when the frame is narrower than 600 px. |
|
|
@@ -169,6 +166,237 @@ build, whether or not the add named the game.
|
|
|
169
166
|
alone.
|
|
170
167
|
- The plumbing behind the tools is `homie-studio parts find | new | add | share | unshare | check`.
|
|
171
168
|
|
|
172
|
-
App parts use the same lift/import/credit paths as game parts. Music and video parts can start empty with the corresponding kind; add their selected files, licence and provenance before sharing. `uses` describes what they are for, not permission to use them: the licence controls that.
|
|
169
|
+
App parts use the same lift/import/credit paths as game parts. Music and video parts can start empty with the corresponding kind; add their selected files, licence and provenance before sharing. `uses` describes what they are for, not permission to use them: the licence controls that. Optional `sale` metadata adds pricing without changing the meaning of `share`.
|
|
173
170
|
|
|
174
171
|
For example: `homie-studio parts new welcome-queue --kind waitlist --uses app,venue`, or `parts new gala-loop --kind loop --uses venue,cause`. A source app is selected by the existing `--from <id>` and `--game <id>` options.
|
|
172
|
+
|
|
173
|
+
## Selling a part to another studio
|
|
174
|
+
|
|
175
|
+
A part can be free or have `sale` in its `part.json`. The seller chooses its price,
|
|
176
|
+
licence and refund terms. Homie takes no cut and sets no price or spending ceiling
|
|
177
|
+
for studio purchases. Existing player shop rules apply to players, not these licences.
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"license": "LicenseRef-Studio-Commercial",
|
|
182
|
+
"licenseTerms": "LICENSE.txt",
|
|
183
|
+
"sale": {
|
|
184
|
+
"amount": 2500,
|
|
185
|
+
"currency": "usd",
|
|
186
|
+
"billing": "one-time",
|
|
187
|
+
"scope": "studio",
|
|
188
|
+
"source": true,
|
|
189
|
+
"updates": "major",
|
|
190
|
+
"taxCode": "txcd_10000000",
|
|
191
|
+
"refund": "Contact the studio for the refund terms written in LICENSE.txt.",
|
|
192
|
+
"onRefund": "terminate",
|
|
193
|
+
"onExpiry": "retain",
|
|
194
|
+
"commercialUse": true,
|
|
195
|
+
"transferable": false,
|
|
196
|
+
"publicFiles": ["preview/demo.js"]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This fragment is added to a complete part; choose the tax code for the actual goods.
|
|
202
|
+
`LICENSE.txt` must contain the complete seller-chosen licence and refund terms and
|
|
203
|
+
identify each custom licence. `license` also accepts SPDX expressions (`MIT OR
|
|
204
|
+
Apache-2.0`, for example); the toolkit never chooses an alternative for the buyer.
|
|
205
|
+
`LicenseRef-…` identifies a custom commercial licence, not an SPDX listed licence.
|
|
206
|
+
The structured summary and licence text must agree; the text is what the parties read.
|
|
207
|
+
|
|
208
|
+
- `amount` is a positive integer in Stripe's currency minor units (JPY has no cents).
|
|
209
|
+
There is no toolkit price ceiling; Stripe's own supported amounts still apply.
|
|
210
|
+
- `billing`: `one-time`, `day`, `week`, `month`, `year` (the recurring intervals Stripe supports). Recurring sales use Stripe Billing, with
|
|
211
|
+
paid-through periods from paid invoices. Cancellation uses Stripe's customer portal.
|
|
212
|
+
- `scope` is the seller’s label for licensed use (for example `studio`, `game`, `seat`, `venue` or `app`). A game licence names the buyer's game. A seat
|
|
213
|
+
licence has a quantity; seat assignment and transfer follow the written terms.
|
|
214
|
+
- `source`, `commercialUse`, `transferable` are explicit booleans. Package only the
|
|
215
|
+
files being sold. `source: false` does not strip or obfuscate files automatically.
|
|
216
|
+
- `updates`: `none`, `major`, `all`. Existing purchases use their original update
|
|
217
|
+
promise. Later list prices and new-buyer terms never remove the purchased promise. Included updates remain licensed under the purchased terms.
|
|
218
|
+
- `refund` is the seller's policy in plain words; `onRefund` and `onExpiry` choose
|
|
219
|
+
`retain` or `terminate` for rights to already downloaded copies. Full refunds and
|
|
220
|
+
lost disputes stop new downloads. Partial refunds leave the licence active.
|
|
221
|
+
- `publicFiles` explicitly names preview dependencies that may be downloaded without
|
|
222
|
+
buying. The preview page, image and licence text are public automatically. A browser
|
|
223
|
+
demo exposes whatever code and assets it uses. Keep private source out of previews.
|
|
224
|
+
|
|
225
|
+
On an already deployed studio, ask the AI to connect the shop and sell the part. The AI runs `shop init` if needed, `shop connect`, configures private storage and shares the release. **One browser approval** connects Stripe when its CLI session is not already authorized; an existing session needs **zero** new approvals. Account onboarding and Cloudflare authorization are separate provider requirements. Products, Prices, Payment Links and the webhook are created through Stripe's official CLI. The webhook signing secret and an automatically generated purchase signing key go privately into the Worker. No person finds, copies or pastes either secret.
|
|
226
|
+
|
|
227
|
+
The default hosted fallback uses these Payment Links with **no Stripe API key in the Worker**. The hosted review records the accepted terms, then redirects to Stripe with an opaque order reference. A verified signed event binds the exact link, revision, currency, amount and mode before granting files. Lost events wait for Stripe redelivery; the owner's AI can resend them through Stripe's CLI. Refund in Stripe or ask that connected AI. Keyless retirement returns the subscriptions for that AI to cancel through Stripe; signed cancellation events let retirement finish without revoking paid-period downloads. The shop's signed refund snapshots update access; the Worker makes no Stripe API request. The AI can preconfigure any positive quantities in `parts/offers.json`, for example `{"camera":{"quantities":[1,5,20]}}`, then run `shop connect`; it creates a fixed link for each chosen quantity. The default is one unit. A quantity without a matching link needs that setup sync or the fuller Session connection, so the paid quantity always matches the approved quote. Signed paid invoices supply recurring access periods; invoice-payment events associate their refunds. A later billing period never removes the rights the seller promised for earlier downloads.
|
|
228
|
+
|
|
229
|
+
Paid releases use a **separate private R2 bucket and `PURCHASE_MEDIA` binding**. Never enable a public R2 domain for it. Deploy verifies the selling Worker before uploading private files and reads every uploaded hash back. No paid payload is copied into public static assets; the normal Worker cannot read this bucket. Technical file and request limits are security/resource safeguards, not spending limits.
|
|
230
|
+
|
|
231
|
+
### Which payment paths need credentials
|
|
232
|
+
|
|
233
|
+
| Path | Stripe API key in Worker? | Other requirements |
|
|
234
|
+
| --- | --- | --- |
|
|
235
|
+
| Hosted Payment Link fallback | **No** | CLI browser approval at setup; signed webhook; private signing key and storage |
|
|
236
|
+
| MPP Stripe shared payment tokens | **Yes** | Authenticated PaymentIntent creation, account profile and SPT eligibility |
|
|
237
|
+
| MPP over MCP | **Yes** | The same Stripe shared-token charge, carried by MCP |
|
|
238
|
+
| x402 Base recorded into Stripe | **Yes** | Authenticated transaction-verification PaymentIntent; live CDP facilitator credentials; Stripe-approved deposit address |
|
|
239
|
+
| Dynamic Checkout Sessions, direct refunds, provider-read recovery | **Yes** | Stripe API access for those operations |
|
|
240
|
+
|
|
241
|
+
Stripe's [MPP guide](https://docs.stripe.com/payments/machine/mpp) initializes the Stripe client with a secret credential. Its [x402 guide](https://docs.stripe.com/payments/machine/x402) separates the on-chain facilitator settlement from authenticated PaymentIntent recording. **x402 itself does not require a Stripe API key**; this supported path records money into the seller's Stripe books as Stripe documents. The keyless Payment Link path uses [signed webhooks](https://docs.stripe.com/webhooks) instead of provider reads. CLI OAuth is not exported as a Worker REST credential; see [the setup research](../../../docs/stripe-connect-research.md).
|
|
242
|
+
|
|
243
|
+
For a fuller connection, the AI operates Stripe's [key-creation page](https://docs.stripe.com/keys), selects the necessary permissions and clicks Copy, then runs `shop connect --from-clipboard` (`--live` for live mode). The local command privately reads the clipboard, verifies the key, makes the webhook and writes secrets to Wrangler stdin, then clears the clipboard. **The human only handles provider sign-in, verification or approval prompts**; their count is controlled by Stripe. Never ask the person to find or paste a key when browser control is available. No supported public API to mint a persistent Stripe REST credential from the CLI approval was established. On macOS this uses `pbpaste`/`pbcopy`, Windows its clipboard commands, and Linux Wayland `wl-paste`/`wl-copy`; other desktops require an equivalent private credential transport. The manual local form remains an explicit fallback, not the default instruction. Live CDP credentials likewise belong to the seller and should be transferred privately by the AI from the provider's setup; no Homie account is involved.
|
|
244
|
+
|
|
245
|
+
Turnstile is optional. If the studio configures it, the fallback verifies its hostname and action. The per-address Worker rate limiter remains configurable flood protection. Machine payment proofs are verified by the protocol libraries.
|
|
246
|
+
|
|
247
|
+
A release's bytes never change. `parts/offers.json` holds only price overrides and
|
|
248
|
+
availability choices. Other terms come from the release being offered. Each accepted
|
|
249
|
+
offer version stores the complete terms and price together. Earlier free copies cannot be recalled. `share: false` stops discovery
|
|
250
|
+
and new checkout after deploy; R2 keeps paid releases for existing buyers. Do not
|
|
251
|
+
remove purchased releases. Keep the packed files, D1 purchase records and signing key
|
|
252
|
+
in the seller's backups. Run shop connect again after adding paid parts to register the additional
|
|
253
|
+
subscription webhook events and permissions. Stripe's Dashboard must have its customer
|
|
254
|
+
portal enabled for subscription management; refunds and cancellation are separate.
|
|
255
|
+
|
|
256
|
+
## Buying and keeping the proof
|
|
257
|
+
|
|
258
|
+
Requires studio **0.36.0**. Discovery uses `/.well-known/api-catalog`, linking
|
|
259
|
+
`/parts/catalog.json`, `/purchases/keys.json`, the HTTP resource and MCP endpoints.
|
|
260
|
+
The seller's public page and catalogue contain schema.org Product and Offer descriptions.
|
|
261
|
+
|
|
262
|
+
The buyer sees the full offer and exact quote before approving payment. Set
|
|
263
|
+
`HOMIE_PARTS_WALLET` to the person's installed `link-cli` or `purl` client.
|
|
264
|
+
The tool calls the wallet, which owns all spending authority and limits. Link card spends
|
|
265
|
+
currently require approval in the Link app. Retry the same purchase after that approval.
|
|
266
|
+
A compatible funded crypto wallet can use its existing authority. No seller checkout
|
|
267
|
+
page is involved in a successful protocol exchange.
|
|
268
|
+
|
|
269
|
+
MPP uses Stripe shared payment tokens. Tempo is disabled. x402 exact uses Base
|
|
270
|
+
payments to the studio's Stripe deposit address. Both return files, a payment receipt
|
|
271
|
+
and signed purchase proof. MCP uses MPP's JSON-RPC challenge and receipt binding.
|
|
272
|
+
Checkout remains the human fallback, and handles subscriptions, non-USD prices and final
|
|
273
|
+
tax calculation. Managed Payments is a Checkout option; machine charges use the studio
|
|
274
|
+
as seller. In fallback Checkout the person accepts the licence and presses Pay; an
|
|
275
|
+
assistant must not submit acceptance with a browser tool.
|
|
276
|
+
|
|
277
|
+
`shop connect` obtains the profile and stablecoin deposit addresses from Stripe and writes
|
|
278
|
+
`PURCHASE_MACHINE_PAYMENTS` and a mode/key-bound `PURCHASE_PAYMENT_CAPABILITIES` record. It needs
|
|
279
|
+
**PaymentIntents Write**, Business Profiles access, Payment Method Configurations reads and
|
|
280
|
+
Crypto Deposit Addresses access, as well as the existing Checkout and webhook permissions.
|
|
281
|
+
The fuller connection reports Stripe readiness errors. Connect and scheduled checks read the
|
|
282
|
+
profile, enabled methods and existing deposit addresses. They create no provider objects.
|
|
283
|
+
These reads establish configuration, not PaymentIntents Write permission, funded settlement
|
|
284
|
+
or SPT eligibility. Real purchase outcomes update readiness. The scheduled check retries
|
|
285
|
+
failures with backoff and rechecks successful configuration every six hours.
|
|
286
|
+
Stablecoins still require Stripe approval; the office reports unavailable methods and the
|
|
287
|
+
catalogue lists the available paths. Live Base additionally needs the studio's own
|
|
288
|
+
`CDP_API_KEY_ID` and `CDP_API_KEY_SECRET`. The toolkit does not invent those credentials.
|
|
289
|
+
|
|
290
|
+
At the documentation check on 2026-10-08, the Link wallet supports US and Canadian wallet accounts and a $500 spend-request
|
|
291
|
+
limit. An x402 client's default spend control can be $1 per payment; the wallet owner decides
|
|
292
|
+
whether to change it. These are provider/client constraints, not studio pricing limits.
|
|
293
|
+
`part_add` also accepts a `wallet` argument. A failed token clears pending wallet approval. Explicit wallet refusals, missing wallet software,
|
|
294
|
+
unsupported methods and unavailable machine paths offer a clearly labelled hosted Checkout fallback. An uncertain settlement must be reconciled before paying again.
|
|
295
|
+
|
|
296
|
+
`/openapi.json`, linked as `service-desc` from the API catalogue, publishes each offer's
|
|
297
|
+
request schema and `x-payment-info`. Buyers can submit the published `offerVersion`.
|
|
298
|
+
Challenges bind to the server hostname and an individual purchase resource URL. Unpaid
|
|
299
|
+
machine orders and challenges last five minutes; reprice, withdrawal and retirement close
|
|
300
|
+
old unpaid offers. The limiter applies before protected R2 file reads too.
|
|
301
|
+
|
|
302
|
+
Verified stablecoin settlements grant access even while Stripe recording is unavailable.
|
|
303
|
+
`purchase_jobs` retains the recording parameters and transaction reference. A five-minute
|
|
304
|
+
cron and on-demand order refresh retry recordings and re-read processing PaymentIntents.
|
|
305
|
+
If paid files cannot be restored, the owner must refund the order manually. Maintenance never
|
|
306
|
+
refunds for a storage outage. Every five minutes it reads unresolved Stripe objects and retries
|
|
307
|
+
delivery availability. The owner can run the same recovery with
|
|
308
|
+
`POST /_studio/api/shop/reconcile-payments` and an optional `order` identifier; buyers retrying
|
|
309
|
+
their purchase also trigger this read. Existing payment facts and order identifiers are retained
|
|
310
|
+
for late webhooks. Authentication, permission and idempotency errors are not charge refusals.
|
|
311
|
+
After 23 hours, no charge is replayed. Reads continue: a paid object is recorded; a canceled or
|
|
312
|
+
unpaid object releases the claim. An exhausted complete list after the deadline also releases
|
|
313
|
+
the claim while keeping its order. If Stripe remains unreadable, the office shows the order,
|
|
314
|
+
request key, recorded PaymentIntent (if known), age and last error. Reconnect the key and use
|
|
315
|
+
Reconcile payments now. If you have the exact PaymentIntent id, use Read this payment beside
|
|
316
|
+
the recovery row (or pass `payment` and `order` to the same endpoint). Its order metadata must
|
|
317
|
+
match. Use the normal refund action once Stripe confirms payment; an unpaid canceled object
|
|
318
|
+
releases the claim. Never infer no charge from an API error.
|
|
319
|
+
|
|
320
|
+
An explicit sandbox readiness check is available through
|
|
321
|
+
`POST /_studio/api/shop/readiness`. It reads current configuration without creating any provider object, order, entitlement or refund. Failures expire after five minutes;
|
|
322
|
+
success expires after six hours. The report records the check time and its limited scope.
|
|
323
|
+
Scheduled capability checks are read-only, at most four successful checks per day, with
|
|
324
|
+
backoff after failure. No extra permissions or checks apply to a shop without paid releases.
|
|
325
|
+
|
|
326
|
+
Paid objects live under `paid-parts/` in the private `PURCHASE_MEDIA` bucket, separate from
|
|
327
|
+
public `MEDIA`. Deployment creates that bucket, deploys the guarded Worker, then uploads and
|
|
328
|
+
verifies the files. Failure before deployment uploads nothing. An older public Worker has no
|
|
329
|
+
binding to those objects. Public routes also refuse the prefix if it is bound accidentally.
|
|
330
|
+
|
|
331
|
+
The studio can tune `PURCHASE_RATE_LIMITER` in its Wrangler configuration; deployment keeps
|
|
332
|
+
that binding’s chosen limit and period. It is an abuse control, not a spending allowance.
|
|
333
|
+
|
|
334
|
+
Stripe can initiate Managed Payments refunds within 60 days in certain cases, including
|
|
335
|
+
support escalations left unanswered for 48 hours, and can accept disputes. A customer’s
|
|
336
|
+
data-deletion request cancels their Managed Payments subscriptions. These are Stripe’s
|
|
337
|
+
rules, described in its [Managed Payments guide](https://docs.stripe.com/payments/managed-payments/how-it-works).
|
|
338
|
+
Provider reconciliation observes refunds and cancellation regardless of who requested them.
|
|
339
|
+
|
|
340
|
+
## Seller settings and recovery
|
|
341
|
+
|
|
342
|
+
`part.json.sale` supplies terms for each immutable release. `parts offer <id> --amount`
|
|
343
|
+
sets only a persistent price override. Other terms change with a new release. Offers are
|
|
344
|
+
versioned from all terms, resource and price; a buyer keeps the version they purchased.
|
|
345
|
+
The build does not write `parts/offers.json`. Only the newest release sells by default;
|
|
346
|
+
`--release <version> --keep-on-sale` retains an older release. Release settings merge,
|
|
347
|
+
so adding an included upgrade does not silently withdraw an older release.
|
|
348
|
+
|
|
349
|
+
`--withdraw` closes new sales; `--on-sale` explicitly reopens an offer. Deployment expires
|
|
350
|
+
obsolete open Checkout sessions. A completed payment is honored even if its webhook
|
|
351
|
+
arrives after withdrawal or another release. `parts retire` closes sales and cancels
|
|
352
|
+
future subscription billing, preserving already-paid periods. `--upgrade included` may
|
|
353
|
+
add rights; changing it back cannot revoke promised or already-granted releases.
|
|
354
|
+
|
|
355
|
+
`intentMinutes` is 30–1440 and controls the approval intent before Checkout. Stripe chooses
|
|
356
|
+
the Checkout Session's default expiration, keeping retry parameters stable. Download
|
|
357
|
+
credentials last five minutes. Reconciliation happens once per grant rather than per file.
|
|
358
|
+
The studio can configure its abuse rate limit; it is not a spending cap.
|
|
359
|
+
|
|
360
|
+
`refundWindowDays` enables buyer self-service refunds when present and positive. It counts
|
|
361
|
+
from the current payment's paid timestamp, including renewals. Otherwise ask the seller.
|
|
362
|
+
`refundEndsSubscription` cancels future billing only after a successful full refund.
|
|
363
|
+
Pending or failed refunds keep billing intact. Credit-only invoices have no charge to refund.
|
|
364
|
+
`immediateDelivery: true` adds the consumer immediate-delivery acknowledgement beside Pay;
|
|
365
|
+
the seller chooses it to match its full licence and obligations.
|
|
366
|
+
|
|
367
|
+
A public test-card payment gets no protected files until the owner identifies the order
|
|
368
|
+
and runs `parts test-access <order>`. This authorizes that claim only while the shop remains
|
|
369
|
+
in test mode. Reissue needs new approval. Never approve an unknown test buyer. Use a second
|
|
370
|
+
studio for tests: one Worker has one Stripe key/mode, and switching a live shop to test
|
|
371
|
+
interrupts its live buyers. A grant or proof from test never becomes a live purchase.
|
|
372
|
+
|
|
373
|
+
Claims, download tokens and pristine backups live in `.homie/paid-parts`, outside public
|
|
374
|
+
source. Back them up privately. A long-lived Ed25519 proof cannot download; a short-lived
|
|
375
|
+
grant can. Updates derive from the purchased licence. A verified backup restores offline;
|
|
376
|
+
new delivery and updates require the seller to remain reachable. There is no project escrow.
|
|
377
|
+
|
|
378
|
+
If a claim is lost, `parts recover <seller>/<part> --order <order>` generates a new private
|
|
379
|
+
claim and public claim hash. Give the hash and Stripe receipt to the seller. The seller
|
|
380
|
+
matches the payer/receipt in Stripe and runs `parts reissue <order> --claim-hash <hash>
|
|
381
|
+
--receipt-verified`. Never pass `--receipt-verified` on the assistant's own word. Reissue
|
|
382
|
+
revokes the former claim and downloads. An order ID alone proves nothing. A healthy local
|
|
383
|
+
claim cannot be overwritten by recovery. Transfer requires the purchased terms to allow it.
|
|
384
|
+
|
|
385
|
+
`parts manage` creates a fresh Stripe portal session. Without a local record, a seller's
|
|
386
|
+
configured `purchasesPortalLogin` lets Stripe authenticate the buyer and provide billing and
|
|
387
|
+
receipt access for assisted recovery. Run refunds only when requested by the person.
|
|
388
|
+
|
|
389
|
+
Paid resource setup uses the `shop connect` page for `TURNSTILE_SITE_KEY` and
|
|
390
|
+
`TURNSTILE_SECRET` (hosted fallback), and `CDP_API_KEY_ID` and `CDP_API_KEY_SECRET`
|
|
391
|
+
(live Base). Machine and fallback readiness are independent. Base is an explicit choice
|
|
392
|
+
with manual refund responsibility; Tempo is disabled. If Stripe cannot record a settled
|
|
393
|
+
Base transfer, the office names the payer, token, atomic amount and transaction. Send any
|
|
394
|
+
owed refund from the studio's own wallet and use `shop refund <order> --manual-transaction
|
|
395
|
+
<hash>` to verify and record it after owner confirmation. Missing storage never authorizes
|
|
396
|
+
an automatic refund. Configure the studio's own Base RPC in purchase settings for recovery.
|
|
397
|
+
|
|
398
|
+
The recovery status `fulfilled` means the purchased files are available and the entitlement
|
|
399
|
+
has been issued; it is not proof that the buyer downloaded them. An unpaid challenge retains
|
|
400
|
+
a five-minute quote row so a retry uses the same terms and the owner can grant test access.
|
|
401
|
+
It creates no provider object. Cleanup removes that row only if no provider or settlement job
|
|
402
|
+
was ever attached. A paid order whose files cannot be restored needs an owner's manual refund.
|
package/port/probe.ts
CHANGED
|
@@ -123,7 +123,8 @@ export function exposePort(net: Netplay<unknown, unknown, unknown> | null, opts:
|
|
|
123
123
|
rows.push([t, me ? me.x : NaN, me ? me.y : NaN, b.right[0], b.right[1], b.up[0], b.up[1], m, busy]);
|
|
124
124
|
if (rows.length > RING) rows.splice(0, rows.length - RING);
|
|
125
125
|
};
|
|
126
|
-
|
|
126
|
+
// The position is sampled now; rAF may carry an earlier timestamp on a busy renderer.
|
|
127
|
+
const loop = (): void => { sample(performance.now()); requestAnimationFrame(loop); };
|
|
127
128
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(loop);
|
|
128
129
|
const probe: PortProbe = {
|
|
129
130
|
v: 1,
|
package/rules/core.ts
CHANGED
|
@@ -177,7 +177,7 @@ export interface Core {
|
|
|
177
177
|
* `noted`: told, as it happens, of a value the rules wrote that the runtime changed to make it fit, or dropped (pack.ts
|
|
178
178
|
* `Adjusted`; also `effect`, `think` and `decision`): the handler, what was done, the field's name and what was written.
|
|
179
179
|
*/
|
|
180
|
-
export function createCore(c: Compiled, opts: { observe?: (kind: string, handler: string, error?: string) => void; noted?: (kind: string, handler: string, what: string, at: string, written: string) => void; seed?: number; epoch?: number; restore?: SavedCore | null; restoreEpoch?: number; stage?: string; decisions?: boolean } = {}): Core {
|
|
180
|
+
export function createCore(c: Compiled, opts: { moved?: (kind: string, tick: number, input: Readonly<Record<string, unknown>>, before: import('./pack.ts').MoveBody, after: import('./pack.ts').MoveBody) => void; observe?: (kind: string, handler: string, error?: string) => void; noted?: (kind: string, handler: string, what: string, at: string, written: string) => void; seed?: number; epoch?: number; restore?: SavedCore | null; restoreEpoch?: number; stage?: string; decisions?: boolean } = {}): Core {
|
|
181
181
|
if (c.dims !== 2) throw new Error('this release runs rules with space.dims: 2; bodies with height (dims: 3) arrive in a later one');
|
|
182
182
|
const dims = c.dims;
|
|
183
183
|
const tickHz = c.settings.tickHz;
|
|
@@ -1002,6 +1002,7 @@ export function createCore(c: Compiled, opts: { observe?: (kind: string, handler
|
|
|
1002
1002
|
}
|
|
1003
1003
|
if (!k.move) continue;
|
|
1004
1004
|
const b = { pos: e.pos, vel: e.vel, heading: e.heading, grounded: e.grounded, motion: e.mself };
|
|
1005
|
+
const beforeMove = opts.moved ? { pos: e.pos, vel: e.vel, heading: e.heading, grounded: e.grounded, motion: { ...e.m } } : null;
|
|
1005
1006
|
moveRadius = body.radius;
|
|
1006
1007
|
run(k.name, 'move', e, 'move', () => (k.move as NonNullable<KindTable['move']>)(b, e.input, moveCtx), 'body');
|
|
1007
1008
|
// The runtime rounds to 32-bit floats, here and in the browser, so both step from exactly the same numbers.
|
|
@@ -1011,6 +1012,7 @@ export function createCore(c: Compiled, opts: { observe?: (kind: string, handler
|
|
|
1011
1012
|
naming('body', 'pos'); e.pos = clampIn(V(own(b, 'pos')), body.radius); naming('body', 'vel'); e.vel = V(own(b, 'vel')); naming('body', 'heading'); e.heading = dir(own(b, 'heading'), dims); naming();
|
|
1012
1013
|
G.note = null;
|
|
1013
1014
|
e.grounded = own(b, 'grounded') === true;
|
|
1015
|
+
if (beforeMove) opts.moved!(k.name, tick, e.input, beforeMove, { pos: e.pos, vel: e.vel, heading: e.heading, grounded: e.grounded, motion: { ...e.m } });
|
|
1014
1016
|
}
|
|
1015
1017
|
// Phase 2: for every entity, its commands and then its tick, starting from a different entity each tick.
|
|
1016
1018
|
const list = [...ents.values()];
|
package/rules/host.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface HostClock {
|
|
|
58
58
|
}
|
|
59
59
|
export interface HostOptions {
|
|
60
60
|
game: string;
|
|
61
|
+
/** The build check compares a movement step before any tick handler can change it. */
|
|
62
|
+
moved?: NonNullable<Parameters<typeof createCore>[1]>['moved'];
|
|
61
63
|
/** Told of every handler as it ends, and what it threw if it threw (core.ts `observe`): the build check listens. */
|
|
62
64
|
observe?(kind: string, handler: string, error?: string): void;
|
|
63
65
|
/** Told of a written value the runtime changed to make it fit (core.ts `noted`): the build check listens. */
|
|
@@ -226,7 +228,7 @@ export function createHost(o: HostOptions): Host {
|
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
if (saved && saved.v !== 1) throw new Error('this save was written by another version of the runtime');
|
|
229
|
-
const core = createCore(c, { observe: o.observe, noted: o.noted, seed: Math.floor(random() * 4294967296) >>> 0, epoch: saved ? undefined : (Math.floor(random() * 4294967295) >>> 0) + 1, restore: saved?.core ?? null, restoreEpoch: o.restoreEpoch, stage: o.stage, decisions: !o.check });
|
|
231
|
+
const core = createCore(c, { moved: o.moved, observe: o.observe, noted: o.noted, seed: Math.floor(random() * 4294967296) >>> 0, epoch: saved ? undefined : (Math.floor(random() * 4294967295) >>> 0) + 1, restore: saved?.core ?? null, restoreEpoch: o.restoreEpoch, stage: o.stage, decisions: !o.check });
|
|
230
232
|
let epoch = core.epoch;
|
|
231
233
|
if (saved && (o.startPaused ?? o.restoreEpoch !== undefined)) for (const body of core.bodies()) if (body.driver !== 'bot' && body.owner !== 'reserved') core.seatAway(body.seat, true);
|
|
232
234
|
let lastSaveAt = o.clock.now();
|
package/rules/pack.ts
CHANGED
|
@@ -263,12 +263,39 @@ export interface MoveBody { pos: Vec3; vel: Vec3; heading: Vec3; grounded: boole
|
|
|
263
263
|
*/
|
|
264
264
|
export function stepMove(fn: (body: MoveBody, input: unknown, ctx: unknown) => void, body: MoveBody, input: Readonly<Record<string, unknown>>, ctx: unknown, quota: number, motion: FieldList, dims: number, failed: (error: unknown) => void): MoveBody & { used: number } {
|
|
265
265
|
G.left = quota;
|
|
266
|
+
const bag = coerceFields(motion, body.motion, dims);
|
|
267
|
+
const target: Record<string, unknown> = {};
|
|
268
|
+
const work: Record<string, unknown> = {};
|
|
269
|
+
// Match core.typed: scalar writes round immediately; collections copy on first read and settle at the end.
|
|
270
|
+
for (const [name, fd] of motion) {
|
|
271
|
+
const held = (v: unknown): unknown => { naming('body.motion', name); const out = coerce(fd, v, dims); naming(); return out; };
|
|
272
|
+
Object.defineProperty(target, name, { enumerable: true,
|
|
273
|
+
get: () => {
|
|
274
|
+
if (!mutable(fd)) return bag[name];
|
|
275
|
+
if (work[name] === undefined) {
|
|
276
|
+
const size = fd.t === 'list' ? 1 + (bag[name] as unknown[]).length * cellsOf(fd.of as Field)
|
|
277
|
+
: fd.t === 'map' ? 1 + Object.keys(bag[name] as object).length * (1 + cellsOf(fd.of as Field)) : cellsOf(fd);
|
|
278
|
+
charge(6 * size); work[name] = thaw(fd, bag[name]);
|
|
279
|
+
}
|
|
280
|
+
return work[name];
|
|
281
|
+
},
|
|
282
|
+
set: (v: unknown) => {
|
|
283
|
+
charge(mutable(fd) ? 6 * est(fd, v) : cellsOf(fd) === 1 ? 3 : 6 * cellsOf(fd));
|
|
284
|
+
bag[name] = held(v); work[name] = undefined;
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
body.motion = Object.preventExtensions(target);
|
|
266
289
|
try { fn(body, input, ctx); } catch (error) { failed(error); }
|
|
290
|
+
try {
|
|
291
|
+
for (const [name, fd] of motion) if (work[name] !== undefined) {
|
|
292
|
+
charge(6 * est(fd, work[name])); naming('body.motion', name); bag[name] = coerce(fd, work[name], dims); naming();
|
|
293
|
+
}
|
|
294
|
+
} catch (error) { failed(error); }
|
|
267
295
|
const used = quota - (G.left > 0 ? G.left : 0);
|
|
268
296
|
G.left = Infinity;
|
|
269
|
-
naming('body', 'pos'); const pos = vec3(body.pos, dims); naming('body', 'vel'); const vel = vec3(body.vel, dims); naming('body', 'heading'); const heading = dir(body.heading, dims);
|
|
270
|
-
|
|
271
|
-
return { pos, vel, heading, grounded: body.grounded === true, motion: held, used };
|
|
297
|
+
naming('body', 'pos'); const pos = vec3(body.pos, dims); naming('body', 'vel'); const vel = vec3(body.vel, dims); naming('body', 'heading'); const heading = dir(body.heading, dims); naming();
|
|
298
|
+
return { pos, vel, heading, grounded: body.grounded === true, motion: thawFields(motion, bag), used };
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
/** How many values `pack` has packed since this was last set to 0: the core reads it to charge a tick for the state it sends. */
|