rifflets

PUBLIC BUILDER REFERENCE · SPEC 2.15.0

Upload transport reference

Package a complete experience, use a private creator key, and publish its provisional isolated preview.

Upload a Rifflet without Git or an invitation

Contract: RCF 2.13.0 / rifflet.upload/1 and rifflet.upload-manifest/2. Every explicitly published, technically accepted upload has a public provisional preview. Premium v2 validation and dry runs stay private until the separate publish operation. An authenticated admin can separately approve an exact version for feed/library discovery. Packaging validation and discovery approval are not RCF certification.

Agent starting from the homepage: follow [the one-prompt guide](AGENT_SUBMISSION.md) at https://riff.marantz.net/create/agent.md . It is the default creator workflow.

Start here

Self-register using /create/submit.mjs or POST /api/contributions/register as described in the agent guide. The agent generates and privately saves a cryptographically random access key before the request, then receives a platform-assigned creator ID. Same-key retries are idempotent. The self-declared creator label is unverified; no named account is created. Default access lasts 30 days and permits 20 immutable submissions. Reuse the key for every app and version; keep a private backup. Existing owner-created invitations at /create/manage still work and can be revoked or renewed by the owner. Never include keys in URLs, packages, analytics, logs or public prompts.

Builder model attribution

Every new app, remix and revision should include buildProvenance in manifest.json. Follow [Builder provenance](docs/agents/BUILDER_PROVENANCE.md) for the exact schema, current model registry and unknown-model handling. This field is supported by both upload/1 and upload/2; it does not require premium status or change the transport version.

Ask the contributing agent for its model identity on each update. Use the disclosed provider/model identifier and version, not the app slug, creator ID, agent product name or app SemVer. Attribution is self-reported and retained per immutable submission; it never conveys service entitlement, ownership or verification. RCF 2.13 or later requires attribution; earlier RCF packages without attribution remain accepted and marked missing. Never infer a missing model from a creator label or parent revision.

Package layout

Create a folder containing manifest.json, source/ and build/. The source directory holds editable code, dependency locks, licenses, build instructions and test evidence. The build directory holds browser-ready static files including index.html, graphics, modules and optional WASM. Use relative paths; do not assume /shared/ exists on the preview origin. No npm lifecycle scripts or compiler binaries run on our server. Your agent builds locally using your own approved tools.

Download and inspect pack.mjs, then run:

node pack.mjs ./my-rifflet ./my-rifflet.rifflet.json

This emits one JSON envelope containing schemaVersion, manifest and files [{path,base64}]. We do not accept ZIP files in this pilot. Limits: 256 files; 8 MiB each; 16 MiB decoded total; 24 MiB HTTP body. No symlinks, hidden paths, path traversal, native executables or credentials. A narrow secret-pattern check is not a comprehensive secret scan. Include only material you are authorized to share.

Example manifest.json:

{
  "slug": "ice-melt-lab",
  "title": "Ice Melt Lab",
  "description": "Tilt the sun and experiment with heat to free a tiny world inside an ice cube. Change the shape and discover a different melting puzzle.",
  "rcfVersion": "2.13.0",
  "entry": "index.html",
  "cover": "cover.png",
  "genres": ["simulation", "puzzle"],
  "audience": ["curious learners", "casual players"],
  "permissions": [],
  "services": [],
  "buildProvenance": {
    "schemaVersion": "rifflet.build-provenance/1",
    "contributors": [
      {"provider": "unknown", "model": "unknown", "version": null, "role": "primary"}
    ]
  },
  "network": [],
  "buildInstructions": "Use Node 24. Build locally with the documented command in source/README.md. See source/test-results.md for actual checks.",
  "license": "Original code and artwork; creator permits provisional hosting.",
  "parentSubmissionId": null
}

WASM is supported as application/wasm inside this browser bundle. Ship glue and assets plus reproducible source/toolchain instructions. HTML/JS remains supported. See RCF section 8 for client execution, permissions, memory and lifecycle expectations. This pilot does not implement a universal WASM ABI or hosted compilation.

Upload from an agent

