VEYNDO
Handbook
docs/app-security.mdView source ↗

English source · 8 min

App isolation and operator setup

This guide records the enforced boundaries of the current app platform. Read it before adding runtimes, stores or credentials. Architecture and Studio describe the same production owners.

Audit fixes

Finding Current owner and protection Regression
B1 table bomb sandbox_engine.rs, sandbox.rs, component_runtime.rs: preflight, element/memory limits, pooling, fuel, epochs Rust oversized private table/growth/host-loop tests; app_components
B2 shared app storage apps/records.rs, migrations 101–102: fixed tables, full logical keys, signed RLS, managed unique/reference projections and own-version compatibility app_shared_storage, app_storage_migration, apps, app_schema, tenant_isolation
B3 service ID impersonation apps/approval.rs: exact Rust-canonical approved digest; reserved names include Payments apps, app_surfaces, payment_providers
B4 wrong webhook tenant webhook_scope.rs, auth middleware: route tenant admission, reject conflicting header app_secrets strict non-owner RLS
B5 global slow event lane events.rs: eight lanes, bounded batches, lease fences, retry and replay app_events, services, rust_connectors
B6 event disclosure event_contract.rs, event_projection.rs: family/own scopes plus minimized payloads app_events, Rust projection tests
B7 client-only surface ACL surface_grants.rs: actor/package/context/action-bound five-minute grant apps, app_surfaces, app_assets
B8 input/storage/drafts input_schema.rs, storage.rs, developer/drafts.rs: recursive bounds, database quotas, actor-private CAS autosave apps, app_preview, frontend safety tests

Operator service configuration

Use the server's serializer rather than an independent JSON hashing implementation:

cargo run --bin veyndo -- --app-digest extensions/apps/service-example/manifest.json
python3 scripts/build_app_ui.py service-example --output .run/service-example.html

The UI builder embeds the SDK into one self-contained HTML bundle and emits its SHA-256. Store that bundle on the configured service. Configure the reviewed manifest digest and exact UI path/digest server-side:

{
  "service_example": {
    "url": "https://apps.example.test",
    "uiUrl": "https://apps.example.test",
    "token": "SERVER_SECRET_INJECTED_BY_OPERATOR",
    "approvedDigests": ["RUST_CANONICAL_PACKAGE_SHA256"],
    "uiDigests": {"index.html": "SELF_CONTAINED_HTML_SHA256"}
  }
}

APP_SERVICES is operator-owned. A merchant cannot replace its URL or become that service by choosing its app ID. Exact bundled and frozen historical manifests remain approved for compatibility; altered content requires an explicit pin. Private origins need exact APP_SERVICE_PRIVATE_ORIGINS approval. Ordinary public egress uses HTTPS, blocks private/reserved addresses and mixed DNS answers, pins DNS and rejects redirects. Ambient proxy variables are ignored. A trusted private origin intentionally grants private-network access; it must never be merchant-controlled.

Register Ed25519 public keys in APP_PUBLISHERS as {"publisher":{"key_id":"BASE64_PUBLIC_KEY"}}. Only signatures using registered keys can occupy publisher_app. veyndo --sign-app MANIFEST.json consumes a base64 32-byte seed from stdin and returns the signed manifest and public key. Private seeds do not belong in app packages, environment JSON, logs or Git. A publisher signature does not automatically authorize an operator service or a changed UI bundle.

Credentials and surfaces

Per-shop/app service, incoming webhook and outgoing signature secrets reuse platform AES-GCM encryption. Authenticated associated data binds tenant, app, kind, digest and revision. Rotation uses a current personal team-manager identity, explicit approval and optimistic revision; GET returns metadata only. Once configured, an invalid or stale tenant secret fails closed rather than falling back to a global token. Existing operator bootstrap secrets remain usable only where no tenant secret was configured.

App callback credentials store only hashes, never raw tokens. Keys bind creator, tenant, app, package, scopes and expiry. Current membership is checked on every call; revocation, permission loss or package changes remove authority. Original app capabilities are stored separately from their mapped core rights: products.read cannot imply assets.read, and jobs.read cannot imply jobs.write. Migration 070 leaves legacy app keys without callback capabilities; rotate them with explicit consent before use. Core callbacks project personal data only with separate customers.pii consent. Surface grants are short-lived browser capabilities with an even narrower action/context boundary.

Pinned iframe HTML is limited to 1 MiB, UTF-8 and expected content type. The host first authorizes the surface and returns an /app-ui/{app}/{digest} document path. This path serves only operator-pinned, secret-free static UI code, with no actor context or credentials passed upstream. The document enforces sandbox allow-scripts in its HTTP CSP as well as the iframe attribute, including when opened directly. Its isolated response allows inline bundle code; the credential-bearing Studio keeps its stricter external-build-script policy. srcdoc is not used because it inherits Studio's CSP and prevents the approved guest code from starting. Unpinned or changed bytes fail closed. Document reads have an eight-request process bulkhead, bounded transport and no-store/no-referrer responses. The host sets an opaque script-only sandbox with restrictive CSP and communicates through source/nonce-checked SDK messages. Direct fetch and form submission are blocked. This is not a proof of zero exfiltration from hostile browser code: self-navigation and browser behavior remain a trust boundary. Review sensitive permissions and UI publishers, or prefer host-native views. No popup permission is implemented by default.

