@homie-rocks/studio 0.56.0 → 0.58.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 +55 -1
- package/apps/APPS.md +9 -0
- package/apps/apps.ts +19 -1
- package/docs/studio-to-studio.md +248 -0
- package/docs/toolkit-place-patterns.md +16 -0
- package/functions/FUNCTIONS.md +9 -0
- package/lib/cloudflare.mjs +3 -0
- package/lib/needs-headless.mjs +1 -1
- package/lib/studios-keys.mjs +13 -0
- package/lib/template-history.json +84 -0
- package/netplay/NETPLAY.md +7 -0
- package/package.json +1 -1
- package/parts/README.md +28 -0
- package/parts/catalogue.json +236 -0
- package/parts/owner-action-checklist/LICENSE +202 -0
- package/parts/owner-action-checklist/NOTICE +3 -0
- package/parts/owner-action-checklist/README.md +68 -0
- package/parts/owner-action-checklist/part.json +95 -0
- package/parts/owner-action-checklist/preview/index.html +16 -0
- package/parts/owner-action-checklist/src/handler.mjs +44 -0
- package/parts/owner-action-checklist/src/index.mjs +96 -0
- package/parts/owner-action-checklist/src/style.css +3 -0
- package/parts/owner-action-checklist/tuning.json +3 -0
- package/parts/terrain-route-map/LICENSE +202 -0
- package/parts/terrain-route-map/NOTICE +3 -0
- package/parts/terrain-route-map/README.md +67 -0
- package/parts/terrain-route-map/THIRD-PARTY-LICENSE +21 -0
- package/parts/terrain-route-map/part.json +133 -0
- package/parts/terrain-route-map/preview/demo.js +4127 -0
- package/parts/terrain-route-map/preview/heightmap.png +0 -0
- package/parts/terrain-route-map/preview/index.html +1 -0
- package/parts/terrain-route-map/preview/main.mjs +4 -0
- package/parts/terrain-route-map/preview/route.json +1 -0
- package/parts/terrain-route-map/src/data.mjs +81 -0
- package/parts/terrain-route-map/src/index.mjs +131 -0
- package/parts/terrain-route-map/src/style.css +3 -0
- package/parts/terrain-route-map/tuning.json +5 -0
- package/tools/TOOLS.md +16 -0
- package/tools/tools.ts +15 -2
- package/worker/booking-availability.mjs +35 -0
- package/worker/booking-calendar.mjs +15 -2
- package/worker/booking-holds.mjs +2 -2
- package/worker/booking-resources.mjs +14 -6
- package/worker/build-id.mjs +1 -1
- package/worker/business-events.mjs +2 -1
- package/worker/mcp-store.mjs +3 -1
- package/worker/mcp-tools.mjs +25 -2
- package/worker/mcp.mjs +43 -8
- package/worker/room.mjs +29 -0
- package/worker/routes.mjs +1 -1
- package/worker/studios-auth.mjs +71 -0
- package/worker/studios-client.mjs +93 -0
- package/worker/studios-events.mjs +52 -0
- package/worker/studios-keys.mjs +54 -0
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,63 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [Unreleased]
|
|
21
|
+
|
|
22
|
+
## [0.58.0] - 2026-10-16
|
|
23
|
+
|
|
24
|
+
**Plugin 0.58.0**
|
|
25
|
+
|
|
26
|
+
Two new shared parts — an owner action checklist and a 3D terrain route map — and booking calendars that can skip or change single dates in a weekly series.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Two Apache-2.0 shared building blocks: an owner action checklist with visible consequences, one-tap confirmation, server-authorized undo and existing studio role/origin checks; and a three.js terrain route map with a local heightmap coastline, route tube, markers and phone/TV camera controls. Both include standard part manifests, catalogue entries, AI integration docs, self-contained previews and fast logic checks. No tile service is required.
|
|
31
|
+
- Weekly booking calendars can omit individual dates or override an occurrence's time, capacity and duration. Occurrence IDs stay stable when a start time changes; undefined overrides, off-series or invalid dates and ambiguous DST times are refused, and a date that already has reservations cannot be silently removed.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Shared terrain map: smooth high-resolution coast, lit terrain with cavity shading, animated depth-coloured water and foam, sky/haze, flowing elevated route, collision-aware leader labels, intro camera and measured-frame lite fallback. Owner checklist distinguishes pending, confirming, done and undone, announces actual consequences, labels irreversible actions and restores fresh pending actions after undo.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Public checklist proposals require per-action staff authorization, validate tokens, retain retry keys and use separate expiring undo grants. Terrain heightmaps restrict origins and PNG types and enforce byte/dimension budgets before decoding.
|
|
40
|
+
- The MCP Events workerd conformance test parses SSE frames across HTTP chunks, including combined event/heartbeat frames and split UTF-8 (the intermittent 0.57.0 publish-gate failure); production delivery and webhook formats are unchanged. PR CI adds a path-filtered Events/webhook gate.
|
|
41
|
+
- CI Chrome launches keep each caller's protocol timeout.
|
|
42
|
+
|
|
43
|
+
### Upgrade notes
|
|
44
|
+
|
|
45
|
+
- No migration and nothing to change. Studios use the new parts by asking their AI to add them from the catalogue; existing weekly calendars keep working, and dated exceptions are optional (see "Weekly calendar exceptions" in `packages/studio/docs/toolkit-place-patterns.md`).
|
|
46
|
+
|
|
47
|
+
## [0.57.0] - 2026-10-16
|
|
48
|
+
|
|
49
|
+
**Plugin 0.57.0** · [release-2026-10-16-studio-0.57.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-16-studio-0.57.0)
|
|
50
|
+
|
|
51
|
+
Studios can book at each other on behalf of a person, with proven studio identity and signed events, and friends joining a browser-hosted game at the same moment always get their seat.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Browser-hosted games retry connected players’ seat claims when the host’s roster has no player body for them. Friends joining while the host is still loading now become seated players when its world is ready, including games with older bundled netplay clients. Fixes the 0.56.0 regression where Kart Royale replicas stayed bots across rounds; the two-browser check retains its original early-join timing. Host-driven AI seats count as acknowledged; missing-seat retries use capped exponential backoff and stop after eight attempts with one diagnostic, avoiding repeated joins when a game has no available body.
|
|
56
|
+
- Studio ingress now limits requests by IP before discovery; failed JWKS fetches and unknown keys share an atomic 60-second per-origin fetch bound. Authentication errors are generic, with stage details only in owner audit logs. Signed events use the exact inbound path rule, honour disabled entries and reject non-object bodies with 400. Removed the unused v1-only webhook production dependency and documented exact manual key rotation steps through stdin.
|
|
57
|
+
|
|
58
|
+
- Studio-to-studio limits aggregate by proven origin, outbound destinations require explicit configuration, and remote results retain untrusted provenance. Asserted handles never identify local accounts.
|
|
59
|
+
- Availability allocates from a page snapshot in three queries instead of querying per slot, option and resource.
|
|
60
|
+
- Studio identity uses Ed25519 private-key JWT and origin-hosted JWKS; events use Standard Webhooks v1a with the same key. Deploy provisions private keys through stdin. Office Allow/Deny is stored in D1, denied by default, with origin/handle audit entries. Owners exchange no secrets.
|
|
61
|
+
- Studio events and business feed data share an 8,192-byte limit. Discovery and tokens are cached within each request; proof scripts use deploy URLs without reading Wrangler OAuth files.
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- Studio-to-studio tools use the official MCP SDK with bounded HTTPS transport, receiver-owned OAuth client credentials and a verified studio caller distinct from an asserted end-user handle.
|
|
66
|
+
- Booking availability returns paged schema.org offers and exact booking arguments, shared by AI tools and the existing booking choices UI.
|
|
67
|
+
- Signed cross-studio events reuse Standard Webhooks, per-sender allowlists and durable business-event receipts to trigger studio functions.
|
|
68
|
+
- Tools can call their own custom routes with the caller's identity and share opt-in Cloudflare Rate Limiting bindings.
|
|
69
|
+
|
|
70
|
+
### Upgrade notes
|
|
71
|
+
|
|
72
|
+
- Studios that call or accept calls from other studios list them in studio.json (outbound) and allow them in the office (inbound); the deploy provisions each studio's signing key — nothing is exchanged between owners. Existing OAuth browser connections, booking migrations and function delivery remain in use. No version bump or new migration. See `packages/studio/docs/studio-to-studio.md`.
|
|
73
|
+
|
|
20
74
|
## [0.56.0] - 2026-10-15
|
|
21
75
|
|
|
22
|
-
**Plugin 0.56.0**
|
|
76
|
+
**Plugin 0.56.0** · [release-2026-10-15-studio-0.56.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-15-studio-0.56.0)
|
|
23
77
|
|
|
24
78
|
Studios scale with hibernating rooms, add custom routes with built-in access checks, and deploy reliably with background queue handlers.
|
|
25
79
|
|
package/apps/APPS.md
CHANGED
|
@@ -339,3 +339,12 @@ compiler; run `npm install` in the studio first if it is missing. Review the dif
|
|
|
339
339
|
combined timer/jobs, hand-written routes and business-event mirrors need a semantic
|
|
340
340
|
migration to the APIs above; they are not automatically deleted. The owner's
|
|
341
341
|
words, historical evidence and unresolved integration work remain intact.
|
|
342
|
+
|
|
343
|
+
## Availability shared with an AI
|
|
344
|
+
|
|
345
|
+
`studio_availability` and `context.bookings.availability` return paged offers with
|
|
346
|
+
valid booking arguments, checking the existing slots, holds, options and resources.
|
|
347
|
+
Pass that result to `createBookingChoices({container,availability})`; `value()`
|
|
348
|
+
returns the selected offer's booking arguments for the same booking tool an AI
|
|
349
|
+
uses. Availability is a snapshot; authoritative holds still recheck capacity.
|
|
350
|
+
See [the convention](../docs/studio-to-studio.md#availability-convention-v).
|
package/apps/apps.ts
CHANGED
|
@@ -78,7 +78,25 @@ export function createAppContext() {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** Native accessible choices; submit value() to the same booking tool used by an AI. */
|
|
81
|
-
export
|
|
81
|
+
export interface BookingAvailability {
|
|
82
|
+
convention: 'studio.booking-availability/1';
|
|
83
|
+
bookingTool: string;
|
|
84
|
+
items: Array<{identifier: string; name?: string; availabilityStarts?: string; booking: {app: string; collection: string; slot: string; quantity: number; option?: string; resource?: string; addons?: Record<string,number>}}>;
|
|
85
|
+
next: string|null;
|
|
86
|
+
}
|
|
87
|
+
export function createBookingChoices({container,options=[],resources=[],availability}:{container:HTMLElement;options?:Array<{id:string;name:string}>;resources?:Array<{id:string;name:string}>;availability?:BookingAvailability}) {
|
|
88
|
+
if(availability){
|
|
89
|
+
if(availability.convention!=='studio.booking-availability/1')throw new Error('Unsupported booking availability convention');
|
|
90
|
+
const label=document.createElement('label');label.textContent='Available bookings';
|
|
91
|
+
const select=document.createElement('select');select.name='booking';
|
|
92
|
+
const empty=document.createElement('option');empty.value='';empty.textContent='Choose a booking';select.append(empty);
|
|
93
|
+
const bookings=availability.items.map(item=>structuredClone(item.booking));
|
|
94
|
+
for(const [index,item] of availability.items.entries()){
|
|
95
|
+
const option=document.createElement('option');option.value=String(index);option.textContent=[item.name??item.booking.slot,item.availabilityStarts].filter(Boolean).join(' · ');select.append(option);
|
|
96
|
+
}
|
|
97
|
+
label.append(select);container.append(label);
|
|
98
|
+
return {value:():Partial<BookingAvailability['items'][number]['booking']>=>select.value===''?{}:structuredClone(bookings[Number(select.value)]),dispose:()=>label.remove()};
|
|
99
|
+
}
|
|
82
100
|
const fields=new Map<string,HTMLSelectElement>();
|
|
83
101
|
const group=document.createElement('fieldset');
|
|
84
102
|
const legend=document.createElement('legend');legend.textContent='Choose an option';group.append(legend);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# One studio acting at another
|
|
2
|
+
|
|
3
|
+
A concierge can discover a venue, select a visit, reserve it for a person and tell
|
|
4
|
+
its host when they will arrive. Each studio runs its own tools, OAuth, database and
|
|
5
|
+
functions. The same mechanisms apply to a resort stay or any other collaboration.
|
|
6
|
+
|
|
7
|
+
## Client (t)
|
|
8
|
+
|
|
9
|
+
Tools and functions receive `context.studios`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const origin = 'https://venue.example';
|
|
13
|
+
const page = await ctx.studios.listTools(origin); // pass {cursor: page.nextCursor}
|
|
14
|
+
const available = await ctx.studios.call(origin, 'studio_availability', {
|
|
15
|
+
app: 'venue', collection: 'bookings'
|
|
16
|
+
});
|
|
17
|
+
if (available.isError) throw new Error('Availability could not be read');
|
|
18
|
+
// Inspect available.data.items, choose an offer, then:
|
|
19
|
+
const booked = await ctx.studios.call(origin, 'studio_book', {
|
|
20
|
+
...chosenOffer.booking, id: stableBookingId, expiresAt: Date.now() + 600000
|
|
21
|
+
}, {assertedHandle: 'guest-42'});
|
|
22
|
+
if (booked.isError) throw new Error('Reservation was refused');
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The official MCP TypeScript client owns initialization, Streamable HTTP/SSE,
|
|
26
|
+
OAuth discovery, client authentication, schema validation and protocol errors.
|
|
27
|
+
`call` returns `{source:{origin,authenticated},untrusted:true,isError,data?,content}`. Structured content is preserved; the
|
|
28
|
+
kit's legacy `untrusted-tool-data` envelope is decoded while retaining the untrusted marker and provenance. `listTools` and each description are also labelled untrusted. Before any `ctx.ai` call, wrap remote data and descriptions as untrusted input, preserving `source` and `untrusted`; never concatenate them into system instructions. Content and
|
|
29
|
+
`data` remain untrusted business data, never instructions or verified claims.
|
|
30
|
+
Transport failures reject; tool failures return `isError:true`. No automatic
|
|
31
|
+
booking or payment retries are introduced. Use stable business IDs for retries.
|
|
32
|
+
|
|
33
|
+
Each operation has a ten-second deadline including discovery, a 1 MiB response
|
|
34
|
+
limit before SDK parsing, and a 64 KiB argument limit. Origins must be bare HTTPS
|
|
35
|
+
origins on port 443 with public DNS names. IP literals (including IPv6, mapped and integer forms), localhost, .localhost, .local and .internal are refused unless that exact outbound entry explicitly sets `allowPrivate:true`. Config keys are normalized; duplicate normalized keys or mismatched `origin` fields fail. Never take an origin from model or user input without checking it against a list of permitted origins. All redirects are refused. Every fetch, including OAuth metadata and token
|
|
36
|
+
requests, must stay on the chosen origin. Credentials are issuer-pinned. A client
|
|
37
|
+
and its transport close after each operation; discovery and tokens are cached per origin within the request; tool listing is cursor-paged.
|
|
38
|
+
Keep the toolkit's `global_fetch_strictly_public` compatibility flag for
|
|
39
|
+
Worker-to-Worker HTTP calls. Every destination must be configured and delegated to the calling tool. Public anonymous calls require `anonymous:true` on that exact outbound entry. An unknown origin never falls back to anonymous. Studio code is trusted Worker
|
|
40
|
+
code: these safeguards are not a sandbox for imported JavaScript. Choose trusted
|
|
41
|
+
remote operators; this does not implement DNS address pinning.
|
|
42
|
+
|
|
43
|
+
## Studio identity and allowlists (u)
|
|
44
|
+
|
|
45
|
+
Each studio deploy generates its own Ed25519 key into the `STUDIO_SIGNING_KEY`
|
|
46
|
+
Worker secret via stdin. Private keys never appear in model output, source or
|
|
47
|
+
studio.json. Existing keys survive redeploys. Owners exchange nothing.
|
|
48
|
+
The caller publishes only public keys at `https://<origin>/.well-known/jwks.json`.
|
|
49
|
+
The receiver fetches that exact origin with bounded responses and no redirects.
|
|
50
|
+
|
|
51
|
+
B declares permitted capabilities:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{"mcp":{"studios":{"inbound":{"concierge":{
|
|
55
|
+
"origin":"https://concierge.example",
|
|
56
|
+
"tools":["studio_availability","studio_book"]
|
|
57
|
+
}}}}}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A delegates the outbound destination:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{"mcp":{"studios":{"outbound":{"https://venue.example":{
|
|
64
|
+
"tools":["plan_drive"]
|
|
65
|
+
}}}}}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
B's owner opens **Office → AI connections** and clicks **Allow** for the
|
|
69
|
+
configured calling origin. All start denied. Allow/Deny is stored in D1 and takes
|
|
70
|
+
effect on every dispatch without redeploying, including existing tokens. Tool and
|
|
71
|
+
event capability declarations remain in configuration. Audit entries show the
|
|
72
|
+
calling origin and the asserted handle, not a misleading local account name.
|
|
73
|
+
|
|
74
|
+
`/oauth/token` accepts `client_credentials` with RFC 7523 `private_key_jwt`,
|
|
75
|
+
`scope=studio`, and the receiver's `/mcp` resource. The client ID, issuer and subject
|
|
76
|
+
are the caller's canonical origin; audience is the exact receiver token endpoint.
|
|
77
|
+
Assertions have short expiry, `kid`, and a one-use `jti` checked atomically in D1.
|
|
78
|
+
The receiver accepts Ed25519/EdDSA keys. Five-minute opaque access tokens are hashed
|
|
79
|
+
in D1. Browser OAuth continues using the existing provider.
|
|
80
|
+
|
|
81
|
+
JWKS cache lifetime is five minutes. Failed fetches are cached for 60 seconds;
|
|
82
|
+
unknown `kid` refreshes are limited to once per origin per 60 seconds, including
|
|
83
|
+
concurrent requests. Token and studio-hook requests share a mandatory limit of
|
|
84
|
+
60 requests per IP per minute before key discovery. Throttled requests return
|
|
85
|
+
429 with `Retry-After: 60`. Authentication failures return the same generic 401;
|
|
86
|
+
only the owner-visible audit log records the failed stage.
|
|
87
|
+
|
|
88
|
+
### Manual key rotation
|
|
89
|
+
|
|
90
|
+
Rotation is manual today. From the studio's `site/` directory (the directory with
|
|
91
|
+
its deployed `wrangler.jsonc`), run the following with the deployed origin in
|
|
92
|
+
`STUDIO_ORIGIN`. These commands fetch only public keys; the new private key stays
|
|
93
|
+
in the Node process and Wrangler stdin. Do not enable shell tracing or print the
|
|
94
|
+
bulk input. Use the same Wrangler environment/account as the existing deployment.
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
export STUDIO_ORIGIN=https://your-studio.example
|
|
98
|
+
node --input-type=module <<'NODE' | npx wrangler secret bulk
|
|
99
|
+
import {generateKeyPairSync, randomUUID} from 'node:crypto';
|
|
100
|
+
const response = await fetch(new URL('/.well-known/jwks.json', process.env.STUDIO_ORIGIN), {redirect:'error'});
|
|
101
|
+
if (!response.ok) throw new Error('Cannot read existing public keys; rotation aborted');
|
|
102
|
+
const jwks = await response.json();
|
|
103
|
+
if (!Array.isArray(jwks.keys) || !jwks.keys.length || jwks.keys.length > 7 ||
|
|
104
|
+
jwks.keys.some(k => !k || k.d || k.kty !== 'OKP' || k.crv !== 'Ed25519' || !k.x || !k.kid))
|
|
105
|
+
throw new Error('Invalid or too many retained public keys; rotation aborted');
|
|
106
|
+
const previous = jwks.keys.map(({kty,crv,x,kid}) => ({kty,crv,x,kid,alg:'EdDSA',use:'sig'}));
|
|
107
|
+
const {privateKey} = generateKeyPairSync('ed25519');
|
|
108
|
+
const next = {...privateKey.export({format:'jwk'}), kid:randomUUID()};
|
|
109
|
+
process.stdout.write(JSON.stringify({
|
|
110
|
+
STUDIO_PREVIOUS_PUBLIC_KEYS: JSON.stringify(previous),
|
|
111
|
+
STUDIO_SIGNING_KEY: JSON.stringify(next)
|
|
112
|
+
}));
|
|
113
|
+
NODE
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
1. Confirm Wrangler reports that **both** secrets were updated. If the command
|
|
117
|
+
fails, inspect the public JWKS and Wrangler deployment status before retrying.
|
|
118
|
+
2. Check `curl --fail "$STUDIO_ORIGIN/.well-known/jwks.json"`: it must publish the
|
|
119
|
+
new `kid` and the previous public keys, with no `d` field. Peer caches may take
|
|
120
|
+
up to 60 seconds to refresh for the new `kid`; retry failed operations with
|
|
121
|
+
their original stable business/event IDs.
|
|
122
|
+
3. Retain the old public keys for at least ten minutes after successful rotation.
|
|
123
|
+
Then, provided no other rotation is still in its overlap period, remove them:
|
|
124
|
+
`printf '%s' '[]' | npx wrangler secret put STUDIO_PREVIOUS_PUBLIC_KEYS`.
|
|
125
|
+
4. Check the public JWKS again: only the new public key should remain. Existing
|
|
126
|
+
signing keys survive future normal deploys. Deny the origin immediately if a
|
|
127
|
+
key is compromised; rotation overlap is not emergency revocation.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
ctx.caller.studio // {origin:'https://concierge.example', verified:true,
|
|
131
|
+
// method:'private_key_jwt'}
|
|
132
|
+
ctx.caller.actingFor // {assertedHandle:'guest-42', verified:false,
|
|
133
|
+
// assertedBy:'https://concierge.example'}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Verified means the caller proved possession of a key published by that HTTPS
|
|
137
|
+
origin. It does not verify any person. `assertedHandle` permits only
|
|
138
|
+
`[A-Za-z0-9._@-]`, 1–128 characters. Never match it to a local account or grant
|
|
139
|
+
roles from it. It only separates bookings within the calling studio. A studio can
|
|
140
|
+
assert any handle within its own origin; all handles share origin-level rate
|
|
141
|
+
limits. Studio callers use only allowlisted public tools, never signed-in roles.
|
|
142
|
+
|
|
143
|
+
References: [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) and the
|
|
144
|
+
[MCP client credentials extension](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/oauth-client-credentials.mdx).
|
|
145
|
+
|
|
146
|
+
## Availability convention (v)
|
|
147
|
+
|
|
148
|
+
`studio_availability({app,collection,after?,quantity?,option?,resource?,addons?})`
|
|
149
|
+
and `ctx.bookings.availability(...)` return:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{"convention":"studio.booking-availability/1","bookingTool":"studio_book",
|
|
153
|
+
"items":[{"@type":"Offer","identifier":"[\"morning\",null]",
|
|
154
|
+
"availability":"https://schema.org/InStock","remaining":2,
|
|
155
|
+
"availabilityStarts":"2030-10-15T10:00:00Z",
|
|
156
|
+
"availabilityEnds":"2030-10-15T11:00:00Z",
|
|
157
|
+
"booking":{"app":"venue","collection":"bookings","slot":"morning","quantity":1}}],
|
|
158
|
+
"next":null}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The snapshot loads a page of slots, resource records and active allocations in three queries regardless of slot × option count (plus access-policy checks), and uses the same
|
|
162
|
+
resource/option allocator as `studio_book`. It checks app visibility and slot-read
|
|
163
|
+
grants. A page scans at most 50 slots and supports at most 64 alternatives per
|
|
164
|
+
slot; `next` can be non-null even when no offers on this page are available.
|
|
165
|
+
Only feasible options are returned. Add-ons and preferred resources can be checked
|
|
166
|
+
as query inputs. No booking is taken during discovery. Holds still enforce capacity
|
|
167
|
+
atomically; concurrent clients may see the same offer and only one may book.
|
|
168
|
+
Expired holds continue occupying capacity until the existing expiry function
|
|
169
|
+
releases them, matching the authoritative hold path.
|
|
170
|
+
|
|
171
|
+
The existing `createBookingChoices({container,availability})` UI part accepts this
|
|
172
|
+
same result and returns the selected offer's exact booking arguments from `value()`.
|
|
173
|
+
Its existing explicit `options`/`resources` mode remains available. Share custom
|
|
174
|
+
booking parts with an availability tool returning this convention and name their
|
|
175
|
+
booking tool in `bookingTool`.
|
|
176
|
+
|
|
177
|
+
[Schema.org Offer](https://schema.org/Offer) supplies `availability`, `identifier`
|
|
178
|
+
and availability dates. A successful hold corresponds to a
|
|
179
|
+
[Reservation](https://schema.org/Reservation): its `id` maps to `reservationId`,
|
|
180
|
+
`state` to reservation status, and the chosen slot to `reservationFor`. The
|
|
181
|
+
kit keeps its existing versioned hold response and payment lifecycle. Local
|
|
182
|
+
`booking`, `remaining`, pagination and convention fields are explicit extensions;
|
|
183
|
+
this is not a claim that schema.org defines a booking API.
|
|
184
|
+
|
|
185
|
+
The [Agentic Commerce Protocol](https://github.com/agentic-commerce-protocol/agentic-commerce-protocol)
|
|
186
|
+
standardizes commerce/checkout interactions. Its separation of available offers
|
|
187
|
+
from authoritative checkout is compatible with this convention, but it does not
|
|
188
|
+
supply this toolkit's time-slot and multi-resource allocation schema. Availability
|
|
189
|
+
does not invent checkout, represent a payment approval, or claim ACP conformance.
|
|
190
|
+
|
|
191
|
+
## Signed arrival events (o)
|
|
192
|
+
|
|
193
|
+
Use Standard Webhooks asymmetric `v1a` Ed25519 signatures with the same studio key and the envelope `{eventId,name,timestamp,source,data}`. On A's outbound destination, add:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{"eventPath":"/hooks/studios/concierge","events":["arrival"]}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
On B's inbound `concierge` entry, add
|
|
200
|
+
`"events":{"arrival":"arrivals"}`. No shared event secret is needed. B's `mcp.events.arrivals` declares `event:"guest.arriving"`, an audience,
|
|
201
|
+
and a schema for `{sender:{origin,verified:true},data:{booking,minutes}}`.
|
|
202
|
+
Only mapped names can publish to local feeds. Define a function with
|
|
203
|
+
`event:'guest.arriving'` to consume `event.data.data` and its verified
|
|
204
|
+
`event.data.sender`. The sender cannot select an arbitrary local event type.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
await ctx.studios.send('https://venue.example', 'arrival', {
|
|
208
|
+
id: stableArrivalId, data: {booking: bookingId, minutes: 10}
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The JavaScript `standardwebhooks` package currently supports HMAC `v1` only, so
|
|
213
|
+
studio `v1a` uses Web Crypto directly, with fixed cryptographic and spec-framing
|
|
214
|
+
regression vectors. The receiver verifies Standard Webhooks `id.timestamp.body` signatures with Web Crypto Ed25519, including a five-minute timestamp window. Both studio event envelopes and business-feed data are limited to **8,192 UTF-8 bytes**; oversized events return a clear 413 error. `webhook-key-id` selects the origin JWKS key; `webhook-origin` must match the configured sender and signed body. IDs are 1–128 letters,
|
|
215
|
+
digits, underscores or hyphens. A receipt is committed atomically with the event
|
|
216
|
+
using existing business-event publication. Identical retries return 204 without
|
|
217
|
+
running the function again; a signed changed payload under the same ID returns
|
|
218
|
+
409. Receipts survive event pruning. Function execution remains at least once:
|
|
219
|
+
handlers must deduplicate external effects by event ID. If acceptance fails, the
|
|
220
|
+
sender gets an error and can retry the same ID. Calls to `send` do not create an
|
|
221
|
+
outbound retry queue; call from an existing durable function for automatic retries.
|
|
222
|
+
The existing configured-feed/MCP Events subscription dispatcher remains available
|
|
223
|
+
for subscription delivery. No new event bus or MCP method is introduced.
|
|
224
|
+
|
|
225
|
+
## Friend routes and rate limits (p, q)
|
|
226
|
+
|
|
227
|
+
`await ctx.fetchSelf('/api/friend-action', {method:'POST', body:...})` invokes the
|
|
228
|
+
studio's registered `useRoutes` handler in-process. It carries the caller through
|
|
229
|
+
the existing internal identity mechanism. `isOwner`, `hasRole`, and the studio's
|
|
230
|
+
player lookup still perform their normal checks. Explicit cookies/Authorization
|
|
231
|
+
headers, URL credentials and other origins are refused. A missing custom route
|
|
232
|
+
returns 404. `fetchSelf` sets `Origin` to the local studio origin, so a custom route’s same-origin/CSRF check will pass; that check is not authorization. Routes must still check the propagated identity and roles. It does not borrow `fetchWithSecret` or mint an owner credential.
|
|
233
|
+
|
|
234
|
+
`await ctx.rateLimit('friends')` uses a declared Cloudflare Rate Limiting binding,
|
|
235
|
+
sharing the same namespace and account key between delegated tools:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{"mcp":{"rateLimits":{"friends":{"binding":"FRIEND_RATE","tools":["join","reserve"]}}}}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
In wrangler.jsonc, configure `ratelimits` with a stable `namespace_id`, name
|
|
242
|
+
`FRIEND_RATE` and `simple:{limit:30,period:60}`. The helper returns `{success}`;
|
|
243
|
+
the tool chooses its refusal response. No policy is enabled by default. Signed-in
|
|
244
|
+
callers default to their identity key; studio callers default to `studio:<origin>` regardless of asserted handle. Public MCP callers expose `ctx.caller.clientAddressKey` and use that address-based key by default. Do not let customers select keys to bypass your policy.
|
|
245
|
+
[Cloudflare's binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/)
|
|
246
|
+
is an approximate per-location limiter, not a globally strict quota or booking
|
|
247
|
+
capacity mechanism. Existing authoritative booking/database checks remain necessary.
|
|
248
|
+
|
|
@@ -66,3 +66,19 @@ Tokens are bound to this origin, limited to the exact named tools, expire after
|
|
|
66
66
|
## Deployment
|
|
67
67
|
|
|
68
68
|
Cloudflare's official MCP request interface cannot authorize new static-asset uploads with an upload JWT; deploy with Wrangler, Cloudflare's own CLI using browser OAuth.
|
|
69
|
+
|
|
70
|
+
## Weekly calendar exceptions
|
|
71
|
+
|
|
72
|
+
`calendarSlots` (also exposed by `ctx.bookings.calendar`) accepts `exceptions`,
|
|
73
|
+
keyed by local `YYYY-MM-DD`. Use `null` to omit a holiday or an object containing
|
|
74
|
+
`time`, `capacity` and/or `durationMinutes` to override that occurrence. Dates
|
|
75
|
+
must belong to the declared range and weekdays. The occurrence ID remains based
|
|
76
|
+
on the original series time when an exception changes its start time. DST gaps
|
|
77
|
+
and ambiguous times refuse rather than silently changing the booking. Publishing
|
|
78
|
+
new calendar data does not itself move existing reservations or change payments.
|
|
79
|
+
|
|
80
|
+
A `durationMinutes` exception replaces the series `days` duration for that date.
|
|
81
|
+
Explicit `undefined` override values are invalid. Removing a published slot with
|
|
82
|
+
active reservations is refused by the database (`slot has active bookings`).
|
|
83
|
+
Release or cancel those reservations through the payment-aware booking flow before
|
|
84
|
+
deleting the slot; omitting a generated date alone does not delete a published row.
|
package/functions/FUNCTIONS.md
CHANGED
|
@@ -424,3 +424,12 @@ or declared webhook; functions never send email/SMS themselves.
|
|
|
424
424
|
Apply `0028_business_events.sql` on upgrade. Local proof publications carry their
|
|
425
425
|
scope and do not appear in production feed reads. Event feeds retain their declared
|
|
426
426
|
type; give different business feeds distinct types when their content differs.
|
|
427
|
+
|
|
428
|
+
## Events from another studio
|
|
429
|
+
|
|
430
|
+
An explicitly allowed studio can send a Standard Webhooks arrival event to
|
|
431
|
+
`/hooks/studios/<connection>`. The receiver maps allowed names to declared business
|
|
432
|
+
feeds and commits their existing durable receipt before acknowledging. Functions
|
|
433
|
+
receive the verified sender origin separately from the sender's untrusted data.
|
|
434
|
+
Use `context.studios.send` inside a durable function when sending needs retries.
|
|
435
|
+
See [studio-to-studio](../docs/studio-to-studio.md) for setup and replay semantics.
|
package/lib/cloudflare.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {ensureStudioSigningKey} from './studios-keys.mjs';
|
|
1
2
|
import {redactText} from './redact.mjs';
|
|
2
3
|
import {enableReadReplication} from './read-replication.mjs';
|
|
3
4
|
import {ensureBackgroundQueues,backgroundQueues} from './background-queue.mjs';
|
|
@@ -440,6 +441,7 @@ async function deployLocked(root, { log = () => {}, homie, fetchFn = null, ownRo
|
|
|
440
441
|
dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
|
|
441
442
|
}
|
|
442
443
|
if (dep.code !== 0) return refuse(`wrangler deploy failed: ${wranglerDeployError(dep.out)}`, dep.out);
|
|
444
|
+
try{await ensureStudioSigningKey(readStudio(root),w);}catch(error){return refuse(error.message);}
|
|
443
445
|
if (paidReleases(root).length) {
|
|
444
446
|
const bin = wranglerBin(root);
|
|
445
447
|
const uploadOptions = { cwd: workerDir(root), env: { ...process.env, CLOUDFLARE_ACCOUNT_ID: accountId, WRANGLER_SEND_METRICS: 'false', CI: '1' } };
|
|
@@ -664,6 +666,7 @@ export async function ciDeploy(root, { log = () => {}, wrangler = null } = {}) {
|
|
|
664
666
|
const started = Date.now();
|
|
665
667
|
const dep = w(['deploy', ...(workerEntry(root) ? [workerEntry(root)] : []), ...repoVar(root)]);
|
|
666
668
|
if (dep.code !== 0) return refuse(`wrangler deploy failed: ${wranglerDeployError(dep.out)}`, dep.out);
|
|
669
|
+
try{await ensureStudioSigningKey(readStudio(root),w);}catch(error){return refuse(error.message);}
|
|
667
670
|
step(`deployed ${names.worker ?? 'the Worker'} in ${Math.round((Date.now() - started) / 1000)} s`);
|
|
668
671
|
const repo = repoOf(root);
|
|
669
672
|
step(repo ? `the live site knows its GitHub repository (${repo}), for the Claude app's hand-off` : 'no GitHub repository found here (no HOMIE_REPO, studio.json "github" or git remote): the Claude app\'s card asks the person which it is');
|
package/lib/needs-headless.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export function needsHeadless({url,secret,app,meta,scope,sessionFor}) {
|
|
|
26
26
|
if(!call?.tool)throw new Error('MCP step requires tool and optional arguments');
|
|
27
27
|
const result=await client.callTool({name:call.tool,arguments:call.arguments??{}},undefined,{timeout:item.schedule?15000:10000});
|
|
28
28
|
if(result.isError)throw new Error(result.content?.filter(c=>c.type==='text').map(c=>c.text).join('; ')||'MCP tool failed');
|
|
29
|
-
if(result.structuredContent!==undefined)
|
|
29
|
+
if(result.structuredContent!==undefined){const value=result.structuredContent;return value?.kind==='untrusted-tool-data'?value.data:value;}
|
|
30
30
|
const content=result.content?.filter(c=>c.type==='text');
|
|
31
31
|
if(content?.length===1) {try {const parsed=JSON.parse(content[0].text);return parsed.kind==='untrusted-tool-data'?parsed.data:parsed;}catch{}}
|
|
32
32
|
return result;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {generateKeyPairSync,randomUUID} from 'node:crypto';
|
|
2
|
+
/** Key material travels only in memory and Wrangler stdin. Never return it or include command output in errors. */
|
|
3
|
+
export async function ensureStudioSigningKey(studio,w){
|
|
4
|
+
if(!studio.mcp?.studios)return;
|
|
5
|
+
const result=await w(['secret','list']);
|
|
6
|
+
if(result.code!==0)throw new Error('Cannot inspect studio signing key bindings');
|
|
7
|
+
let names;try{names=JSON.parse(result.stdout??result.out);}catch{throw new Error('Cannot inspect studio signing key bindings');}
|
|
8
|
+
if(names.some(entry=>entry.name==='STUDIO_SIGNING_KEY'))return;
|
|
9
|
+
const {privateKey}=generateKeyPairSync('ed25519');
|
|
10
|
+
const key={...privateKey.export({format:'jwk'}),kid:randomUUID()};
|
|
11
|
+
const saved=await w(['secret','bulk'],{input:JSON.stringify({STUDIO_SIGNING_KEY:JSON.stringify(key)})});
|
|
12
|
+
if(saved.code!==0)throw new Error('Cannot provision studio signing key');
|
|
13
|
+
}
|
|
@@ -4092,6 +4092,90 @@
|
|
|
4092
4092
|
}
|
|
4093
4093
|
}
|
|
4094
4094
|
}
|
|
4095
|
+
},
|
|
4096
|
+
"0.57.0": {
|
|
4097
|
+
"files": {
|
|
4098
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4099
|
+
"README.md": "684b3cca8001673c",
|
|
4100
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4101
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4102
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4103
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4104
|
+
"posts/README.md": "37841aae476bd540",
|
|
4105
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4106
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4107
|
+
},
|
|
4108
|
+
"sections": {
|
|
4109
|
+
"AGENTS.md": {
|
|
4110
|
+
"(intro)": "af7f2f732505d379",
|
|
4111
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4112
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4113
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
4114
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
4115
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4116
|
+
"## Making games": "bb715272fd1a615b",
|
|
4117
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4118
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4119
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4120
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
4121
|
+
"## Selling things (the shop)": "0df5a5fe1833c624",
|
|
4122
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4123
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4124
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4125
|
+
"## Beta": "9d02638207a26e73"
|
|
4126
|
+
}
|
|
4127
|
+
},
|
|
4128
|
+
"named": {
|
|
4129
|
+
"Homie Arcade": {
|
|
4130
|
+
"sections": {
|
|
4131
|
+
"AGENTS.md": {
|
|
4132
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
},
|
|
4138
|
+
"0.58.0": {
|
|
4139
|
+
"files": {
|
|
4140
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
4141
|
+
"README.md": "684b3cca8001673c",
|
|
4142
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
4143
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
4144
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
4145
|
+
"videos/README.md": "16babc3437d5545b",
|
|
4146
|
+
"posts/README.md": "37841aae476bd540",
|
|
4147
|
+
"site/README.md": "1dbdf22c03a1e505",
|
|
4148
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
4149
|
+
},
|
|
4150
|
+
"sections": {
|
|
4151
|
+
"AGENTS.md": {
|
|
4152
|
+
"(intro)": "af7f2f732505d379",
|
|
4153
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
4154
|
+
"## Layout": "f3a280b8c96cd068",
|
|
4155
|
+
"## Apps: one screen, roles and parts": "c10aa0441ea89702",
|
|
4156
|
+
"## Commands (all through the pinned CLI in node_modules)": "c20d90de21cde0c7",
|
|
4157
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
4158
|
+
"## Making games": "bb715272fd1a615b",
|
|
4159
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
4160
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
4161
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
4162
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
4163
|
+
"## Selling things (the shop)": "0df5a5fe1833c624",
|
|
4164
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
4165
|
+
"## Rules": "b2234d80f6cbe71d",
|
|
4166
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
4167
|
+
"## Beta": "9d02638207a26e73"
|
|
4168
|
+
}
|
|
4169
|
+
},
|
|
4170
|
+
"named": {
|
|
4171
|
+
"Homie Arcade": {
|
|
4172
|
+
"sections": {
|
|
4173
|
+
"AGENTS.md": {
|
|
4174
|
+
"## Commands (all through the pinned CLI in node_modules)": "8364e05cebefcba4"
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4095
4179
|
}
|
|
4096
4180
|
}
|
|
4097
4181
|
}
|
package/netplay/NETPLAY.md
CHANGED
|
@@ -290,6 +290,13 @@ at once, with bots filling the slots (Gem Rush: 1 human + 2 bots).
|
|
|
290
290
|
- A human who leaves turns back into a bot in the same body.
|
|
291
291
|
- `trim()` removes surplus bots between rounds.
|
|
292
292
|
- After every change the host calls `net.roster(slots)`.
|
|
293
|
+
- A browser host may receive `join` before its world is ready. Once it publishes a roster,
|
|
294
|
+
snapshot traffic makes the relay retry any connected replica seat missing from that roster,
|
|
295
|
+
with delays doubling from 1 second to a 30-second cap, for at most eight retries per peer
|
|
296
|
+
and host. A roster seat (including `agent.seat` for host-driven AI) acknowledges the claim.
|
|
297
|
+
If still unacknowledged after the final wait, the relay logs once and stops retrying. Treat a repeated `join` for an
|
|
298
|
+
already claimed seat as idempotent; keep its body and score. This uses the existing wire
|
|
299
|
+
message, including for older bundled clients. Rules hosts reconcile seats separately.
|
|
293
300
|
|
|
294
301
|
**Join in progress.**
|
|
295
302
|
1. The `welcome` carries the last snapshot, round, roster and **all keyed state**. The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/parts/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Toolkit parts catalogue
|
|
2
|
+
|
|
3
|
+
These are reusable files, not new npm packages or complete apps. The catalogue entries
|
|
4
|
+
are the standard `part.json` records, also collected in `catalogue.json` for local browsing.
|
|
5
|
+
|
|
6
|
+
| Part | What it supplies | Source | Licence |
|
|
7
|
+
| --- | --- | --- | --- |
|
|
8
|
+
| [Owner action checklist](owner-action-checklist/README.md) | Consequence-first owner/staff action UI, server integration boundary, optional undo | Owner action checklist / Homie toolkit | Apache-2.0 |
|
|
9
|
+
| [Terrain route map](terrain-route-map/README.md) | Local heightmap coast, draped route, markers and phone/TV camera controls | Terrain route map / Homie toolkit | Apache-2.0 (three.js MIT) |
|
|
10
|
+
|
|
11
|
+
Each folder includes its licence, attribution, AI integration contract, tunables and small
|
|
12
|
+
working preview. Ryan Campbell authored both. To preview locally, serve this directory with
|
|
13
|
+
a static HTTP server, then open `<id>/preview/index.html`.
|
|
14
|
+
|
|
15
|
+
These public source parts are explicitly shared in their manifests. This checkout is not
|
|
16
|
+
a deployed studio and does not invent a catalogue host or claim a live directory listing.
|
|
17
|
+
To distribute from a studio, retain the original provenance/credit, place these original
|
|
18
|
+
source folders under that studio's `parts/`, run the existing `parts share` checks/packing,
|
|
19
|
+
and deploy. The standard well-known catalogue then advertises exact immutable releases;
|
|
20
|
+
consuming studios use `parts_find` / `part_add`, never copy another studio's release by hand.
|
|
21
|
+
Follow [PARTS.md](PARTS.md) for hashes, licence checks, packing and delivery. Do not serve
|
|
22
|
+
unpacked source as an immutable release. Later changes to a shared release need a new part
|
|
23
|
+
version. These initial parts are 1.0.0; no toolkit version is changed.
|
|
24
|
+
|
|
25
|
+
Maintainers: `node scripts/build-shared-parts.mjs` builds the local map preview and rewrites
|
|
26
|
+
hashes with `writeHashes`. `node --test packages/studio/test/shared-parts.test.mjs` checks
|
|
27
|
+
logic, licences and real pack/catalogue/integrity paths. Run
|
|
28
|
+
`node scripts/check-shared-parts.mjs` for one short browser check and 4× CPU measurements.
|