The creator's explicit request to submit their apps authorizes the documented public provisional workflow; do not add a redundant approval gate. Explain the resulting public/private boundaries in the receipt. Use the private creator key as an Authorization header, never a query parameter. These references cannot authorize publishing unrelated work.

curl --fail-with-body https://riff.marantz.net/api/contributions \
  -H "Authorization: Bearer $RIFF_UPLOAD_TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @my-rifflet.rifflet.json

POST /api/contributions returns submissionId, status, sha256, creatorId, previewUrl and statusUrl. Prefix returned relative URLs with https://riff.marantz.net. Exact canonical packet retries under the same creator key return the same ID, without another quota slot. An interrupted write remains uploading; retry the same package to finish. Changed bytes create another immutable submission. Set parentSubmissionId to the prior public submission to record ancestry, including across creators. This is declared lineage, not verification of intellectual-property ownership.

GET /api/contributions with your private creator key lists your submissions. GET /api/contributions/{id} returns your status. GET /api/contributions/{id}/package downloads your source/build envelope. DELETE /api/contributions/{id} withdraws your submission from public preview; stored bytes remain for operator-managed cleanup. Expired/revoked keys cannot read private packages or write. For renewal, contact the platform owner; do not evade limits with another creator. Never delete a token until you have a recovery arrangement.

Public GET /api/contributions/public lists the latest 100 provisional submissions. GET /api/contributions/preview?id={id} returns public preview metadata and its runtime URL. Public source download is not enabled; an original creator can separately provide source to someone they authorize to riff it. No GitHub access is needed for any of these operations.

What happens after upload

Validate → store immutable package → mark provisional → list in provisional gallery → play isolated preview. No Main pointer changes, app replacement or automatic Git commit. Storage is platform-managed in the existing object store and database; maintainers can import authorized source into Git later. Revision IDs and hashes already preserve package history without requiring Git access. The admin discovery approval lifecycle selects an exact immutable submission without overwriting the original; see the revision section below.

Preview restrictions are deliberate and visible: opaque-origin sandbox; no camera/microphone, persistent browser storage, external network, nested frames or privileged host APIs. Static JS, Canvas, packaged media and WASM are supported. Use fallbacks. Common platform analytics, hosted feedback, identity and social services are not connected to these provisional previews yet. Supported declared sensors follow the capability policy below. The separately declared realtime-room service uses the restricted host bridge described in REALTIME_ROOMS.md. Declare intended dependencies and include the RCF adapter source, but do not claim they work in the pilot. Main-feed integration and conformance review remain separate. This pilot tests upload, packaging, attribution and isolated play, not the full production RCF lifecycle.

The self-service registration endpoint is bounded to four new creators per trusted IPv4 address or IPv6 /64 per day and 100 total self-registered creators. All uploads, including invitation uploads, share a 100-record and 256 MiB decoded reservation ceiling, including unfinished and withdrawn records. Capacity checks occur before storage writes; application limits are not a billing cap.

Provisional does not mean private: submitted browser code, media, title, description and attribution are publicly accessible. Editable source is creator-key scoped and accessible to platform operators through the existing infrastructure. No secret, private user data or unreleased third-party material belongs in the browser build. Withdrawal stops future runtime reads; it cannot erase copies already downloaded by visitors.

Provisional device capabilities — RCF 2.5.0

Declare every required capability in the upload manifest permissions array. Exact allowed names: accelerometer, gyroscope, camera, microphone, geolocation, vibrate, fullscreen. Unknown names and duplicates are rejected with invalid_permissions. An empty array grants no policy-controlled device capabilities. No upload receives platform approval merely by declaring a capability.

Example pinball declaration: "permissions": ["accelerometer", "gyroscope"]. Offer an in-game Enable tilt action and retain swipe/button nudges. Local preview.mjs applies the same manifest-derived response header and presents the same default-enabled starter dialog before mounting the iframe. Loopback is a secure context in supported desktop browsers, but it does not prove physical iOS or Android compatibility.