Limits and failure behavior

  • Wasm: 1 MiB linear memory, 10,000 table elements, bounded stack, four core instances per component, 64 pooled runtime instances/memories/tables per process. Typed hooks use 100,000 fuel, five 10 ms epochs and 128 read-only host calls.
  • Compile: four admitted blocking compiles/process, 32 fixed deduplication stripes, 128-entry digest-checked LRU cache, 32 KiB source. Five-second response deadline. A timeout cannot cancel the already-running compiler thread; its admission permit stays held until it exits. This is a bounded compiler gate, not OS process isolation.
  • Service calls: eight slots/tenant-app, sixteen/tenant and 64/process, five-second request deadline and 64 KiB bounded JSON input/output; excess gets 429. Tenant and app limits also use PostgreSQL leases across replicas; the process cap is local.
  • App storage: database trigger enforces 100,000 rows and 64 MiB/tenant-app, including relation writes and rollback. Files use existing product asset quotas; file callback is explicitly requested and uploads are limited to 8 MiB. Inline previews ≤256 KiB.
  • Input: depth twelve, 2,048 nodes, 1,000 array entries and bounded strings/properties; closed supplied schemas support nested object/array/number validation.
  • Preview: ten actor-private environments, one-hour access, twenty apps; no live service/provider effects. Expiry denies access; storage cleanup is an operator concern.
  • Jobs: fenced 60-second claims with explicit bounded lifetime, cancellation ACK and uncertain status after lease loss. Retrying requires review of external effects.

A validation failure rolls back the relevant transaction. SQL quotas also cover concurrent writers, unlike client-only checks. External sends remain at least once; no local transaction proves an external effect occurred only once.

Migration and deployment

Run numbered schema migrations with the dedicated provisioner. Migrations 101–102 consolidate managed app data into four fixed shared tables. Drain old Core writers, create and verify a backup, migrate and compare original records/references, then retire only the verified legacy tables before starting the new runtime. The hosted cutover completed on 15 October; other existing installations still require this coordinated maintenance. Follow the exact procedure and evidence.

Migration 058 is retained immutable history: it introduced the earlier physical tables. It is not the current storage architecture or a standalone upgrade plan. Strict serving uses separate restricted tenant/system logins, role-bound signed contexts and startup audits. Managed app installation never creates SQL objects. Migrations 059–070 add delivery fences, quota accounting, grants, drafts, activity, callback keys, encrypted secrets, private previews, multi-relations, recovery and jobs. Migration 070 stores the exact approved app capabilities separately from team rights. Historical callback keys have no implicit grants and must be renewed.

Update independent service receivers for complete batches and new precise event permissions. Rebuild/pin custom UIs. Existing services without new tenant secrets can keep operator bootstrap tokens, but must pin custom package digests. Existing callback keys and configured tenant secrets need explicit review after package changes. Check private Payments/Storyfront deployment separately; public declaration changes do not deploy their private implementation.

Lean proves selected extracted admission decisions. SQL/RLS correctness, encryption integration, Wasmtime, compiler limits, browsers and remote services rely on concrete regressions and trusted infrastructure. No complete-core certification, universal extension compatibility or production-scale guarantee is claimed.

App lifecycle follow-up — 12 October 2026

Network admission is shared by service actions, event deliveries, pinned UI fetches and remote payment adapters: 64 per process, 16 across a tenant, and eight per tenant/app. PostgreSQL resource leases enforce the tenant/app limits across replicas. Rotating app IDs cannot evade the tenant aggregate. Active counters are never discarded during idle-key cleanup. The real app_events regression holds 16 calls across two replicas and app IDs, rejects a seventeenth before the provider receives it, and confirms another tenant can still proceed.

Opaque guest frames additionally bound pending bridge calls to eight, IDs to 100 characters and serialized inputs to 64 KiB. Duplicate pending IDs are not forwarded. Package/context changes remount the frame; old nonce/source windows and late responses cannot act in its replacement. These are bridge/admission bounds, not a proof of CPU isolation for arbitrary browser JavaScript.

Managed record writes acquire the existing shared tenant/app package fence and compare the installed current digest inside their transaction. Package upgrades use the matching exclusive fence. A queued old-contract write returns conflict without changing records/outbox. Generic workflow declarations validate initial states, edges and immutable fields in this same write path; app-specific states remain in the app Manifest, not Commerce domain directories.

The ERP reference adapter is an independent non-root Rust process with app-only forced-RLS receipt tables. It reuses current scoped Core order callbacks and the shared event signature wire contract. Its own durable receipt joins manual and event entry points and preserves uncertain outcomes through restart. See the app-owned implementation and deployment boundary.

Additional action rights

Action.requiredPermissions contains up to eight known team scopes which must all be granted in addition to permission. Public actions cannot declare private additional rights. HTTP/native UI calls, MCP discovery/invocation, AI plans and job enqueue/claim/progress/retry use the same current actor check. App Studio visual recompilation and AI edits preserve these requirements. Service callback credentials retain their own independent scope and consent checks; a powerful service key never grants the calling user additional rights. For example, marketplace order import requires orders.write, payments.manage and customers.pii; orders.write alone cannot import a paid customer snapshot.

Installed settings and permanent integration credentials

App settings require a current personal administrator and the active package digest. Secrets are write-only in the Studio form, encrypted with the existing app-bound secret owner, and omitted from read responses. Reads expose only nonsecret fields, configured-key names, revision and digest. Stale package digests require explicit review/re-entry. PUT rejects undeclared keys, wrong types, foreign tenants, stale revisions and sandbox writes. Private service actions receive a server-owned _appSettings object; input cannot override it. Public actions and native browser views receive none. Values are not sandbox/release/history-restore units.

Permanent integration keys are an explicit option for current personal team.manage sessions. Their scopes remain intersected with current membership, revocation still applies, and integration credentials cannot mint permanent child keys. persistent_key_admissible has exact generated Lean claims and negative mutations; the proof covers that pure two-boolean predicate, not SQL, encryption, UI correctness or a service provider. Real HTTP/PostgreSQL tests cover those implemented admission boundaries.

The workbench guide documents test coverage and limits.