openvibers/openvibe.codes Commits

README (purpose, owns, does not own, depends on, grants, deploy, acceptance, launch rule, threat notes), STATUS alpha W20, .env.example, deploy (systemd unit, nginx vhost on 4900), CI on Node 22.22.1 (contracts check continue-on-error until release), capability, service manifest and codes.app-manifest@1 proposals

OP OpenVibers committed 23 Sept 2026 · 958890d607 · 11 files, +457 −50


Changes

ddiff --git a/.env.example b/.env.example
nnew file mode 100644
iindex 0000000..9d9c2ed
---- /dev/null
++++ b/.env.example
@@@ -0,0 +1,37 @@
++# OpenVibe.Codes — environment (production: /etc/openvibe/codes.env). Names only; never commit values.
++
++NODE_ENV=production
++PORT=4900
++HOST=127.0.0.1
++BASE_URL=https://openvibe.codes
++TRUST_PROXY=2
++CODES_DB_PATH=/var/lib/openvibe-codes/codes.db
++
++# OpenVibe.Network: SSO (OAuth client `codes`, PKCE S256), JWKS, the /api/v1/projects API, the registry.
++OV_NETWORK_URL=https://openvibe.network
++OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
++# OV_NETWORK_ISSUER=https://openvibe.network # defaults to OV_NETWORK_URL
++OV_OAUTH_CLIENT_ID=codes
++OV_OAUTH_CLIENT_SECRET=
++# OV_OAUTH_REDIRECT_URI=https://openvibe.codes/auth/callback
++
++# Signs the per-person form token (CSRF). Unset: a random per-process key (forms expire on restart).
++CODES_FORM_SECRET=
++COOKIE_SECURE=true
++
++# Codes staff (trust tiers, metadata only): Network admins, plus these usr_ subjects (comma list).
++CODES_STAFF_SUBJECTS=
++
++# Playgrounds call these with the APP's own token (defaults: the public origins in openvibe-contracts).
++# CODES_PLAYGROUND_EVENTS_URL=https://events.openvibe.network
++# CODES_PLAYGROUND_MEDIA_URL=https://openvibe.media
++# CODES_PLAYGROUND_MAX_UPLOAD_BYTES=1048576
++# CODES_PLAYGROUND_RUNS_PER_HOUR=60
++
++# Registry answers are cached this long (ms).
++# CODES_REGISTRY_TTL_MS=30000
++
++# OpenVibe.Events: the outbox relay (codes.app.published|deprecated|revoked) runs when both
++# EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise events wait in event_outbox.
++EVENTS_URL=http://127.0.0.1:4300
++# EVENTS_RELAY_INTERVAL_MS=2000
ddiff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
nnew file mode 100644
iindex 0000000..df7c0d2
---- /dev/null
++++ b/.github/workflows/ci.yml
@@@ -0,0 +1,22 @@
++name: ci
++on:
++ push:
++ branches: [main]
++ pull_request:
++jobs:
++ test:
++ runs-on: ubuntu-latest
++ steps:
++ - uses: actions/checkout@v4
++ - uses: actions/setup-node@v4
++ with:
++ node-version: 22.22.1 # production runs Node 22
++ cache: npm
++ - run: npm ci --no-audit --no-fund
++ - run: for f in $(find server public/js test -name '*.js'); do node --check "$f"; done
++ - run: npm test
++ # Codes' service manifest, its capabilities and codes.app-manifest@1 are proposals (docs/) until
++ # the lead releases them in openvibe-contracts; until then the check reports the placeholder
++ # manifest's range and the unreleased contract, so it does not fail the build.
++ - run: npx openvibe-contracts-check --service codes --src server
++ continue-on-error: true
ddiff --git a/README.md b/README.md
iindex bdff230..e50ddef 100644
---- a/README.md
++++ b/README.md
@@@ -1,79 +1,108 @@
# OpenVibe.Codes
--> The developer portal: registry and contract explorers, credentials, playgrounds, examples, mod/app publishing, governance.
++> The developer portal: projects and apps over OpenVibe.Network, scoped credentials, generated contract and SDK docs, OAuth and webhook tools, grant-respecting playgrounds, manifest validation and release metadata.
--**Status:** placeholder — planning only, no runnable code yet.
--**Domain:** `openvibe.codes`
--**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.2 and §15.5.
++**Status:** alpha (roadmap Wave 20). Runs and is tested against in-process stand-ins for Network, Events and Media. **Not deployed**; `openvibe.codes` still serves its placeholder from [OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites). See [STATUS.json](STATUS.json) for exactly what works and what does not.
++**Domain:** `openvibe.codes` · **Port:** 4900 · **Service id:** `codes`
++**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.17, §24.2 (proof flow 6), §30; binding decision ADR-014 in OpenVibe.Contracts.
**License:** AGPL-3.0 (same as every OpenVibe service).
## Purpose
--A real developer portal launched only once Contracts, SDK, the registry, Events and Media are genuinely usable. Until then the domain stays a placeholder that says so.
++A place where a developer outside the network goes from an OpenVibe account to a working, scoped integration using only public docs, the SDK and their own credentials. Codes is a **portal**: the identity authority (OpenVibe.Network) owns projects, apps, credentials, grants and quotas (ADR-014), and Codes shows and forwards what the signed-in person asks for, with that person's own Network token.
## Owns
--- project/app credentials and scope editor, OAuth callback helper, webhook tester and signed-event inspector
--- docs generated from published contract and SDK versions
--- playgrounds for Events/Realtime, Media uploads, Billing sandbox, Chat bots
--- mod/app manifest editor/validator, publish/release/deprecate/revoke, trust tiers as metadata
--- RFC process, compatibility/deprecation policy, contributor ladder, licensing and transparency pages
--- scoped vibe-coding session publication
++- **Release metadata** keyed to Network app ids: validated app and mod manifests, releases (`draft → published → deprecated → revoked`), an append-only release log.
++- **Trust tiers** (`untrusted`, `verified`, `trusted`, `platform-maintained`) — **metadata only**; a tier never grants, allows or bypasses anything.
++- **Playground run logs** (who ran what, outcome, stage, problem code; never a credential).
++- The events `codes.app.published`, `codes.app.deprecated`, `codes.app.revoked` (through the openvibe-sdk transactional outbox).
++- The generated reference (rendered from the pinned packages at boot; nothing hand-written that can drift) and the portal's pages.
## Does not own
--- the registry itself (Network), the contracts (Contracts), authority for any product
++- Projects, members, apps, credentials, grants, allowances, quotas, audit: **OpenVibe.Network** (`/api/v1/projects`). Codes never copies them.
++- Secrets: Network returns a client secret once; Codes renders it in that one response (`Cache-Control: no-store`) and keeps no copy. Tests scan every table, every log line and every later response for each secret.
++- Token issuance and quota enforcement: Network issues tokens; the service that owns a capability enforces its quota. Codes labels quotas "recorded limit — enforced by `<service>`".
++- The registry (Network) and the contracts (OpenVibe.Contracts).
--## Planned surfaces
++## What is here
--- `apps/web` SSR portal, `apps/api` project/app/release metadata, `packages/docs-gen`, `packages/playground`, `packages/templates`, `workers/`
++| Surface | Path | Notes |
++|---|---|---|
++| Portal | `/projects`, `/projects/:project`, `/projects/:project/apps/:app` | Create projects; add members; create sandbox/production apps (secret shown once); rotate and revoke credentials; request, approve, deny and revoke grants through a scope editor that offers only grantable capabilities (public, or partner when in the allowance) with each one's description, owner and visibility; quotas; audit (admin+). Network's errors are shown with its status, code, detail and request id. |
++| OAuth helper | `/oauth`, `/oauth/test-callback` | Explains authorization code + PKCE (S256) for apps, builds a test authorize URL, and a callback that shows `code`/`state`/`error` and **never exchanges** the code. |
++| Webhook tools | `/tools/webhooks` | Verify `X-OpenVibe-Signature: sha256=<HMAC>` against a raw body (in the browser with Web Crypto, or on the server without JavaScript, constant-time, secret dropped); generate a signed sample delivery for any event type the contracts catalog lists. |
++| Docs | `/docs/*` | Contracts (field tables, fixtures as examples, raw schema), capability catalog (grantable ones highlighted), event types, SDK reference from its `.d.ts` files, ADRs. Every page states the versions it was generated from. The service registry is read live from Network, with health as Network reports it. |
++| Playgrounds | `/projects/:project/apps/:app/playground` | Media upload (sandbox) and Events publish, run **as the app** with its own token (from its secret typed for that one request, or a pasted token). Refused — with the missing grant named, and nothing requested or called — unless Network lists the grant as approved. Sandbox apps only. |
++| Manifests | `/manifests/validate`, `/projects/:project/apps/:app/releases/new` | Validate `mods.mod-manifest@1` and app manifests with openvibe-contracts; create, publish, deprecate and revoke releases. |
++| Policy | `/policy/*` | Proposal process and the decision record; compatibility and deprecation policy rendered from ADR-002 and ADR-016 as published; licensing read from package metadata; transparency (what Codes stores and does not). |
++| API | `/api/v1/*` | Public release reads, manifest validation, docs versions; release management by an app with its own token (`codes.release.manage`, proposed). RFC 9457 problems. |
++| Machine | `/api/health`, `/api/ready`, `/release.json`, `/metrics` | Readiness is truthful (db and docs required; Network, JWKS, OAuth client and events relay reported as optional checks). `/metrics` answers loopback callers only. |
--## Data (authority tables / families)
++Everything is server-rendered and usable without JavaScript. The only scripts of Codes' own are optional: in-browser webhook verification and a copy button.
--- projects, apps, credentials metadata (no raw secrets), releases, trust status, usage/trace views
++## Depends on
--## Capabilities and events
++- **OpenVibe.Network** — SSO (OAuth client `codes`, PKCE S256), JWKS, `/api/v1/projects` (called server-side with the person's Network access token), the registry (`/api/v1/registry/services`, `/.well-known/openvibe`), client-credentials tokens (Codes' own for the events relay; the app's own in playgrounds).
++- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token.
++- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace.
++- **openvibe-contracts v0.26.0**, **openvibe-sdk v0.2.2**, **openvibe-shared v1.3.0** (pinned tag tarballs).
--- `codes.project.*`, `codes.app.*`, `codes.release.*`, `codes.credential.revoke`
++No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).
--Events: ``codes.app.published|deprecated|revoked``
++## Grants and registration the lead must add
--## Depends on
++In Network (`server/identity/principals.js` / `server/db/database.js`, as for coupons and host):
--- OpenVibe.Contracts
--- OpenVibe.SDK
--- OpenVibe.Network (registry, principals)
--- OpenVibe.Events
--- OpenVibe.Media
--- OpenVibe.Examples
++- OAuth client `codes`, name `OpenVibe.Codes`, redirect URI `https://openvibe.codes/auth/callback`.
++- Service grant `['codes', 'events.event.publish', 'openvibe.events', []]` (the outbox relay).
--## Acceptance (must be true before "done")
++In OpenVibe.Contracts (next release): the service manifest `docs/service-manifest-proposal.json`, the capabilities in `docs/capabilities-proposal/`, and the contract `codes.app-manifest@1` from `docs/contracts-proposal/`.
++
++For the playgrounds to succeed end to end (not Codes' code; configuration elsewhere): Network `DEV_SANDBOX_AUDIENCES` including `openvibe.media` (and `openvibe.events`), a staff-set allowance containing `media.object.upload` for the project, and a Media tenant keyed by the project id. `events.event.publish` is `internal` in contracts v0.26.0, so no app can publish events until Events publishes a public capability; the playground says so.
++
++## Configuration
++
++See [.env.example](.env.example). Required in production: `OV_OAUTH_CLIENT_SECRET`, `CODES_FORM_SECRET`, `BASE_URL`, `CODES_DB_PATH`. Optional: `EVENTS_URL` (relay), `CODES_STAFF_SUBJECTS`, `CODES_PLAYGROUND_*`, `CODES_REGISTRY_TTL_MS`.
++
++## Deploy (for the lead)
--- a new external developer goes from account to a working Media/event/capability integration with only public docs, SDK and credentials — no loopback `X-Internal-Key`
--- credentials can be scoped, rotated and revoked
--- the playground cannot exceed its project grants
++1. `git clone` to `/opt/openvibe.codes`; `npm ci --omit=dev` with Node 22.
++2. `/etc/openvibe/codes.env` from `.env.example` (secrets from the Network client registration).
++3. `deploy/systemd/openvibe-codes.service` → `/etc/systemd/system/`; `systemctl enable --now openvibe-codes`. State lives in `/var/lib/openvibe-codes`.
++4. `deploy/nginx/openvibe.codes.conf` → `/etc/nginx/sites-available/`, certificate for `openvibe.codes` + `www`, reload nginx.
++5. Check `curl -s http://127.0.0.1:4900/api/ready`.
++6. Only when the launch rule below holds: remove `openvibe.codes` from `OpenVibe.Sites/sites.json` and switch routing.
--## Bootstrap / extraction source
++## Development
--Today's scattered docs (Live streaming docs, Tools dev tools, repo READMEs) plus the new Contracts/SDK/registry surfaces.
++```bash
++npm install
++fnm exec --using=22.22.1 npm test # every test/*.test.js on temp databases with mock Network, Events, Media
++fnm exec --using=22.22.1 npm run dev # http://localhost:4900
++npx openvibe-contracts-check --service codes --src server
++```
++
++Tests: secrets never persisted or re-displayed; Network errors surfaced honestly (404/403/409/422/503, unreachable, expired session refresh); scope editor offers only grantable capabilities and refuses forged requests before Network sees them; generated docs match the pinned versions (every contract, capability, event type, SDK module); webhook verification (tampering, prefixes, lengths, constant-time); playgrounds refuse without the grant and call nothing; manifest validation; release lifecycle and events; no internal key anywhere; PKCE sign-in; readiness, release.json, loopback metrics; proposals valid against the contract schemas.
++
++## Acceptance (must be true before "done")
++
++- A new external developer goes from account to a working Media, event and capability integration with only public docs, the SDK and scoped credentials — no loopback key. **Codes' part is built; the path end to end also needs the Network registration, sandbox audiences, a project Media tenant and a public events capability listed above.**
++- Credentials can be inspected, scoped, rotated and revoked. **Yes, through Network's API (tested against a stand-in).**
++- The playground cannot exceed its project's grants. **Yes (tested).**
++- A published app or mod release carries compatibility and trust metadata. **Yes (tested).**
## Launch rule
--This repository does not make the product real, and the domain keeps its placeholder page on
--[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the
--following exist here (plan §12.12):
--
--1. an owning runtime with health/readiness endpoints and observability;
--2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals);
--3. server-rendered or static public routes that are useful without JavaScript;
--4. real persistence and end-to-end workflows;
--5. capability and event registration against `OpenVibe.Contracts`;
--6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour;
--7. acceptance tests proving the advertised functionality.
--
--The launch release removes the domain from `OpenVibe.Sites/sites.json`, switches routing and
--registers maturity in the ecosystem registry atomically. A placeholder is never counted as an
--implemented service.
++This repository does not make the product real, and the domain keeps its placeholder page on [OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the following exist (plan §12.12): an owning runtime with health/readiness and observability (**done**); canonical identity/auth integration (**done in code; the OAuth client is not registered**); server-rendered public routes useful without JavaScript (**done**); real persistence and end-to-end workflows (**persistence done; end-to-end blocked on the items above**); capability and event registration against OpenVibe.Contracts (**proposed, not released**); a migration/seed strategy (none needed: no data is imported; raw old developer keys are never imported), a security review and sitemap/robots behaviour (**sitemap and robots done**); acceptance tests proving the advertised functionality (**against stand-ins**). Roadmap binding: do not launch Codes as a developer portal while these paths are mocked.
++
++### Threat notes
++
++- Session tokens are httpOnly (unlike the navbar-readable `ov_token` elsewhere) because this site displays secrets; forms carry an HMAC form token and require a same-site `Origin`.
++- Secrets never reach logs: errors are logged without request bodies; playground failure details are scrubbed of the credential before they are stored or shown.
++- Codes never fetches a URL a developer types (the webhook tester produces a `curl` for their own endpoint instead).
++- Grant checks happen twice: Codes refuses to forward non-grantable capabilities, and Network decides. Playgrounds verify the app token's signature, subject, project, environment, audience and capability before calling anything.
---
ddiff --git a/STATUS.json b/STATUS.json
iindex 39c29fa..8468cf1 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -1,7 +1,42 @@
{
"repository": "OpenVibers/OpenVibe.Codes",
-- "stage": "placeholder",
-- "code": false,
++ "stage": "alpha",
++ "code": true,
++ "wave": "W20",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
-- "updated": "2026-09-21"
++ "runtime": "node 22, express 4, better-sqlite3 11",
++ "port": 4900,
++ "domain": "openvibe.codes",
++ "packages": [
++ "openvibe-contracts v0.26.0",
++ "openvibe-sdk v0.2.2",
++ "openvibe-shared v1.3.0"
++ ],
++ "summary": "The developer portal runs and is tested against in-process stand-ins for Network, Events and Media. It is not deployed, the OAuth client and grants it needs are not registered in Network yet, and openvibe.codes still serves the OpenVibe.Sites placeholder.",
++ "works": [
++ "Sign-in with OpenVibe.Network (authorization code + PKCE S256), session tokens in httpOnly cookies",
++ "Projects, members, apps, credentials (rotate, revoke), grants (request, approve, deny, revoke), quotas and audit over Network's /api/v1/projects API with the person's own token; Network's errors shown as Network answered them",
++ "Client secrets shown once, in the create/rotate response only, with no-store; never stored or logged (tested)",
++ "Scope editor offering only capabilities apps can be granted (public, or partner when in the allowance)",
++ "Docs generated at boot from openvibe-contracts v0.26.0 and openvibe-sdk v0.2.2: contracts with field tables and fixtures, capability catalog, event types, SDK reference; service registry read live from Network with its health",
++ "OAuth callback helper and a test callback that never exchanges the code",
++ "Webhook signature verifier (server or in-browser) and signed sample deliveries for catalogued event types",
++ "Playgrounds (Media upload, Events publish) that run with the app's own token and refuse without the grant",
++ "App and mod manifest validation; release metadata (draft, publish, deprecate, revoke) with codes.app.* events through the SDK outbox; trust tiers as staff-set metadata",
++ "Policy pages rendered from the published ADRs; licensing read from package metadata"
++ ],
++ "notYet": [
++ "Not deployed; the Network OAuth client `codes` and the events.event.publish grant are not registered",
++ "The Events playground cannot run for any app: events.event.publish is internal in openvibe-contracts v0.26.0 and Network never grants it to apps",
++ "The Media playground needs Network to accept sandbox tokens for openvibe.media (DEV_SANDBOX_AUDIENCES) and Media to have a tenant per project id; neither exists yet",
++ "codes.release.manage and codes.release.read are proposals; until Contracts releases them no app can hold the release-management grant",
++ "No usage, trace or error dashboards: no service reports per-project usage yet",
++ "Billing sandbox, Chat bot and Realtime playgrounds; CLI and scaffolding; templates; project data export",
++ "Consent screen for third-party production apps (a Network gap)"
++ ],
++ "capabilitiesRegistered": false,
++ "serviceManifestRegistered": false,
++ "deployed": false,
++ "placeholderInSites": true,
++ "updated": "2026-09-22"
}
ddiff --git a/deploy/nginx/openvibe.codes.conf b/deploy/nginx/openvibe.codes.conf
nnew file mode 100644
iindex 0000000..b2abb4a
---- /dev/null
++++ b/deploy/nginx/openvibe.codes.conf
@@@ -0,0 +1,100 @@
++## ═══════════════════════════════════════════════════════════════
++## openvibe.codes — Nginx configuration (REFERENCE)
++##
++## Deployed to /etc/nginx/sites-available/openvibe.codes.conf — keep them in sync and reload nginx
++## after changing it. The Node app (port 4900, systemd unit openvibe-codes) renders every page;
++## nginx terminates TLS, redirects www → apex, rate-limits sign-in, forms and API, and compresses text.
++##
++## No proxy_cache: the app decides cacheability per response (public only for docs and policy pages
++## rendered for anonymous visitors; private, no-store for everything personal, and no-store for the
++## one page that shows a new client secret). Never log request bodies here: forms carry secrets.
++## ═══════════════════════════════════════════════════════════════
++
++limit_req_zone $binary_remote_addr zone=ovcodes_auth:10m rate=10r/m;
++limit_req_zone $binary_remote_addr zone=ovcodes_forms:10m rate=2r/s;
++limit_req_zone $binary_remote_addr zone=ovcodes_api:10m rate=5r/s;
++
++server {
++ listen 80;
++ listen [::]:80;
++ server_name openvibe.codes www.openvibe.codes;
++
++ location /.well-known/acme-challenge/ { root /var/www/certbot; }
++ location / { return 301 https://openvibe.codes$request_uri; }
++}
++
++server {
++ listen 443 ssl;
++ listen [::]:443 ssl;
++ http2 on;
++ server_name www.openvibe.codes;
++
++ ssl_certificate /etc/letsencrypt/live/openvibe.codes/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/openvibe.codes/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
++
++ return 301 https://openvibe.codes$request_uri;
++}
++
++server {
++ listen 443 ssl;
++ listen [::]:443 ssl;
++ http2 on;
++ server_name openvibe.codes;
++
++ ssl_certificate /etc/letsencrypt/live/openvibe.codes/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/openvibe.codes/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
++
++ access_log /var/log/nginx/openvibe.codes.access.log;
++ error_log /var/log/nginx/openvibe.codes.error.log;
++
++ # Playground uploads are at most 1 MB (CODES_PLAYGROUND_MAX_UPLOAD_BYTES); forms are small.
++ client_max_body_size 2m;
++
++ add_header X-Content-Type-Options nosniff always;
++ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
++
++ gzip on;
++ gzip_proxied any;
++ gzip_vary on;
++ gzip_comp_level 5;
++ gzip_min_length 1024;
++ gzip_types text/css application/javascript text/javascript application/json application/problem+json application/schema+json image/svg+xml text/plain application/xml;
++
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
++ proxy_set_header X-Forwarded-Proto $scheme;
++
++ # ── Network SSO session endpoints (strict) ──────────────
++ location /auth/ {
++ limit_req zone=ovcodes_auth burst=10 nodelay;
++ proxy_pass http://127.0.0.1:4900;
++ }
++
++ # ── API ─────────────────────────────────────────────────
++ location /api/ {
++ limit_req zone=ovcodes_api burst=20 nodelay;
++ proxy_pass http://127.0.0.1:4900;
++ }
++
++ # ── State-changing forms (projects, releases, tools, staff) ──
++ location ~ ^/(projects|releases|tools|manifests|staff) {
++ limit_req zone=ovcodes_forms burst=20 nodelay;
++ proxy_pass http://127.0.0.1:4900;
++ }
++
++ # ── Prometheus metrics: host-local scrapers only (http://127.0.0.1:4900/metrics) ──
++ location = /metrics { return 404; }
++
++ # ── Pages, docs, robots.txt, sitemap, static assets ─────
++ location / {
++ proxy_pass http://127.0.0.1:4900;
++ }
++
++ location ~ /\.(?!well-known) {
++ deny all;
++ }
++}
ddiff --git a/deploy/systemd/openvibe-codes.service b/deploy/systemd/openvibe-codes.service
nnew file mode 100644
iindex 0000000..a478f6e
---- /dev/null
++++ b/deploy/systemd/openvibe-codes.service
@@@ -0,0 +1,32 @@
++[Unit]
++Description=OpenVibe.Codes — the developer portal (projects over Network's API, generated docs, tools, release metadata)
++# Network (sign-in, JWKS, the projects API, registry, tokens) and Events (the outbox relay) should be
++# up first, but Codes degrades honestly without them: docs, tools and policy pages serve; project
++# pages show Network's failure; events wait in the outbox (see /api/ready).
++After=network.target
++Wants=openvibe-network.service
++
++[Service]
++Type=simple
++User=ubuntu
++Group=ubuntu
++WorkingDirectory=/opt/openvibe.codes
++EnvironmentFile=/etc/openvibe/codes.env
++Environment=NODE_ENV=production
++Environment=PORT=4900
++Environment=PATH=/usr/local/bin:/usr/bin:/bin
++ExecStart=/usr/bin/env node /opt/openvibe.codes/server/index.js
++Restart=always
++RestartSec=5
++KillSignal=SIGTERM
++TimeoutStopSec=10
++NoNewPrivileges=true
++PrivateTmp=true
++ProtectSystem=strict
++ProtectHome=true
++# Codes' own database lives in /var/lib/openvibe-codes; the code tree stays read-only.
++StateDirectory=openvibe-codes
++Environment=CODES_DB_PATH=/var/lib/openvibe-codes/codes.db
++
++[Install]
++WantedBy=multi-user.target
ddiff --git a/docs/capabilities-proposal/codes.release.manage.json b/docs/capabilities-proposal/codes.release.manage.json
nnew file mode 100644
iindex 0000000..5c7573c
---- /dev/null
++++ b/docs/capabilities-proposal/codes.release.manage.json
@@@ -0,0 +1,19 @@
++{
++ "id": "codes.release.manage",
++ "version": "1.0",
++ "owner": "codes",
++ "status": "active",
++ "visibility": "public",
++ "description": "An app manages release metadata for ITSELF with its own token (audience openvibe.codes): create a release from a validated app or mod manifest, publish, deprecate (with a reason and an optional replacement) and revoke it. The token's sub must be app:<the app in the path>; people manage releases in the portal with their project role instead. Trust tiers are not part of this capability (staff metadata only).",
++ "inputSchema": "codes.app-manifest@1",
++ "permissions": ["codes:releases:write"],
++ "resourceConstraints": ["owner", "project"],
++ "quotaClass": "codes-release-write",
++ "events": ["codes.app.published", "codes.app.deprecated", "codes.app.revoked"],
++ "implementedBy": [
++ "POST /api/v1/apps/:app/releases",
++ "POST /api/v1/releases/:id/publish",
++ "POST /api/v1/releases/:id/deprecate",
++ "POST /api/v1/releases/:id/revoke"
++ ]
++}
ddiff --git a/docs/capabilities-proposal/codes.release.read.json b/docs/capabilities-proposal/codes.release.read.json
nnew file mode 100644
iindex 0000000..5eb6877
---- /dev/null
++++ b/docs/capabilities-proposal/codes.release.read.json
@@@ -0,0 +1,18 @@
++{
++ "id": "codes.release.read",
++ "version": "1.0",
++ "owner": "codes",
++ "status": "active",
++ "visibility": "public",
++ "description": "Read public release metadata: an app's published, deprecated and revoked releases with their manifests, compatibility ranges and the app's trust tier (metadata only; it never grants anything). Drafts are never returned. Public; no token needed.",
++ "outputSchema": "codes.app-manifest@1",
++ "permissions": ["codes:releases:read"],
++ "resourceConstraints": ["none"],
++ "quotaClass": "codes-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/apps/:app/releases",
++ "GET /api/v1/apps/:app/trust",
++ "GET /api/v1/releases/:id"
++ ]
++}
ddiff --git a/docs/contracts-proposal/README.md b/docs/contracts-proposal/README.md
nnew file mode 100644
iindex 0000000..622fac8
---- /dev/null
++++ b/docs/contracts-proposal/README.md
@@@ -0,0 +1,17 @@
++# Contracts proposal: codes.app-manifest@1
++
++`codes/app-manifest.v1.json` is the schema OpenVibe.Codes validates app manifests with. Until
++OpenVibe.Contracts publishes it, Codes compiles this file into openvibe-contracts' own validator
++(so its `$ref` to `identity/subject-ref.v1.json` resolves to the released schema); once a
++contracts release contains `codes.app-manifest`, Codes uses `contracts.validate('codes.app-manifest@1')`
++instead, with no code change.
++
++Proposed catalog entry:
++
++```json
++{ "id": "codes.app-manifest", "version": "1.0.0", "owner": "codes", "schema": "codes/app-manifest.v1.json",
++ "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-014" }
++```
++
++Fixtures to add with it: the template `server/domain/manifests.js` produces (valid) and one with an
++`internal` capability or a missing `project_id` (invalid).
ddiff --git a/docs/contracts-proposal/codes/app-manifest.v1.json b/docs/contracts-proposal/codes/app-manifest.v1.json
nnew file mode 100644
iindex 0000000..35f2c41
---- /dev/null
++++ b/docs/contracts-proposal/codes/app-manifest.v1.json
@@@ -0,0 +1,68 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/codes/app-manifest.v1.json",
++ "title": "AppManifest",
++ "description": "PROPOSAL (OpenVibe.Codes, roadmap Wave 20): an app release as the platform knows it. The app is a Network developer app (ADR-014): its id is the Network app id, its capabilities are only a request (the grants in Network are the authority), and trust tiers are metadata that never change a grant check. OpenVibe.Codes validates with this schema, loaded into openvibe-contracts' validator, until Contracts publishes it as codes.app-manifest@1.",
++ "type": "object",
++ "required": ["id", "name", "version", "publisher", "project_id", "capabilities", "compatibility"],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The Network app id (its client_id). Its principal subject is app:<id>."
++ },
++ "name": { "type": "string", "minLength": 1, "maxLength": 80 },
++ "version": {
++ "type": "string",
++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
++ "description": "Semantic version of this release."
++ },
++ "description": { "type": "string", "maxLength": 1000 },
++ "publisher": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "Who publishes the release: the app itself or a member of its project."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The Network project that owns the app. Tenancy in other services is keyed by it (ADR-014)."
++ },
++ "environment": { "enum": ["sandbox", "production"], "description": "The app's environment. Apps never change environment." },
++ "capabilities": {
++ "description": "Capability ids the app asks for (3+ segments). Only active public (or partner, by staff allowance) capabilities can ever be granted to apps.",
++ "type": "array",
++ "maxItems": 64,
++ "uniqueItems": true,
++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" }
++ },
++ "events": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "consumes": {
++ "description": "Event types (or a trailing .* family) the app subscribes to.",
++ "type": "array", "maxItems": 64, "uniqueItems": true,
++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }
++ }
++ }
++ },
++ "redirect_uris": {
++ "description": "Informational copy of the redirect URIs registered in Network (Network's list is the authority).",
++ "type": "array", "maxItems": 10, "uniqueItems": true,
++ "items": { "type": "string", "maxLength": 500 }
++ },
++ "homepage": { "type": "string", "pattern": "^https://[^\\s]{1,2000}$" },
++ "repository": { "type": "string", "pattern": "^https://[^\\s]{1,2000}$" },
++ "license": { "type": "string", "minLength": 1, "maxLength": 100, "description": "SPDX license expression." },
++ "compatibility": {
++ "type": "object",
++ "required": ["contracts"],
++ "additionalProperties": false,
++ "properties": {
++ "contracts": { "type": "string", "minLength": 1, "maxLength": 100, "description": "Semver range of openvibe-contracts releases it was built against." },
++ "sdk": { "type": "string", "minLength": 1, "maxLength": 100, "description": "Semver range of openvibe-sdk releases it uses." }
++ }
++ }
++ }
++}
ddiff --git a/docs/service-manifest-proposal.json b/docs/service-manifest-proposal.json
nnew file mode 100644
iindex 0000000..47dcf67
---- /dev/null
++++ b/docs/service-manifest-proposal.json
@@@ -0,0 +1,30 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": ">=0.26.0 <1.0.0"
++ },
++ "id": "codes",
++ "name": "OpenVibe.Codes",
++ "status": "alpha",
++ "repository": "OpenVibers/OpenVibe.Codes",
++ "domains": [
++ "openvibe.codes"
++ ],
++ "publicOrigin": "https://openvibe.codes",
++ "health": "/api/health",
++ "ready": "/api/ready",
++ "capabilities": [
++ "codes.release.read",
++ "codes.release.manage"
++ ],
++ "eventsProduced": [
++ "codes.app.published",
++ "codes.app.deprecated",
++ "codes.app.revoked"
++ ],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "codes.*"
++ ],
++ "notes": "Port 4900, unit openvibe-codes.service, state /var/lib/openvibe-codes. The developer portal (ADR-014): projects, members, apps, credentials, grants and quotas are OWNED BY NETWORK and shown through Network's /api/v1/projects API with the signed-in person's own token; Codes stores none of them and never a secret. Codes owns only manifests, releases (+ append-only release_log), trust tiers (metadata) and playground run logs in its own SQLite. Consumes OpenVibe.Network (SSO with PKCE S256 as OAuth client codes, JWKS, the projects API, the registry, client-credentials) and publishes through OpenVibe.Events (events.event.publish). Playgrounds call Events and Media with the APP's own token only. Docs are generated at boot from the pinned openvibe-contracts and openvibe-sdk."
++}