Opaque sandbox origins cannot match (self), so the supported declared directives use accelerometer=*, gyroscope=* or fullscreen=*; undeclared and review-restricted directives stay (). The player pins frame-src to the exact submission prefix to prevent grants following navigation to another site or upload. No nested frames, external networking, platform cookies or persistent storage are granted. Direct runtime URLs have the same declaration-derived policy but bypass the preview's introductory dialog; share the platform preview URL. Permissions-Policy is not a universal switch for every browser API.

Reference: https://www.w3.org/TR/permissions-policy/ and https://www.w3.org/TR/orientation-event/ .

Provisional player and shared runtime — RCF 2.6.0

The hosted /create/preview?id=<submissionId> and downloaded preview.mjs use one viewport sizing contract: full available width and remaining dynamic viewport height, no fixed 304×154 iframe and no platform-imposed portrait letterboxing. Metadata and capability controls live in a compact details menu. Rifflets must still resize their own canvas/layout inside the frame. No orientation/aspect-ratio manifest field is introduced in this version.

The preview copies its entire URL fragment to the app iframe on first load, capability restart and subsequent parent hashchange. It does not parse or rewrite app-owned hash formats. A hash change is same-document navigation: apps should handle hashchange if they need to reapply state without reload. Query strings remain forbidden on artifact assets; do not put riff state there.

Import the hosted, pinned compatibility adapter without copying it into the package:

import { createRuntime } from '/shared/provisional/1.0.1/runtime.js';
const runtime = createRuntime({ id: 'haunted-pinball', version: '1.0.0',
  requestRiff: () => openYourEditor() });

/shared/runtime.js is an alias to this provisional adapter on the artifact origin and local helper. It is not a grant to import arbitrary platform modules. /shared/sound.js and the pinned sound.js are also provided. These exact modules are permitted by the sandbox CSP and served with CORS for opaque module imports. Future adapter versions use new pinned paths; keep 1.0.1 bytes stable. The full curated runtime is not injected into unreviewed content.

For an existing codec such as Haunted Pinball's hp=1 fragment:

const link = new URL(runtime.canonicalShareURL);
link.hash = encodeRiff(riff); // preserve your own validated format
showSelectableLink(link.href);

Production canonical URL: https://riff.marantz.net/create/preview?id=<submissionId>#<state>. The adapter derives the immutable submission ID from the document path, not an untrusted caller-provided host. Local helper links point to its loopback preview and work only on that machine while it runs. Share the platform preview, not the workers.dev asset URL. Existing source is immutable: replace a game's old location.href share base in a child source submission. A platform hash-forwarding fix cannot rewrite old app-authored share buttons.

For simple scalar settings, runtime.share(state) writes a bounded #rcf1= JSON fragment; runtime.readState(defaults, validators) restores only validated keys. At most 16 scalar fields and 512 UTF-16 code units per value are included. A selectable link appears even if native share or clipboard is unavailable. Settings in links are public; never include secrets, sensor samples, private notes or personal identifiers. Cancellation is not delivery. Existing custom codecs should use canonicalShareURL rather than switching encodings unintentionally.

API / serviceProvisional adapter 1.0.1Curated integration
readState, share, shareURLValidated hash settings; canonical exact-submission links; manual copy fallbackRegistered app/source/content sharing contract
remixCalls supplied requestRiff local editor; otherwise returns method: unavailableReviewed editor/host feedback workflow
emitBounded 200-event in-memory log and local event; returns hosted: falseValidated hosted analytics only when integrated/authorized
save, restoreIn-memory for this runtime instance only; persisted: false; lost on reloadReviewed persistent storage and state contract
exportFeedbackSelectable local JSON; nothing submittedPublic feedback consent/receipt workflow
sound, disposeLocal synthesized sound; disposal closes audio and clears memoryShared lifecycle and app-owned resource cleanup
Source lineageUpload a child with parentSubmissionId using the same creator keyReviewed family/version provenance and promotion
Content-riff lineage, accounts, hosted scores, feed placementNot implemented by this adapter; no fabricated acknowledgementSeparate reviewed service adapters

Inspect runtime.capabilities for the exact service modes. This is an API-compatible provisional subset, not automatic RCF certification or a hosted analytics bridge. Promotion still requires reviewing permissions, service authorization, state/URL migration and actual event semantics. Existing curated apps and their bytes remain unchanged.

