跳到內容

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.

  1. 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.
  2. Create a draft Space in Creator Console and copy its separate Space resource ID for CLI and API commands.
  3. Copy the starter template, replace your-space-id with that Client ID, and build the ZIP with arinova space build.
  4. Upload the ZIP, inspect its scan result, and open a signed preview.
  5. 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.

space.json must be at the ZIP root. Do not ZIP a containing directory.

space.json
index.html
app.js
styles.css
assets/
cover.webp

The 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.

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:

  • id is required and must exactly match an OAuth Client ID owned by the creator. After the first upload, later versions must use the same app.
  • version is required valid semantic versioning and must be unique within the Space. Increment it before every upload.
  • entry is a required relative .html path present in the ZIP.
  • name and description are optional. Storefront metadata remains managed by Creator Console.
  • assets is optional and may list only safe relative paths present in the ZIP. It may contain at most 512 entries.
  • declaredApiOrigins is 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.
  • requestedScopes defaults to profile. It must contain profile, may also contain agents and economy, contains no duplicates, and has at most three entries. email and arbitrary scopes are not embedded Space scopes.

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.

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:

{
"type": "arinova:ready",
"bridgeToken": "fragment-bound-token",
"payload": { "protocolVersion": 1 }
}
{
"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.

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:

Terminal window
cd apps/rust-server
cargo run --bin space_safety_dryrun -- \
--environment staging \
--output /tmp/managed-space-safety-staging.json

The 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.

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.

The CLI workflow is intentionally bundle-first:

Terminal window
arinova space init my-space
cd my-space
# Put the OAuth Client ID into space.json, then edit the app.
arinova space build
arinova space version create <space-id> --bundle dist/my-space-1.0.0.zip
arinova 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