Managed Space Bundle Contract
Managed Spaces are static web applications uploaded as ZIP bundles and run in an isolated iframe. This document is the authoring contract for the bundle, OAuth binding, runtime bridge, safety review, and release lifecycle.
The Rust validator in
apps/rust-server/src/services/space_bundle.rs is authoritative when this
document and a deployed server differ.
First Publish Checklist
Section titled “First Publish Checklist”- Open Developer Console and create an OAuth app with a custom Client ID. Supply a real HTTP or HTTPS redirect URI; the platform does not invent a callback URL.
- Create a draft Space in Creator Console and copy its separate Space resource ID for CLI and API commands.
- Copy the starter template, replace
your-space-idwith that Client ID, and build the ZIP witharinova space build. - Upload the ZIP, inspect its scan result, and open a signed preview.
- Publish a version whose scan passed. A high-risk finding keeps the Space unlisted until it is resolved or reviewed.
The OAuth Client ID and manifest id are the same stable ASCII identifier.
They accept 1–128 lowercase ASCII letters, digits, and internal hyphens, must
start and end with a letter or digit, and must match exactly. The Space resource
ID is a separate UUID used in Creator Console, CLI arguments, and API paths.
ZIP Layout
Section titled “ZIP Layout”space.json must be at the ZIP root. Do not ZIP a containing directory.
space.jsonindex.htmlapp.jsstyles.cssassets/ cover.webpThe ZIP may contain nested paths. Paths must be relative, use /, contain no
empty, . or .. segments, backslashes, colons, NUL bytes, or file/directory
prefix conflicts. Symbolic links and other special files are rejected.
Allowed extensions are html, js, mjs, css, json, png, jpg,
jpeg, gif, webp, svg, woff, woff2, ttf, otf, mp3, ogg,
wav, and wasm. Files such as .DS_Store, source maps, icons, archives,
executables, and extensionless files are not accepted.
Limits are enforced against both ZIP metadata and actual decompressed bytes:
- uploaded ZIP: 20 MiB;
space.json: 256 KiB;- all uncompressed files: 40 MiB;
- one file: 10 MiB;
- entries: 512, including
space.json; - declared API origins: 8.
Every HTML file is checked, not only the entry. A literal <base element is
forbidden because the runtime injects its own signed asset base URL.
space.json
Section titled “space.json”The manifest is strict: unknown properties are an error. Its JSON shape is:
{ "id": "your-space-id", "version": "1.0.0", "entry": "index.html", "name": "Optional runtime name", "description": "Optional runtime description", "assets": ["app.js", "styles.css", "assets/cover.webp"], "declaredApiOrigins": ["https://api.chat.arinova.ai"], "requestedScopes": ["profile", "economy"]}Fields:
idis required and must exactly match an OAuth Client ID owned by the creator. After the first upload, later versions must use the same app.versionis required valid semantic versioning and must be unique within the Space. Increment it before every upload.entryis a required relative.htmlpath present in the ZIP.nameanddescriptionare optional. Storefront metadata remains managed by Creator Console.assetsis optional and may list only safe relative paths present in the ZIP. It may contain at most 512 entries.declaredApiOriginsis optional and contains at most eight unique, bare HTTPS origins. No path, query, fragment, credentials, or HTTP origin is accepted. A trailing slash is tolerated but an origin-only value is best.requestedScopesdefaults toprofile. It must containprofile, may also containagentsandeconomy, contains no duplicates, and has at most three entries.emailand arbitrary scopes are not embedded Space scopes.
Network And CSP
Section titled “Network And CSP”The runtime iframe has an opaque origin because its sandbox does not include
allow-same-origin. Its policy is deny-by-default and its connect-src is
'self' plus declaredApiOrigins. Each declared HTTPS origin also
automatically emits the matching wss:// origin for the same host and port,
so a manifest does not need a separate WebSocket declaration.
For an opaque iframe, 'self' does not authorize the Arinova API. Therefore a
Space that calls any platform /api/v1 endpoint must declare the current API
origin, for example https://api.chat.arinova.ai in production. Declare every
other API origin used by fetch, WebSocket, or SDK methods too. The safety
scanner permits URL literals whose origins were declared, but undeclared
external URLs may block publication.
The manifest is not a proxy allowlist. The browser still enforces CORS, and a
remote service must explicitly accept the opaque Origin: null. Arinova’s
Space OAuth APIs use wildcard read/write CORS without credentials; browser
session endpoints remain inaccessible from the iframe.
Runtime Bridge Protocol v1
Section titled “Runtime Bridge Protocol v1”The parent places an unguessable bridgeToken in the runtime URL fragment.
The runtime and SDK must retain it on all bridge requests. Because the iframe
origin is intentionally null, the parent authenticates a message using the
exact iframe source, the bridge token, and protocolVersion: 1 together.
Do not send arinova:ready yourself: the server injects it into the entry
document before author code runs.
The eight bridge messages are:
arinova:ready — runtime to parent
Section titled “arinova:ready — runtime to parent”{ "type": "arinova:ready", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1 }}arinova:auth — parent to runtime
Section titled “arinova:auth — parent to runtime”{ "type": "arinova:auth", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "spaceId": "your-space-id", "accessToken": "short-lived-space-oauth-token", "scope": "profile economy", "expiresAt": 1780000000000, "user": { "id": "…", "name": "…", "email": "…", "image": null }, "agents": [] }}Treat the access token as ephemeral memory-only data. Never persist or log it.
The agents array is populated only after the agents scope is granted.
arinova:request-scope — runtime to parent
Section titled “arinova:request-scope — runtime to parent”{ "type": "arinova:request-scope", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "scope": "economy" }}Only agents and economy need an interactive request; profile is the
baseline. A requested scope must also be declared in space.json. The host
shows native consent, records it per user and Space, refreshes the token, and
sends a new arinova:auth message after approval.
arinova:scope-denied — parent to runtime
Section titled “arinova:scope-denied — parent to runtime”{ "type": "arinova:scope-denied", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "scope": "economy", "reason": "User denied the request" }}Denial is a normal outcome. Keep the Space usable without the elevated capability and do not loop the consent prompt.
arinova:purchase-request — runtime to parent
Section titled “arinova:purchase-request — runtime to parent”{ "type": "arinova:purchase-request", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "productKey": "coins.small" }}The parent loads the authoritative product and shows native confirmation. The iframe cannot choose a price or debit points directly, and only one purchase request may be outstanding at a time.
arinova:purchase-result — parent to runtime
Section titled “arinova:purchase-result — parent to runtime”{ "type": "arinova:purchase-result", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "productKey": "coins.small", "status": "purchased", "grantId": "…" }}status is purchased, cancelled, or error. An error may include
errorCode; a successful product or subscription purchase includes its
server result. See Managed Space commerce for
inventory, subscription, refund, and revenue behavior.
arinova:wager-buyin-request — runtime to parent
Section titled “arinova:wager-buyin-request — runtime to parent”{ "type": "arinova:wager-buyin-request", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "sessionId": "11111111-1111-4111-8111-111111111111", "amountPoints": 300 }}The parent validates the UUID and positive safe-integer amount, loads the authoritative session range and version, applies the cancellation cooldown, and shows native confirmation. The runtime cannot provide confirmation copy, the Space name, the idempotency key, or expected economic values.
arinova:wager-buyin-result — parent to runtime
Section titled “arinova:wager-buyin-result — parent to runtime”{ "type": "arinova:wager-buyin-result", "bridgeToken": "fragment-bound-token", "payload": { "protocolVersion": 1, "sessionId": "11111111-1111-4111-8111-111111111111", "status": "accepted", "stakeId": "33333333-3333-4333-8333-333333333333" }}status is accepted, cancelled, or error. Errors may include
errorCode and retryAfterMs. Only one wager request may be pending; the SDK
must wait for its matching result. See
Managed Space wager sessions for the service-token,
settlement, conservation, and recovery contract.
Versions, Preview, And Safety
Section titled “Versions, Preview, And Safety”Uploads begin as draft. Upload responses include scanStatus, risk, and
redacted findings; the same report is available through
GET /api/v1/spaces/{spaceId}/versions/{versionId}/scan.
Publishing and rollback both rescan the selected bundle. Rollback is a new
publish operation, not a pointer-only change: it reruns current safety rules,
revokes existing Space OAuth tokens, and disconnects live players. A high-risk
scan marks the version rejected, keeps the Space unlisted, returns HTTP 202
with findings, and opens a safety review. After a scanner false-positive fix,
POST .../scan rescans the same immutable version; a pass changes rejected
back to draft and closes its pending review case. For a content fix, bump the
manifest version and upload a replacement bundle. Neither path bypasses
scanning.
A signed preview lasts 15 minutes and does not require publication. Both
session and headless v1 creators can call
POST /api/v1/spaces/{spaceId}/versions/{versionId}/preview; refresh the URL
after expiresAt. The platform retains the active version plus the newest
inactive versions up to five total. Pending-review versions are retained;
older inactive versions may be removed automatically. The active version
cannot be deleted.
Before deploying scanner-rule changes, operators must run the read-only active version inventory from the exact candidate checkout and target environment:
cd apps/rust-servercargo run --bin space_safety_dryrun -- \ --environment staging \ --output /tmp/managed-space-safety-staging.jsonThe normal deployment DATABASE_URL, REDIS_URL, storage, and server secret
environment must be present. The report includes every active version’s risk
and redacted finding kind/field, never the matched source text. Review every
high-risk result before cutover, repeat in production, and attach both reports
to the release evidence. A zero-row local report proves only that the command
ran against an empty local inventory; it is not production evidence.
Commerce And Revenue
Section titled “Commerce And Revenue”Admission is free or a one-time purchase. Creators can separately define up to 100 consumable, durable, or subscription products. Product and subscription state is server-authoritative; never use Space key/value storage as inventory. For paid-funded purchases, 70% is credited to creator earnings. Bonus-funded points do not create creator earnings. Refunds and administrative clawbacks reverse the corresponding share.
Build And Upload
Section titled “Build And Upload”The CLI workflow is intentionally bundle-first:
arinova space init my-spacecd my-space# Put the OAuth Client ID into space.json, then edit the app.arinova space buildarinova space version create <space-id> --bundle dist/my-space-1.0.0.ziparinova space version preview <space-id> <version-id>arinova space version publish <space-id> <version-id>space build mirrors the server’s path, extension, manifest, HTML, count, and
size checks. The server still validates and scans every upload. For Space
storage commands, pass a Space-bound OAuth token with --token; a creator API
key is not a runtime token.
Build a7f47a5ca54ddcf7806cd48b81ce1b9827042766