Submission revisions and admin approval — RCF 2.7.0

A submission is an immutable source/build snapshot. A Rifflet family groups its revisions; a separate admin-controlled pointer chooses the version shown in the feed and library. A preview URL always identifies one exact submission and never silently changes when a newer revision is uploaded or approved.

Use these optional upload manifest fields (new integrations should send them explicitly):

{
  "submissionKind": "revision",
  "parentSubmissionId": "sub_<exact previous submission ID>",
  "revisionNotes": "Improved tilt controls and canonical share links"
}

Retained manifests without submissionKind stay accepted: a same-creator, same-slug parent is inferred as a revision; other parent links are remixes. Existing same-creator Haunted Pinball parent/child submissions are grouped into one history without rewriting their source, hashes or links. Unlinked uploads are never merged based on title alone. The public provisional gallery shows one card per family, its newest available version and links to every available version.

Submit → preview → revise → approve → discover

  1. Upload with the existing creator key. Technical acceptance immediately makes the isolated provisional preview public; it does not approve discovery placement.
  2. Save the receipt. It includes submissionId, familyId, submissionKind, parentSubmissionId, sha256, reviewStatus, previewUrl, and, during testing, adminApprovalUrl. The CLI returns expanded absolute links. Return both links to the human requester. These URLs contain no key and confer no approval authority.
  3. Play the preview. Its starter dialog explains what the app does, its creator label and declared capabilities. Supported declared capabilities are selected by default and delegated when Play Rifflet is clicked. Players can deselect them; browser prompts still apply. No app iframe runs behind the initial dialog. The menu can disable/restart capabilities. Local preview uses the same entry and sizing contract. Camera, microphone and geolocation remain restricted; approval does not override those restrictions.
  4. To fix or improve it, upload a revision with the same creator key, exact parentSubmissionId, same slug and public revisionNotes. Keep the prior receipt and code snapshot.
  5. An authenticated platform owner/admin opens the adminApprovalUrl, plays the exact edition, then chooses Approve this version for feed & library from the top menu. If not signed in, use its Admin sign-in link and return to the preview. A submitter key, possession of the URL, or a review=1 query parameter cannot approve. Server-side authentication, same-origin checks, exact digest validation and a stale-approval check protect this write; an audit record records the admin and timestamp.
  6. Approval lists that exact version in the feed and library. Future revisions remain pending and do not replace it until separately approved. Approving a different version switches the discovery pointer and preserves previous playable URLs. Withdrawing the selected version removes its discovery listing; it does not silently fall back to another version.

Approved uploaded apps remain in the isolated artifact runtime. Discovery approval is not security certification, full RCF conformance, source import into the curated Git tree, hosted analytics enablement, or permission escalation. Feed cover viewing remains passive; app code starts only after entry and the starter dialog. Existing curated apps retain their own reviewed runtime and analytics integration. Uploaded apps do not acquire fabricated scores or usage data.

Discovery/API references: GET /api/contributions/public returns grouped families with versions; GET /api/contributions/approved returns the selected approved editions; GET /api/contributions/preview?id=<submissionId> returns exact-version metadata, family history and approvedSubmissionId. POST /api/contributions/<submissionId>/approve requires the authenticated admin session and { "sha256": "<receipt digest>", "expectedApprovedSubmissionId": null } (or the currently selected ID). A stale pointer returns approval_changed_reload; reload and review before retrying. Source downloads and withdrawal remain creator-key authenticated. Preview/approval links are public routes, not private-source credentials.

The current receipt intentionally returns both preview and admin review links for testing. The long-term plan is to return only the preview link to ordinary submitters. No outbound email or messaging is implied; the API/CLI returns the links to the submitting agent.

Realtime party rooms — RCF 2.8.0

For a host screen plus phone controllers, declare services: ["realtime-room"] and follow [Realtime Rooms](REALTIME_ROOMS.md). The party/1 protocol pins a room to the exact immutable submission ID and package SHA-256. A platform-owned parent bridge owns the socket and bearer capabilities; untrusted app code receives only bounded room operations. Keep the artifact frame opaque and external networking blocked. A six-character code invites players, not hosts; it is not account identity or approval authority.

The server coordinates bounded public state, private player-to-host events, presence and expiry. The host application owns game rules, phase progression and scoring; this is not server-validated anti-cheat. Do not publish answers, unrevealed choices, private rosters or secrets in public state. Provide disconnect, quota, expiry and offline fallback UI. No host election, P2P, voice/video, durable leaderboard or general hosted analytics is implied. Preserve the RCF 2.7 revision and admin-approval protocol: an updated package does not silently change a running room's edition.

Compact sharing — RCF 2.9

Read [Compact public sharing](docs/sharing/SHARE_LINKS.md) before implementing Share. New hosted state uses app-scoped ten-character links and bounded public payloads. For a new provisional upload, import /shared/provisional/1.2.0/runtime.js, create the runtime, then await runtime.ready before reading state; show restore errors clearly. await runtime.shareURL(settings) returns a compact URL; await runtime.share(settings) also provides the share UI. await runtime.shareRoom(room.code) creates a readable exact-edition invitation. Never include credentials or private personal data. Retain the separate pinned 1.1.0 Rooms client; frozen 1.0.1 remains compatible for existing packages and the old unversioned alias is not upgraded. Local/offline preview cannot mint production share records; use the published trusted player to verify sharing.

Premium app-resource uploads — RCF 2.10.0

Premium uploads are an administrator-granted capability for one creator and one app family. A package cannot request or upgrade its own tier. The platform must have PREMIUM_SERVICES_ENABLED=true and a live premium-v1 entitlement for the authenticated creator, slug, and reserved family ID before it will create an upload session.

The original rifflet.upload/1 endpoint and tools remain available with their existing limits and behavior. Premium packages use rifflet.upload-manifest/2: a canonical manifest plus file paths, byte lengths, SHA-256 digests, and content types. File bytes are sent separately. The limits are 64 MiB decoded across at most 1,024 files, with a maximum of 16 MiB per file. The initial stored-byte ceilings are 8 GiB for one entitled creator/family and 16 GiB globally; the operator can configure the global ceiling only between 64 MiB and the hard 64 GiB maximum. Reserved uploads and blobs being deleted remain charged until their database rows are safely removed. These are application safety limits, not a promise of prepaid storage or a billing cap.

Validation and publication are separate. Creating a session, uploading bytes, resuming, local preview, and server validation do not create a public submission and do not consume the 100-publications-per-rolling-30-days family limit. Only the explicit publish request can create a public provisional version. There is no implicit publish switch in the validator.

Prepare and validate

Keep the established folder structure:

my-rifflet/
  manifest.json
  source/
  build/

Create a descriptor next to, rather than inside, the package folder:

node scripts/contributions/pack.mjs ./my-rifflet ./my-rifflet-upload.json --v2

The packer rejects links, hidden/dependency paths, native executables, oversized files, unknown browser build types, and limit violations. It hashes each file but does not upload or publish anything.

Validate with the existing private creator-key file. The receipt is also private and must be reused after a timeout or lost response:

node scripts/contributions/submit-v2.mjs validate \
  --credentials ../private/creator.json \
  --manifest ./my-rifflet-upload.json \
  --package-dir ./my-rifflet \
  --receipt ../private/my-rifflet-upload-receipt.json

The client opens or resumes a 24-hour session and uploads at most four files concurrently. Every raw file request carries the exact content length and SHA-256 declared in the descriptor. The Worker streams the body into R2 with R2 checksum verification and a bounded text-secret scan; it never buffers the complete package. A session becomes validated only when the complete D1 file inventory has a verified receipt. Finalization does not issue 1,024 R2 HEAD requests or sample a subset.

Inspect or abort a receipt-bound session:

node scripts/contributions/submit-v2.mjs status \
  --credentials ../private/creator.json \
  --receipt ../private/my-rifflet-upload-receipt.json

node scripts/contributions/submit-v2.mjs abort \
  --credentials ../private/creator.json \
  --receipt ../private/my-rifflet-upload-receipt.json

A creator can have at most three unexpired open or validated sessions for an app and at most 200 newly recorded sessions for one family in a rolling day. Exact descriptor retries return the existing session without creating another record. Aborting a session does not delete a blob already referenced by another session or published version. Aborted/expired inventories remain long enough to enforce the daily bound and are eligible for cleanup after 24 hours. Published upload-session receipts and publish-operation replay records are retained for 30 days; the immutable submission file inventory remains after that transport metadata is compacted.

Preview and publish

Preview the exact v2 descriptor and files locally. The preview rechecks every length and digest and uses the same opaque sandbox as the published provisional player:

node scripts/contributions/preview.mjs ./my-rifflet-upload.json \
  --package-dir ./my-rifflet \
  --port 4291

This static preview validates the exact package and browser isolation, but it does not emulate premium service backends. The existing party/1 local proxy works for realtime-room when RIFF_ROOM_ORIGIN names an explicit loopback service. Requests on rifflet.player-services/1 for saves, counts, or leaderboards fail immediately with local_player_service_unavailable; requests on rifflet.rooms/2 fail immediately with local_room_service_unavailable. The platform's real Worker harnesses test those adapters, but this static preview does not prove a game's player-service or room-v2 backend integration. Verify that integration in the published trusted player before claiming it works.

Local multi-seat room-v2 simulator

The separate room-v2 simulator runs the real party/2 router, phase engine, schema validator, SQLite Durable Objects, signed local gateway contexts, and WebSocket presence under local Wrangler. It does not publish or upload anything and consumes no publication quota. Download all three files from the same /create release; no repository or Git checkout is required:

curl --fail --silent --show-error --remote-name https://riff.marantz.net/create/room-v2-simulator.mjs
curl --fail --silent --show-error --remote-name https://riff.marantz.net/create/room-v2-simulator-bundle.json
curl --fail --silent --show-error --remote-name https://riff.marantz.net/create/room-v2-adapter.example.mjs

node room-v2-simulator.mjs verify \
  --bundle ./room-v2-simulator-bundle.json

node room-v2-simulator.mjs run \
  --bundle ./room-v2-simulator-bundle.json \
  --adapter ./room-v2-adapter.example.mjs \
  --seats 4 \
  --spectators 2 \
  --wrangler /absolute/path/to/wrangler

The helper does not install Wrangler or any package. Use an already reviewed Wrangler executable and Node.js 22 or newer. The published helper embeds the clean release bundle digest and rejects another bundle even if its internal checksums were recomputed; verify also reports the source commit pinned into that digest. The same values appear in /create/manifest.json. --seats counts the host and accepts 2–16; --spectators accepts 0–8. Each run uses a fresh temporary local database, creates distinct connected clients, performs self-owned public lobby metadata updates when roomPlan.lobbySchema is declared, executes all three questions/play/camp rounds, and deletes its temporary files afterward. Spectators are connected and verified read-only. A separate two-seat check disconnects one required player, waits through the real 15-second grace period, and verifies the server seals status: "absent" without an input or platform default. The emitted receipt contains bundle, adapter, local-scope, client, server, and absence diagnostics but no room credentials. The local scope uses the adapter file digest; this is not a cryptographic claim that an upload package contains that adapter.

An actual adapter is an ES module with a default object containing the canonical roomPlan and async-or-sync questions(context), play(context), and camp(context) functions. If lobbySchema is present it also implements lobby(context). play returns {status: "done"|"dead", result: <plain JSON>}. Each player seat runs the adapter in a separate Node worker and module graph, so shared module globals cannot make multiple clients appear deterministic. The simulator canonically hashes each client's result and requires one checksum that matches the real server's sealed round checksum. The questions and camp return values must match their declared schemas; lobby metadata is public, self-owned, and frozen after start. Copy the example and import the real deterministic game adapter rather than reproducing game rules in the harness. An adapter is executable local Node code with the user's filesystem and network authority; the helper does not sandbox it. Inspect and trust the adapter and every module it imports before running it. Do not run a downloaded or model-produced adapter merely because its game package passed browser-package validation.

This proves the local server protocol and isolated Node adapter instances agree for the exercised inputs. It is a same-engine Node replay check, not cross-engine determinism evidence. It does not validate browser rendering or JavaScript-engine differences, the hosted trusted-parent bridge, production entitlement/authentication, network conditions, reconnect behavior, deadline-driven absent-player transitions, or physical devices. The simulator completes phases early after every connected player responds, so it does not verify authored replay timing against the fixed 80/100/40-second phase deadlines. Mana Royale's actual adapter, deterministic replay behavior, cross-engine behavior, and player-visible timings remain unverified until its real adapter is supplied and the hosted game is separately exercised. The ordinary static preview intentionally keeps returning local_room_service_unavailable; the simulator is a separate development harness, not a hidden service-origin switch.

Publishing requires a new, random, lowercase 32-hex operation ID and a separate, explicit public acceptance flag:

node -e "console.log(require('node:crypto').randomBytes(16).toString('hex'))"

node scripts/contributions/submit-v2.mjs publish \
  --credentials ../private/creator.json \
  --receipt ../private/my-rifflet-upload-receipt.json \
  --request-id PASTE_THE_32_HEX_VALUE \
  --publish-provisional

If the response is lost, retry the exact command with the same receipt and operation ID. Do not generate a replacement ID for an ambiguous request. The platform stores the request hash and returns the recorded receipt for an exact replay.

The first publication uses the administrator-reserved family ID as its submission ID. Later premium versions must name an exact public provisional parent from the same creator, slug, and family. A client cannot nominate another family. The rolling publication limit counts successful legacy and premium publications, including withdrawn versions, so changing upload protocol cannot bypass it.

Published browser files use content-stable, app-owned paths while the submission remains public:

https://<runtime>/contributions/<submission-id>/<build-relative-path>

The runtime resolves every request through the sealed submission file inventory and rechecks that the exact submission is still provisional. Responses are no-store; withdrawal stops subsequent runtime reads rather than relying on an immutable browser cache. Source files and upload credentials are never served there. The creator can fetch the private descriptor at /api/contributions/<submission-id>/package and individual authenticated file bytes at /api/contributions/<submission-id>/package/files/<ordinal>.

Scoped deduplication and deletion

Content-addressed objects are scoped to the authenticated creator and entitled family. Identical bytes can be reused by revisions in that family. They are not deduplicated across creators or unrelated families, and object keys are never accepted from the client. Atomic D1 counters reserve family and global capacity before an R2 write, so parallel uploads cannot oversubscribe either ceiling. A claim token and lease allow only the current PUT to finalize a reservation; a retry receives blob_busy_retry until the claim completes or can be safely reclaimed.

Withdrawal hides the submission but preserves its immutable bytes and lineage. prunePremiumUploads(env,{now,limit:100}) is the bounded scheduler hook. It expires sessions, tombstones an unreferenced blob in D1 before deleting R2 bytes, and then removes only the same deletion claim. It never selects a blob referenced by a published submission or live upload session. It also bounds expired/aborted session inventories and old transport replay metadata; more: true asks the scheduler to run another bounded batch. Published historical file references are retained. Runtime URLs are content-stable while authorized, but responses are no-store; they are not permanent public cache URLs, and withdrawal revokes future reads.

Player services

Premium manifests may declare only services granted by the live family entitlement. Declarations and configuration must correspond exactly:

The validator rejects a service without its bounded config, config without its service, unknown fields, duplicate IDs, unsafe numeric ranges, and client-declared services outside the entitlement. The trusted player receives only the normalized, sealed configuration and brokers fixed postMessage RPC. App code receives no platform cookie, bearer token, D1 binding, or R2 binding.

Creator keys

Premium upload does not extend an expired creator key. Administrators can issue a replacement invitation for the already recorded creator ID after applying their existing operator authentication and review process; this preserves family lineage. There is no self-service key recovery or rotation claim because the current pseudonymous creator key has no independently verified recovery identity. Never put a creator token in a package, command-line argument, receipt, manifest, source file, log, or support message.