openvibers/openvibe.contracts Commits
0.279.0: Website Connect v0 (CON-1 contracts, ADR-057 accepted, OpenVibe Connect live) (#288)
Changes
@@@ -1,5 +1,21 @@# Changelog++## 0.279.0 — 2026-11-02++++- **Website Connect v0: OpenVibe Connect is live** (CON-1 landed with the OQ-13 review; ADR-057 accepted). The nine++ CON-1 contracts join the catalog, all additive: `website.share@1`, `website.share-create-request@1`,++ `website.share-connect-request@1`, `website.policy@1`, `website.audit-entry@1`, `connect.device@1`,++ `connect.policy@1`, `events.hook@1` and `events.hook-delivery@1`, with valid and invalid fixtures. Id prefixes++ `shr`, `dev`, `hok`, `pol` and `aud`.++- **`manifests/services/connect.json`** (live, connect.openvibe.website, port 5240, ov_connect, site position 51 in++ "make") replaces CON-1's placeholder `website.json`: one id, `connect`, for the repository, ovhost, the database and++ the OAuth client. It owns `website.*` and `connect.*`; the 20 capabilities stay `planned`, now owned by `connect`,++ and none is served or granted.++- **ADR-057's review** decides v0: the WebSocket fallback through Cloudflare inside Connect (no UDP, no DNS-only++ record), names `<label>--<user>.openvibe.website` (Space names never contain "--"), a share-scoped connect key until++ the Node plugin, the gate's host-only signed cookie, modes owner/named and staff-first signin, no public shares++ until CON-9 and CON-21.++## 0.278.0 — 2026-11-02- **`live.index_document.upserted` v1 takes type `doc`** (expand only): one of Live's guides, `openvibe.live/docs/<name>`@@@ -8734,6 +8734,87 @@"visibility": "first-party","compatibility": "backward","status": "active"++ },++ {++ "id": "website.share",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "website/share.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "website.share-create-request",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "website/share-create-request.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "website.share-connect-request",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "website/share-connect-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "website.policy",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "website/policy.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "website.audit-entry",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "website/audit-entry.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "connect.device",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "connect/device.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "connect.policy",++ "version": "1.0.0",++ "owner": "connect",++ "schema": "connect/policy.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "events.hook",++ "version": "1.0.0",++ "owner": "events",++ "schema": "events/hook.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "events.hook-delivery",++ "version": "1.0.0",++ "owner": "events",++ "schema": "events/hook-delivery.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"}]}nnew file mode 100644@@@ -0,0 +1,251 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/connect/device.v1.json",++ "title": "ConnectDevice",++ "description": "connect.device@1: one device in a private Connect mesh, as its owner sees it. A device belongs to exactly one tenant (the person's own network or a project's), is identified by a Network node principal, and carries only self-reported posture. The mesh control-plane's internal node id, pre-auth key and WireGuard keys are not part of this document.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "tenant",++ "name",++ "status",++ "os",++ "version",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who owns the device."++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The project whose mesh the device joins; null for a personal mesh."++ },++ "tenant": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A person's own private network."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "project"++ },++ "id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A project's private network."++ }++ ],++ "description": "The isolation boundary: one headscale tenant per workspace in v1, never one shared tailnet for all consumer devices."++ },++ "node_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The Network node principal the device presents; null for an agent or server that joins through a pre-auth key."++ },++ "name": {++ "type": "string",++ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",++ "description": "One DNS label in the mesh, shown as name.owner (e.g. pc.alice); never a public hostname."++ },++ "status": {++ "enum": [++ "pending",++ "online",++ "offline",++ "removed"++ ],++ "description": "pending waits for the owner's approval of a pre-auth join; removed is terminal and its keys are revoked."++ },++ "os": {++ "enum": [++ "linux",++ "macos",++ "windows",++ "ios",++ "android",++ "other"++ ]++ },++ "arch": {++ "enum": [++ "amd64",++ "arm64",++ "386",++ "arm",++ "other"++ ]++ },++ "version": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "The Node release running on the device."++ },++ "addresses": {++ "type": "array",++ "maxItems": 8,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "The device's private mesh addresses; never a public address."++ },++ "tags": {++ "type": "array",++ "maxItems": 50,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,62}$"++ }++ },++ "posture": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "Self-reported posture, a signal not a guarantee; checked_at says when it was read.",++ "properties": {++ "node_current": {++ "type": "boolean"++ },++ "os_patched": {++ "type": "boolean"++ },++ "firewall_on": {++ "type": "boolean"++ },++ "disk_encryption": {++ "type": "boolean"++ },++ "risky_ports": {++ "type": "array",++ "maxItems": 200,++ "uniqueItems": true,++ "items": {++ "type": "integer",++ "minimum": 1,++ "maximum": 65535++ }++ },++ "checked_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "required": [++ "node_current",++ "os_patched",++ "firewall_on",++ "disk_encryption",++ "risky_ports",++ "checked_at"++ ]++ },++ "routes": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "Subnet routes the device advertises, e.g. 192.168.1.0/24; empty for none."++ },++ "exit_node": {++ "type": "boolean",++ "description": "Whether the owner allows the device to act as an exit node for this tenant only; never a public proxy."++ },++ "approved_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "approved_by": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who approved the join; null while pending."++ },++ "last_seen_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When an unapproved or temporary device stops being valid; null for no expiry."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,213 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/connect/policy.v1.json",++ "title": "ConnectPolicy",++ "description": "connect.policy@1: the grants, routes and exit-node settings of one private Connect tenant. It is deliberately tenant-scoped: a rule never names a device, person or tag from another tenant, and default_action is deny unless the owner opens a path. It describes intent, not the control plane's internal ACL file or keys.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "tenant",++ "version",++ "rules",++ "default_action",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "tenant": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A person's own private network."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "project"++ },++ "id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A project's private network."++ }++ ],++ "description": "The one tenant this policy can affect; a rule or route naming another tenant's id is invalid."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "Bumped on every change; a device keeps the version it last applied."++ },++ "default_action": {++ "enum": [++ "allow",++ "deny"++ ],++ "description": "deny unless the owner explicitly opens a path."++ },++ "rules": {++ "type": "array",++ "maxItems": 200,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "action",++ "sources",++ "destinations"++ ],++ "properties": {++ "action": {++ "enum": [++ "allow",++ "deny"++ ]++ },++ "sources": {++ "type": "array",++ "minItems": 1,++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/selector"++ }++ },++ "destinations": {++ "type": "array",++ "minItems": 1,++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/selector"++ }++ },++ "ports": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/port"++ },++ "description": "TCP/UDP ports or ranges; omit for all ports."++ },++ "description": {++ "type": "string",++ "maxLength": 200++ }++ }++ }++ },++ "exit_node": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "properties": {++ "enabled": {++ "type": "boolean"++ },++ "device_ids": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ "required": [++ "enabled",++ "device_ids"++ ],++ "description": "Exit nodes are usable only by this tenant's devices; no public share may target one."++ },++ "subnet_routes": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "device_id",++ "cidr"++ ],++ "properties": {++ "device_id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "cidr": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ }++ }++ }++ },++ "updated_by": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "selector": {++ "type": "string",++ "pattern": "^(?:\\*|(?:user|device|tag|project):[A-Za-z0-9_.:-]+)$",++ "description": "*, or user:<usr_\u2026>, device:<dev_\u2026>, tag:<name> or project:<prj_\u2026>; all within this tenant."++ },++ "port": {++ "type": "string",++ "pattern": "^(?:\\*|[0-9]{1,5}(?:-[0-9]{1,5})?)$",++ "description": "* for any port, 443, or a range such as 8000-8100."++ }++ }++}nnew file mode 100644@@@ -0,0 +1,136 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/hook-delivery.v1.json",++ "title": "EventsHookDelivery",++ "description": "events.hook-delivery@1: one attempt by OpenVibe.Events to deliver an event to a hook. It carries the outcome, timing, size and hash of the payload, never the payload itself, its headers, the signature or the hook's secret. A delivery is retried inside its hook's window and dead-lettered after it; replay creates a new attempt that names the one it replays.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "hook_id",++ "event_id",++ "event_type",++ "attempt",++ "status",++ "payload_bytes",++ "payload_sha256",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "hook_id": {++ "type": "string",++ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "event_id": {++ "type": "string",++ "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "event_type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "attempt": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100++ },++ "status": {++ "enum": [++ "queued",++ "delivering",++ "delivered",++ "failed",++ "dead"++ ],++ "description": "failed is a retryable attempt; dead is the last, after the hook's window. A 2xx answer marks delivered."++ },++ "request_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 64,++ "description": "The delivery request id, for tracing; not a secret."++ },++ "response_code": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 100,++ "maximum": 599++ },++ "latency_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "payload_bytes": {++ "type": "integer",++ "minimum": 0,++ "maximum": 1048576++ },++ "payload_sha256": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$",++ "description": "The SHA-256 of the signed body, so a consumer can check it without the body being stored here."++ },++ "signature_version": {++ "const": "v2"++ },++ "next_attempt_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "attempted_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "completed_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "dead_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "error": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 400,++ "description": "A short transport or status reason; never a response body."++ },++ "replay_of": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The failed or dead attempt this one replays."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,216 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/hook.v1.json",++ "title": "EventsHook",++ "description": "events.hook@1: one durable webhook subscription managed through OpenVibe.Website Connect, delivered by OpenVibe.Events. The hook names its share, the public ingress path, the event topics it accepts, filters/transform, the retry window and the signature version. The signing secret is shown once when the hook is created or its secret is rotated; secret_set says one exists, and no field of this document ever carries the value.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "share_id",++ "name",++ "endpoint",++ "topics",++ "status",++ "signature",++ "retry",++ "secret_set",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "share_id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The share whose tunnel receives deliveries; delivery never goes to an arbitrary host."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "endpoint": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://",++ "description": "The public ingress, https://hooks.openvibe.website/h/<id>; Connect also records the on-share path https://<share host>/__ov/hook/<id>."++ },++ "topics": {++ "type": "array",++ "minItems": 1,++ "maxItems": 50,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Event types this hook accepts, e.g. github.push.received or stripe.invoice.paid; Event's topic patterns may be used by the delivery filter."++ },++ "filters": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "path"++ ],++ "properties": {++ "path": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "A JSONPath into the event payload."++ },++ "equals": {++ "oneOf": [++ {++ "type": "string",++ "maxLength": 500++ },++ {++ "type": "number"++ },++ {++ "type": "boolean"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ }++ },++ "transform": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "properties": {++ "headers": {++ "type": "object",++ "maxProperties": 20,++ "additionalProperties": {++ "type": "string",++ "maxLength": 500++ },++ "description": "Headers added to the delivery; never the hook's signing secret."++ },++ "body_template": {++ "type": "string",++ "maxLength": 10000,++ "description": "An optional template applied to the event; the raw payload is used when absent."++ }++ }++ },++ "retry": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "window_seconds",++ "max_attempts"++ ],++ "properties": {++ "window_seconds": {++ "type": "integer",++ "minimum": 60,++ "maximum": 604800,++ "description": "How long Events keeps retrying while the share is offline; seven days is the hook default."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100++ }++ }++ },++ "signature": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "version",++ "tolerance_seconds"++ ],++ "properties": {++ "version": {++ "const": "v2"++ },++ "tolerance_seconds": {++ "type": "integer",++ "minimum": 1,++ "maximum": 3600,++ "description": "How far a signed timestamp may drift before the Node refuses it; 300 is the default."++ }++ }++ },++ "max_payload_bytes": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1048576,++ "default": 1048576++ },++ "status": {++ "enum": [++ "active",++ "paused",++ "revoked"++ ]++ },++ "secret_set": {++ "type": "boolean",++ "description": "True once a signing secret exists. The secret itself is returned only in the create/rotate answer and is never stored in this contract, a delivery record or an audit entry."++ },++ "secret_rotated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "last_delivery_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "dead_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When the hook stopped receiving deliveries after its retry window; null while it is live."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "href": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,146 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/audit-entry.v1.json",++ "title": "WebsiteAuditEntry",++ "description": "website.audit-entry@1: one decision row for a Share, as the owner or a moderator sees it. It answers who reached (or tried to reach) a share, from where, what method and path, what the policy decided and how much crossed the edge. It deliberately carries no request or response body, headers, cookies, bearer or tunnel tokens, query string, password or hook payload: query strings are stripped before this record is made and bodies are captured, if at all, by the separate opt-in inspector with its own redaction.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "share_id",++ "request_id",++ "decision",++ "at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^aud_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "share_id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "request_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64,++ "description": "The edge request id, for joining a row to a captured request and a trace; never a secret."++ },++ "subject": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "Who the policy identified; absent for an anonymous request."++ },++ "ip": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 45,++ "description": "The observed client address (IPv4 or IPv6). Behind Cloudflare it is CF-Connecting-IP, trusted only from Cloudflare's ranges."++ },++ "country": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{2}$",++ "description": "ISO 3166-1 alpha-2 country from the edge's trusted lookup."++ },++ "user_agent": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 512++ },++ "method": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3,10}$"++ },++ "path": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2048,++ "description": "Path only, with the query string removed. No body, header, cookie or token is ever stored here."++ },++ "status": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 100,++ "maximum": 599++ },++ "decision": {++ "enum": [++ "allow",++ "deny",++ "challenge",++ "redirect",++ "error"++ ],++ "description": "The policy outcome: allow, deny, a bot challenge, an OIDC/sign-in redirect, or an edge error."++ },++ "reason": {++ "type": "string",++ "maxLength": 120,++ "description": "A stable, short reason such as policy.denied or policy.password_required; never free-form request data."++ },++ "policy_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "policy_version": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "bytes_in": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "bytes_out": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "edge_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80++ },++ "region": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40++ },++ "at": {++ "type": "string",++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,231 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/policy.v1.json",++ "title": "WebsitePolicy",++ "description": "website.policy@1: how a Share decides who may reach it and under what conditions (public, OpenVibe sign-in, named people or projects, password, one-time link, expiry), plus IP/country rules, rate limits, a bot check, a WAF preset and required device posture. mode is the only field required except that expiry also requires a non-null expires_at. id, owner and version are present when Connect returns a stored policy. Password and one-time policies stay inactive until their credential or link is configured through the CON-5 API. A password is stored as a one-way hash beside this record and is never part of this document; password_set only says one exists.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "mode"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Present on a stored policy; omitted when the policy is written inline with a new share."++ },++ "owner": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who owns the policy; set from the token when a policy is created."++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The project scope, when the policy belongs to one."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "mode": {++ "enum": [++ "public",++ "signin",++ "named",++ "password",++ "one_time",++ "expiry"++ ],++ "description": "public: anyone with the link after the abuse screen; signin: any OpenVibe account (optionally a project); named: allowed_subjects/allowed_projects only; password: a shared password; one_time: single-use links; expiry: public access only until expires_at. An expires_at may also cap any other mode."++ },++ "allowed_subjects": {++ "type": "array",++ "maxItems": 500,++ "uniqueItems": true,++ "items": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "description": "Explicit people or agents allowed in named mode; a project's members are allowed through allowed_projects instead."++ },++ "allowed_projects": {++ "type": "array",++ "maxItems": 100,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "description": "Projects whose members may reach a named or signin share."++ },++ "ip_allow": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "CIDR ranges allowed before any other rule; empty means no IP allowlist."++ },++ "ip_deny": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "CIDR ranges always refused; evaluated before the allowlist."++ },++ "country_allow": {++ "type": "array",++ "maxItems": 250,++ "items": {++ "type": "string",++ "pattern": "^[A-Z]{2}$"++ },++ "description": "ISO 3166-1 alpha-2 country codes allowed; empty means all."++ },++ "country_deny": {++ "type": "array",++ "maxItems": 250,++ "items": {++ "type": "string",++ "pattern": "^[A-Z]{2}$"++ }++ },++ "rate_limits": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "per_share_per_minute": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000000++ },++ "per_ip_per_minute": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100000++ },++ "max_connections": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100000++ },++ "max_request_bytes": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1073741824++ }++ }++ },++ "bot_check": {++ "enum": [++ "none",++ "turnstile"++ ],++ "default": "none"++ },++ "waf_preset": {++ "enum": [++ "none",++ "standard",++ "strict",++ "api"++ ],++ "default": "standard",++ "description": "Defense in depth at the edge; Cloudflare's managed rules run in front of it for HTTP/WS."++ },++ "posture_required": {++ "type": "array",++ "maxItems": 10,++ "uniqueItems": true,++ "items": {++ "enum": [++ "node_current",++ "os_patched",++ "firewall_on",++ "disk_encryption",++ "no_risky_ports"++ ]++ },++ "description": "Self-reported device posture a share may require; a signal, not a guarantee."++ },++ "password_set": {++ "type": "boolean",++ "description": "Read-only: true when a password hash exists. The hash itself never leaves Connect."++ },++ "password_hint": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 120,++ "description": "An optional hint shown on the password page; never the password."++ },++ "one_time_max_uses": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000,++ "description": "For one_time mode; 1 unless the owner allows more."++ },++ "session_ttl_seconds": {++ "type": "integer",++ "minimum": 60,++ "maximum": 604800,++ "description": "How long a host-only __Host-ov_session on the share host stays valid after sign-in."++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "The absolute end of the policy; required and non-null for expiry mode, optional for other modes."++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "The signed policy version the edge caches and checks on every request; present on a stored policy."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "if": {++ "properties": {++ "mode": {++ "const": "expiry"++ }++ },++ "required": [++ "mode"++ ],++ "type": "object"++ },++ "then": {++ "required": [++ "expires_at"++ ],++ "properties": {++ "expires_at": {++ "type": "string"++ }++ },++ "type": "object"++ }++}nnew file mode 100644@@@ -0,0 +1,48 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/share-connect-request.v1.json",++ "title": "WebsiteShareConnectRequest",++ "description": "website.share-connect-request@1: what a Node presents to open the tunnel for one share (POST /api/v1/shares/:id/tunnel, capability website.share.connect). The Node proves its Network node principal with a client-credentials token whose audience is openvibe.website, and this short-lived, single-use tunnel_token minted by Connect binds the connection to one share. The edge checks both; neither alone opens the route. The token is presented once and never stored, logged or returned.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "share_id",++ "node_id",++ "tunnel_token"++ ],++ "properties": {++ "share_id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "node_id": {++ "type": "string",++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Must equal the node principal in the Network token; a mismatch is refused and audited."++ },++ "tunnel_token": {++ "type": "string",++ "minLength": 20,++ "maxLength": 4096,++ "description": "A short-lived, single-use, share-scoped token minted by Connect. Send it once over TLS; never put it in a URL, log, audit entry or event."++ },++ "transport": {++ "enum": [++ "quic",++ "http2-ws"++ ],++ "default": "quic",++ "description": "The data-plane transport the Node is trying. quic is ALPN ovconnect/1; http2-ws is the 443 fallback when UDP is blocked."++ },++ "node_version": {++ "type": "string",++ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$",++ "description": "The Node release, for posture and diagnostics; never used for authorization."++ },++ "region": {++ "type": "string",++ "maxLength": 40,++ "description": "The edge region the Node is dialing; the edge refuses a region it does not serve."++ }++ }++}nnew file mode 100644@@@ -0,0 +1,64 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/share-create-request.v1.json",++ "title": "WebsiteShareCreateRequest",++ "description": "website.share-create-request@1: the body of website.share.create (POST /api/v1/shares). The caller names one DNS label, the protocol, the local target and the policy; the project scope comes from the token, never from this body. Connect returns only website.share@1; CON-5 defines a separate authenticated exchange for the one-time tunnel token.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name",++ "protocol",++ "local_target",++ "policy"++ ],++ "properties": {++ "name": {++ "type": "string",++ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",++ "description": "The requested single label under openvibe.website. A taken or reserved label is refused; the UI may suggest name-2 rather than changing it silently."++ },++ "protocol": {++ "enum": [++ "http",++ "https",++ "tcp",++ "udp"++ ]++ },++ "local_target": {++ "type": "string",++ "minLength": 3,++ "maxLength": 300,++ "description": "The local port the Node exposes, e.g. 127.0.0.1:3000; never an arbitrary internet host."++ },++ "policy": {++ "allOf": [++ {++ "$ref": "policy.v1.json"++ },++ {++ "type": "object",++ "properties": {++ "owner": false,++ "project_id": false,++ "version": false,++ "created_at": false,++ "updated_at": false,++ "password_set": false++ }++ }++ ],++ "description": "The policy to enforce. An id binds an existing owner-checked policy; without one, Connect creates it. Server-owned fields are refused in this caller body. Credential and link secrets are configured separately."++ },++ "node_id": {++ "type": "string",++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects."++ },++ "region": {++ "type": "string",++ "maxLength": 40,++ "description": "Optional preferred edge region; placement may ignore it when capacity or policy requires."++ }++ }++}nnew file mode 100644@@@ -0,0 +1,148 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/share.v1.json",++ "title": "WebsiteShare",++ "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "name",++ "hostname",++ "protocol",++ "local_target",++ "status",++ "policy_id",++ "policy_version",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who owns the share. A project share is still owned by a person; project_id says which project it belongs to."++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The project scope when the share belongs to one; null for a personal share. The token's project scope, never a caller-supplied body field, decides access."++ },++ "node_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The Network node principal expected to serve the tunnel; null until one is bound."++ },++ "name": {++ "type": "string",++ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",++ "description": "One DNS label under openvibe.website. The first proof uses the single label agents-<owner> because Universal SSL covers one label only."++ },++ "hostname": {++ "type": "string",++ "minLength": 4,++ "maxLength": 253,++ "description": "The fully qualified public host, lowercased; the default is name.openvibe.website, and a verified custom domain may replace it."++ },++ "protocol": {++ "enum": [++ "http",++ "https",++ "tcp",++ "udp"++ ],++ "description": "What the share carries. http and https are HTTP/WS/SSE shares; tcp and udp are port shares."++ },++ "local_target": {++ "type": "string",++ "minLength": 3,++ "maxLength": 300,++ "description": "The local target the Node may forward to, e.g. 127.0.0.1:3000, tcp://127.0.0.1:25565 or udp://127.0.0.1:19132. Loopback by default; a LAN target is allowed only when the owner enabled it locally."++ },++ "status": {++ "enum": [++ "draft",++ "review",++ "active",++ "paused",++ "revoked"++ ],++ "description": "review holds a public share for the abuse screen; revoked is terminal and closes the route and tunnel."++ },++ "policy_id": {++ "type": "string",++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "policy_version": {++ "type": "integer",++ "minimum": 1,++ "description": "The signed policy version the edge enforces. A request is checked against this version; revocation bumps it and closes the tunnel."++ },++ "region": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "The edge region the share is pinned to, e.g. eu or us-east; null before placement."++ },++ "edge_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80,++ "description": "The edge instance serving the active tunnel; null while the share is offline."++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When the share revokes itself; null for no expiry."++ },++ "last_seen_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "The last successful Node liveness check; null until the first tunnel."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "href": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,259 @@++# ADR-057: Website Connect — a QUIC edge for Share, a private mesh for Connect, Events for hooks++++**Status:** Accepted 2026-11-02 with the OQ-13 review below, which decides what v0 ships (proposed 2026-10-29, CON-1; plan 2026-10-28 §5.9b; research++`research-2026-10-29/website-connect.md` §3.3, §4.3, §4.4 and §4.12). For the owner's review. Builds on ADR-053++(the Website/Host boundary), ADR-045 (the Node is the universal runtime), ADR-042 (the events fabric), ADR-048 (one++authority per resource) and ADR-056 (project secrets: write-only, used by name).++++## Context and evidence++++- **The product is bigger than a site builder.** openvibe.website today is a product/redirect manifest++ (`manifests/products/openvibe.website.json`, `kind: "moved"`) that opens OpenVibe.Space. Plan 2026-10-28 §5.9b adds++ the other ways to put something on the web: a local port at a stable HTTPS name (Share), a private mesh of the++ person's machines (Connect), policies and audit (Protect), custom domains through Domains, and durable webhooks++ through Events. It is explicitly P1 and its first proof is the owner's agent portal on a phone over 5G.++- **The reuse study is CON-0.** `research-2026-10-29/website-connect.md` checked the candidates on 2026-10-29:++ headscale/`tsnet` (BSD-3), NetBird (client BSD-3, management AGPL-3), zrok/OpenZiti (Apache-2.0), frp (Apache-2.0),++ sish (MIT), Pangolin (mixed AGPL-3/commercial), boringproxy, bore, wstunnel, `quic-go` (MIT) and cloudflared. Its++ verdict is one path per pillar: build a focused relay on `quic-go` for Share, adopt headscale + `tsnet` with a++ tenant boundary for Connect, build the policy/abuse layer on top of Cloudflare and Network identity, and extend++ Events for hooks. §3.3 and §3.4 name what to build and what not to reuse.++- **The data plane is Go; the control plane is Node.** The Node agent is Go (ADR-045). QUIC, HTTP/3, datagrams,++ half-close and backpressure are native there and awkward in Node.js. The control API can stay Node/Express, like++ every other network service. The edge is a separate, exposed, bandwidth-heavy tier and must not share a host with++ the control plane (research §4.1).++- **Node already has a principal.** Network owns node principals (`nod_…`, `ids.newId('node')`) and pairs machines;++ `network.node.revoked` already exists in Network's manifest. Connect must reuse that principal, not mint a second++ machine identity. Network also owns OIDC, project membership and grants; Domains owns DNS proposals; Events owns++ durable delivery and v2 signatures (ADR-042); Report and Billing consume usage.++- **Space already uses `<name>.openvibe.website`.** A single-label name is in use for tenant sites. A new share cannot++ silently take the same label: the name registry must be shared (or the share namespace reserved) before public++ launch. Research §4.3 recommends one registry and single-label share names for v1, with a reserved list++ (`www`, `api`, `app`, `agents`, `hooks`, `edge`, `share`, `status`, `admin`, `mail`, `space`).++- **Universal SSL covers one label, not two.** The existing wildcard certificate covers++ `agents-alice.openvibe.website`; it does **not** cover `agents.alice.openvibe.website`. A deep-label design needs++ a separately provisioned certificate (`*.agents.openvibe.website` via Advanced Certificate Manager/Total TLS, or++ Cloudflare for SaaS) and a namespace decision. The first proof must use the single label and does not wait for it.++- **The PSL is a launch gate, not a follow-up.** Until `openvibe.website` is on the Public Suffix List, host-only++ `__Host-` cookies, no `Domain=.openvibe.website`, and the cross-share cookie test are mandatory. Public shares stay++ closed until the entry lands (research §4.3, §4.12; plan §5.9b CON-21).++- **The contracts are the first deliverable.** CON-1 is the ADR plus the additive v1 contracts and the service++ manifest. No service exists yet: this ADR registers the shape, not a deployment. `contracts/catalog.json` gains++ `website.share@1`, `website.share-create-request@1`, `website.share-connect-request@1`, `website.policy@1`,++ `connect.device@1`, `connect.policy@1`, `events.hook@1`, `events.hook-delivery@1` and++ `website.audit-entry@1`; the service manifest (`manifests/services/connect.json` since the review) lists none of the 20 planned capabilities as served. Nothing is granted.++++## Decision++++1. **Share is a focused Go edge relay, `openvibe-edge`, plus a Node `connect` core in OpenVibe.Node.** The relay is++ built on `quic-go` (MIT): one QUIC connection per Node, ALPN `ovconnect/1`, TLS 1.3, streams for HTTP/WebSocket/TCP,++ datagrams for UDP, and an HTTP/2 + WebSocket fallback on 443 for networks that block UDP. It routes by SNI/Host to a++ `share_id` from Connect's name registry, never from a user-controlled header, checks the signed policy, and then++ opens a stream to the Node. It does not own users, shares, policies or audit. No frp, sish, zrok, Pangolin or++ cloudflared code is forked or embedded.++2. **The control API is a new Node service, OpenVibe.Connect.** It owns shares, share policies, devices, mesh policies,++ hooks, the name registry, abuse holds, audit and quota counters in `ov_connect` PostgreSQL; it mints tunnel tokens++ and signs the short-lived policy cache the edge reads. It is a peer of Domains, Events, Network, Report and Billing,++ not a module of Bot. Bot keeps robots, the device link and command safety (ADR-043); Connect keeps shares, the mesh,++ policies and hooks.++3. **Connect mesh is headscale + embedded `tsnet`, with a tenant boundary that must be proven before consumer++ multi-tenancy.** v1 maps one workspace to one headscale tenant: a person's own network for a personal account, a++ project's network for a team. Node embeds `tsnet` (userspace, no root, no TUN) for the private mesh; the public++ ingress stays Share. headscale does not implement Funnel or Serve and Connect must not promise Funnel-compatible++ behaviour. The tenant-isolation proof must cover: no cross-tenant MagicDNS resolution, no cross-tenant route or++ exit-node leak, no shared pre-auth key, and revocation in one tenant leaving the others untouched. If the proof++ fails, the fallback is a dedicated control plane per paying tenant or NetBird's management (AGPL, heavier), decided++ before "unlimited devices for everyone" is promised. Mobile clients: the official Tailscale apps may not accept a++ custom control server on every platform; for v1 the phone reaches services through Share in a browser, and Connect++ is for PCs, NAS and servers. DERP is self-hosted (`derper`) so relay traffic stays on OpenVibe infrastructure;++ Live's TURN is for WebRTC and is not a WireGuard relay.++4. **Identity and the tunnel handshake use two credentials, both checked.** The Node presents a Network++ client-credentials token whose subject is its node principal (`node:nod_…`) and whose audience is++ `openvibe.website`; the request body is `website.share-connect-request@1` with the `share_id`, the same `node_id`++ and a short-lived, single-use, share-scoped `tunnel_token` minted by Connect. The edge validates the token against++ Network's JWKS, requires the subject to equal `node:<node_id>` (for example `node:nod_…`), redeems the++ tunnel token with Connect exactly once and binds the connection to that share. One credential is never++ enough. The tunnel token is sent once over TLS and is++ never stored, logged, audited or published in an event. OIDC at the share host uses Authorization Code + PKCE++ against Network; the session is a host-only `__Host-ov_session` (Secure, Path `=`, no `Domain`) carrying++ `share_id`, `sub` and `policy_version`.++ `POST /api/v1/shares` returns only `website.share@1`; its response has no tunnel token. The Node obtains the++ short-lived, single-use token through a separate authenticated credential exchange before connecting. CON-5++ defines that exchange's endpoint, request and response schemas, authorization, and delivery to the Node.++5. **Revocation is immediate and has one owner per trigger.** `DELETE /api/v1/shares/:id` writes++ `website.share.revoked`, closes the edge route and tunnel, removes the DNS route if Connect added one and makes the++ public host return 410 within the 5-second budget. `network.node.revoked` closes every share bound to that node.++ A policy change bumps `policy_version`, so the edge's cached signed policy is stale on the next request. Removing a++ device revokes its mesh key at the control plane. No capability, route or cached policy survives a revoked share.++6. **Policy is enforced at the edge, authored and audited by Connect, and fronted by Cloudflare.** Cloudflare++ terminates TLS for HTTP/WS and applies DDoS/WAF/bot rules; the edge performs share lookup, auth, the++ `website.policy@1` decision (public, signin, named, password, one_time, expiry, IP/CIDR, country, rate limits, bot++ check, WAF preset, posture), request size/timeouts and audit; the Node restricts the local target to the owner's++ chosen loopback/LAN port and never proxies arbitrary hosts. Connect owns ownership, policy CRUD, abuse review,++ quotas and revocation. Passwords are Argon2id hashes stored beside the policy and never in the contract; audit++ entries never carry bodies, headers, cookies, tokens, query strings or passwords. Public shares require a++ signed-in creator and pass the abuse screen (Vision UI/look-alike plus Safe Browsing/Web Risk) before activation.++ Exit nodes never become public proxies, and a public share may not target an exit node.++ `expiry` means public access only until `expires_at`; `expires_at` can also cap any other mode. Password and++ one-time policies remain inactive until their credential or link is configured. CON-5 defines the credential/link++ API and activation rules; neither the password nor the link secret belongs in `website.policy@1`.++7. **Hooks are owned by Events and only managed by Connect.** Connect creates a per-hook subscription whose endpoint is++ the hook's own share host (`https://<share>/__ov/hook/<id>`), never an arbitrary URL, and mints a `whsec_…` secret++ shown once. `hooks.openvibe.website/h/<id>` is a thin ingress: size cap, rate limit, optional provider signature++ check, raw body stored at rest by reference, and an event published; Events stores the event, signs every attempt++ with v2, retries for up to seven days while the laptop is offline, dead-letters and replays. The Node verifies the++ signature and 300-second timestamp tolerance, dedupes on `event_id`, acks 2xx and may buffer locally; a 503 asks++ Events to retry. Events produces `events.hook.received|delivered|dead`; Connect writes none of them. No smee or++ Hookdeck is adopted.++8. **The SDK stays MIT and thin.** `openvibe-sdk` (MIT) gets at most a thin `openvibe-sdk/connect` client that talks++ to the local Node daemon over HTTP/Unix socket and to the control API over HTTP. The QUIC data plane, the mesh++ (`tsnet`) and the policy engine stay in the AGPL Node/Connect/edge code. No AGPL/GPL tunnel code is copied into the++ SDK or `openvibe-shared` (MIT). This keeps the permissive client boundary and the copyleft in the agent.++9. **Naming and the first proof use one label.** The first proof is the owner's agent portal at++ `https://agents-<owner>.openvibe.website` (single label), behind OpenVibe sign-in, opened on a phone over 5G with a++ working microphone (secure context) and WebSocket. Cloudflare Universal SSL does not cover++ `agents.owner.openvibe.website`; the two-label form, a `*.agents.openvibe.website` certificate or Cloudflare for++ SaaS is deferred to CON-2. Deep labels do not gate the proof. Public shares remain blocked until the PSL entry, the++ abuse queue and the DMCA/runbook gates land (CON-21).++10. **The contracts are additive v1 and the manifest claims nothing yet.** The nine schemas above are public or++ first-party as noted in the catalog; the 20 capabilities in `manifests/services/website.json` (now `connect.json`, see the review) are `planned`, owned++ by `website`, and listed in the manifest so consumers see the shape. No token is granted, no service is deployed,++ and no DNS record changes. The separate product/redirect manifest `manifests/products/openvibe.website.json` is++ preserved as the domain's product home and still points at Space; `website.json` has `domains: []` and++ `exposure.state: placeholder`. The service repository is `OpenVibers/OpenVibe.Connect`; the edge may be a second++ repository (`openvibe-edge`) under CON-3.++++## Service boundaries++++| Concern | Owner | Not this service |++|---|---|---|++| Share record, name registry, policy, tunnel token, inspector metadata, audit | OpenVibe.Connect (Node control API, `ov_connect`) | the edge keeps active session state and a signed policy cache only |++| Public QUIC/HTTP/WS/SSE/TCP/UDP ingress, routing, edge policy check, rate limits | `openvibe-edge` (Go, `quic-go`) | it does not own users, shares, policies, audit or hooks |++| Node tunnel, local forwarding, `ov share`, `tsnet` | OpenVibe.Node (`internal/connect`) | it does not decide policy or open arbitrary hosts |++| Account, node principal, OIDC, grants, revocation event | OpenVibe.Network | Connect never mints identity |++| DNS proposal, TXT/CNAME, certificate path | OpenVibe.Domains | Connect never writes DNS or holds a provider credential |++| Durable event store, v2 signature, retries, DLQ, replay | OpenVibe.Events | Connect creates the subscription; the edge does not queue |++| Usage, abuse analytics, billing | OpenVibe.Report / OpenVibe.Billing | Connect emits usage; it does not price or bill |++| Firewall helper, local posture read | OpenVibe.Node, local approval | no remote "open this port" API |++| Thin client for apps | `openvibe-sdk/connect` (MIT) | no tunnel or mesh code in the SDK |++++## Rejected reuse paths++++| Path | Why not |++|---|---|++| frp as the product | Great data plane, wrong abstraction: static proxies and a server-wide token; per-user policy/audit would be a fork. |++| sish | MIT and simple, but SSH-first UX, no UDP and no per-share policy model. |++| zrok / OpenZiti | Right feature list, but operating the OpenZiti controller/router/edge is a second platform. |++| NetBird wholesale | Complete mesh and reverse proxy, but the management stack is AGPL and heavy, Reverse Proxy is beta, and it duplicates Share. |++| Pangolin | Strong SASE, but mixed AGPL/commercial files and a full product to reskin; not embeddable without a file-by-file audit. |++| cloudflared | Apache-2.0 client, but the protocol is tied to Cloudflare's edge; it is not a self-hostable relay. |++| rathole / bore / wstunnel | Good minimal transports with no identity, policy, control API or multi-tenancy. |++| Tailscale SaaS | Excellent product, but a competitor cannot be built on its control plane; headscale + `tsnet` is the reusable path. |++| smee / Hookdeck | smee is development-only (unauthenticated channels, no storage); Hookdeck is closed and per-event priced. Events already has the store, signatures, retries, DLQ and replay. |++++## Consequences++++- **Positive:** the exposed data plane is small and Go-native; the control plane reuses Network identity, Domains++ proposals, Events delivery and the existing operational stack; the SDK stays MIT; every contract is additive and++ consumers can build against it before the service exists.++- **Negative:** a new service and a new edge fleet to run; a headscale/`derper`/PostgreSQL/ValKey footprint; a public++ ingress that will attract abuse, so quotas, the abuse queue and the PSL gate are launch blockers; deep-label URLs++ wait for CON-2; the TCP/UDP port pool and DDoS capacity are real cost.++- **Compatibility:** all nine contracts are new v1 files; no existing contract changes. The manifest is new. The++ generated TypeScript/bundle and OpenAPI outputs are regenerated; no existing route changes. Consumers pin the next++ release tag (v0.279.0) when they start.++- **Rollback:** remove the new catalog rows, schemas, fixtures, capabilities and manifest, and regenerate. No deployed++ service or grant exists to roll back in CON-1; CON-3/CON-5 must add expand/migrate/contract rules before their first++ deploy (ADR-028).++- **Tests:** every schema has a valid and an invalid fixture; the catalog `$id`/path/major checks, manifest and++ capability validation, capability-schema resolution and OpenAPI generation run in `npm test`. The narrow service++ check is `npx openvibe-contracts-check --service website`; because no service checkout exists yet it can only prove++ the manifest resolves and the installed contracts satisfy its range, not that routes enforce the capabilities.++ CON-3/CON-5 add the code check with `--src`.++++## Contracts and capabilities added++++| Contract | Visibility | Purpose |++|---|---|---|++| `website.share@1` | public | the share record: owner, project, node, label, host, protocol, local target, status, policy version, region/edge, timestamps |++| `website.share-create-request@1` | public | name, protocol, local target and an inline policy constrained to caller-owned fields; project scope comes from the token |++| `website.share-connect-request@1` | first-party | `share_id`, `node_id`, single-use `tunnel_token`, transport, node version, region |++| `website.policy@1` | public | mode and rules; no password hash; version for the signed edge cache |++| `website.audit-entry@1` | first-party | one decision row with subject/IP/country/UA/method/path/status/bytes and no bodies or secrets |++| `connect.device@1` | public | one device in one tenant, with self-reported posture and routes |++| `connect.policy@1` | first-party | one tenant's grants, default deny, exit node and subnet routes |++| `events.hook@1` | public | a durable hook: share, ingress, topics, filters, retry, v2 signature, `secret_set` (never the secret) |++| `events.hook-delivery@1` | public | one attempt: status, code, latency, size, hash, next attempt, dead/replay; never the body |++++The 20 planned capabilities are the exact §4.4 set: `website.share.create|read|update|delete|connect|inspect|replay`,++`website.policy.read|write`, `connect.device.read|manage`, `connect.policy.read|write`,++`events.hook.manage|read`, `website.abuse.report|review`, `website.domain.manage` and the internal++`website.admin.read|manage`. The §4.4 wildcard `website.admin.*` is expanded into those two exact ids because a++capability id is concrete; `website.abuse.review` stays the moderator queue. None is active and none is granted.++++## Open questions for CON-2 / CON-3++++- **CON-2 (namespace, TLS, DNS, PSL):** one shared name registry with Space vs a reserved share namespace; the exact++ reserved-label list; `*.agents.openvibe.website` via ACM/Total TLS vs Cloudflare for SaaS; the PSL submission and++ the cross-share cookie test; whether TCP/UDP use `game.openvibe.website` or per-region `edge-<region>` records and++ which port ranges each region owns. The first proof needs only the single label and the existing wildcard.++- **CON-3 (edge v0):** the QUIC stream framing and fallback mux; whether the signed policy cache is a JWT or a++ Connect-signed document and its TTL; the edge's DERP/relay relationship; the exact ready/health contract and the++ region pinning/reconnect budget; how the edge proves a tunnel token was redeemed only once without a round trip per++ request.++- **CON-5 (control API):** the abuse report request/response bodies (deliberately left out of CON-1), the++ authenticated tunnel credential exchange and password/one-time credential/link setup API, the++ `share-access`/role model, the name-registry table owner, and the `website.share.accessed|blocked` sampling policy++ (per request would be too high-volume for Events).++- **Owner:** the free-tier limits (shares, devices, bandwidth, hooks), whether custom domains are free at launch,++ and the DMCA agent/takedown runbook owner (CON-21).++++## Sources++++- `research-2026-10-29/website-connect.md` §1.1–1.7, §3.2–3.4, §4.1–4.13 (checked 2026-10-29).++- `OpenVibe_New_Domains_Plan_2026-10-28.md` §5.9b and the CON-1…CON-21 ledger.++- ADR-053 (Website/Host boundary), ADR-045 (Node runtime), ADR-042 (events fabric), ADR-043 (Bot devices), ADR-048++ (control plane), ADR-056 (project secrets), ADR-028 (expand/migrate/contract).++- `manifests/products/openvibe.website.json` (the distinct product/redirect manifest, preserved), ADR-005 (realtime),++ `manifests/services/events.json` and `manifests/services/network.json` (node revocation and hook events).++++## Review (OQ-13, 2026-11-02): what v0 ships++++Re-derived from the owner's ask (a port on your machine at a stable HTTPS name, opened on a phone over 5G, behind++OpenVibe sign-in) and from what the network runs on today: one host, every name proxied by Cloudflare.++++1. **v0's data plane is the WebSocket fallback, inside OpenVibe.Connect.** A QUIC edge needs UDP to the origin.++ Cloudflare proxies HTTP and WebSockets, not QUIC to an origin, so the edge would need a DNS-only record that shows the++ host's address, and Decision 1 also wants it on a host of its own. Neither exists, and neither is needed for the++ first proof. The relay ships as `server/edge/` in OpenVibe.Connect: the computer opens one WebSocket to++ `wss://connect.openvibe.website/tunnel` through Cloudflare on 443 (`ovconnect-ws/1`: JSON control messages, binary++ data frames per stream, per-stream backpressure), and a visitor's request on a share host goes nginx → Connect →++ that WebSocket → `localhost:<port>`. WebSocket upgrades become a byte pipe to the local port; server-sent events++ stream. The edge module is separate from the control code so CON-3 can move it to a QUIC edge on its own host++ without changing the client's view of a share.++2. **Names: `<label>--<user>.openvibe.website`.** One label, so the existing wildcard certificate covers it (Decision++ 9). The person's handle is part of the name, so names are per person and nobody can take another's. OpenVibe.Space's++ site names are `[a-z0-9]` with single hyphens (`server/domain/sites.js` NAME_RE), so no Space site can ever contain++ "--", and no shared name registry is needed: nginx sends `~^[a-z0-9-]+--[a-z0-9-]+\.openvibe\.website$` to++ Connect, and Space's server_name leaves those hosts out. `connect` is reserved in Space.++3. **The computer's credential in v0 is a share-scoped connect key** (`ovc_` + 32 random bytes, stored as a SHA-256++ hash, shown once, replaced on demand, which disconnects the old computer). Decision 4's two credentials (a node++ principal token plus a single-use tunnel token) need the OpenVibe Node plugin (CON-4); the standalone client cannot++ hold a node principal. The key opens one share's tunnel and nothing else.++4. **The gate is Connect's, and every cookie is host-only.** `/grant` on connect.openvibe.website signs the visitor++ in with OpenVibe, checks the share's policy, and hands the share host a one-time code (60 s, one use, bound to the++ share); the share host sets `__Host-ovc`, HMAC-signed and bound to the share and its `policy_version`. A Set-Cookie++ from the shared app loses its Domain attribute (the PSL entry is not in yet), the app never sees the gate's cookie,++ and nothing a share sends may be kept by Cloudflare's cache (`Cloudflare-CDN-Cache-Control: no-store`, private++ Cache-Control): a cached copy would be served past the gate.++5. **Modes in v0: owner, named, and signin (staff-first).** Public is refused until CON-9 and CON-21, as Decision 6++ says. Because anyone can make an OpenVibe account, "anyone signed in" is nearly public, so it is open to OpenVibe++ staff (Network role admin) until the abuse check, as Space's public publishing is.++6. **The service is `connect`.** One id everywhere: the repository OpenVibe.Connect, ovhost `connect` (port 5240),++ database `ov_connect`, OAuth client `connect`, `manifests/services/connect.json` (domain connect.openvibe.website,++ live), owning the `website.*` and `connect.*` namespaces. The 20 capabilities stay `planned` and owned by++ `connect`; v0's API is a person's own (session or Network user token) and serves none of them. The share API's++ shapes are Connect's own in v0; `website.share@1` conformance comes with CON-5.++7. **What waits:** the QUIC edge (CON-3), the Node plugin and node principals (CON-4), custom domains (CON-7), the++ inspector (CON-8), the abuse gate (CON-9), TCP/UDP shares (CON-11/12), the mesh (CON-13; the headscale tenant proof++ stands), hooks (CON-15) and the launch gate for public shares (CON-21).@@@ -50,3 +50,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each| [ADR-054](ADR-054-inventory-authority.md) | The OpenVibe inventory: a new authority, OpenVibe.Inventory (`inventory`, `ov_inventory`), owns item kinds (contract + renderer per surface), definitions (`itd_`), instances (`inv_`), the equipped set and an append-only ledger; issuers grant within their namespace; no selling, buying, trading or conversion until a later money ADR (ADR-012/ADR-025); Live's cosmetics are the first kinds, migrated convert-verify-delete | Accepted 2026-10-09 (plan T21) || [ADR-055](ADR-055-inventory-money.md) | Money and the inventory, in phases: A primary sales (an issuer prices an item in Vibes, booked by Billing as an ENTITLEMENT with a creator share, or in OpenCoins as a loyalty shop with no payout); B gifting between people for giftable items (an ADR-012 rule 6 amendment); C trading and a market stay closed (ADR-025); never paid chance (no cases, crates, loot boxes) | Proposed 2026-10-09 (plan T21), for the owner's decision || [ADR-056](ADR-056-project-secrets.md) | Project secrets: OpenVibe.Network keeps them per project and environment, write-only (no API returns a value to a person), AES-256-GCM under `NETWORK_SECRETS_KEY`; a service uses one only when the project granted it, to `sign` (HMAC, the value never leaves Network) or to `reveal` it for a listed https origin, through first-party `network.secret.use`; archiving a project shreds its secrets | Accepted 2026-10-15 (plan T13/T17) |++| [ADR-057](ADR-057-website-connect.md) | Website Connect: a focused Go `quic-go` edge relay for Share, a Node OpenVibe.Connect control API, headscale + embedded `tsnet` for the private mesh with a tenant-isolation proof, and Events-owned durable hook delivery; two-credential node/share tunnel handshake, immediate revocation, edge policy enforcement, no secret or body in audit, a thin MIT SDK boundary, and the single-label `agents-<owner>.openvibe.website` first proof (Universal SSL does not cover `agents.owner.openvibe.website`; deep labels are CON-2); registers 9 v1 contracts and 20 planned capabilities, grants and deploys nothing | Proposed 2026-10-29 (CON-1; plan 2026-10-28 §5.9b), for the owner's review |nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "name": "workstation",++ "status": "connected",++ "os": "linux",++ "version": "0.42.0",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "tenant": {++ "type": "user",++ "id": "team_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "name": "workstation",++ "status": "online",++ "os": "linux",++ "version": "0.42.0",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "-ab",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "ab-",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "a-------------------------------------------------------------z",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "a",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "ab",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,45 @@++{++ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "workstation",++ "status": "online",++ "os": "linux",++ "arch": "amd64",++ "version": "0.42.0",++ "addresses": [++ "100.64.0.2"++ ],++ "tags": [++ "owner",++ "linux"++ ],++ "posture": {++ "node_current": true,++ "os_patched": true,++ "firewall_on": true,++ "disk_encryption": true,++ "risky_ports": [],++ "checked_at": "2026-10-29T09:00:00.000Z"++ },++ "routes": [],++ "exit_node": false,++ "approved_at": "2026-10-29T08:05:00.000Z",++ "approved_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "expires_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null++}nnew file mode 100644@@@ -0,0 +1,29 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "version": 1,++ "default_action": "deny",++ "rules": [++ {++ "action": "allow",++ "sources": [++ "*"++ ],++ "destinations": [++ "*"++ ],++ "ports": [++ "http"++ ]++ }++ ],++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,26 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "version": 1,++ "default_action": "deny",++ "rules": [++ {++ "action": "allow",++ "sources": [++ "everyone"++ ],++ "destinations": [++ "*"++ ]++ }++ ],++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,47 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "tenant": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "name": "Home mesh",++ "version": 2,++ "default_action": "deny",++ "rules": [++ {++ "action": "allow",++ "sources": [++ "device:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8"++ ],++ "destinations": [++ "device:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8"++ ],++ "ports": [++ "443",++ "8000-8100"++ ],++ "description": "The owner's own devices reach each other."++ }++ ],++ "exit_node": {++ "enabled": false,++ "device_ids": []++ },++ "subnet_routes": [++ {++ "device_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",++ "cidr": "192.168.1.0/24"++ }++ ],++ "updated_by": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",++ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",++ "event_type": "github.push.received",++ "attempt": 1,++ "status": "delivered",++ "payload_bytes": 2048,++ "payload_sha256": "not-a-sha256",++ "created_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",++ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",++ "event_type": "github.push.received",++ "attempt": 1,++ "status": "delivered",++ "payload_bytes": 2048,++ "payload_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",++ "body": "{\"ref\":\"refs/heads/main\"}",++ "created_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",++ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",++ "event_type": "github.push.received",++ "attempt": 30,++ "status": "dead",++ "response_code": null,++ "latency_ms": null,++ "payload_bytes": 2048,++ "payload_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",++ "next_attempt_at": null,++ "attempted_at": "2026-11-05T09:00:00.000Z",++ "completed_at": "2026-11-05T09:00:01.000Z",++ "dead_at": "2026-11-05T09:00:01.000Z",++ "error": "share offline",++ "replay_of": null,++ "created_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",++ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",++ "event_type": "github.push.received",++ "attempt": 1,++ "status": "delivered",++ "request_id": "req_01J8Z3Q4R5S6T7V8W9",++ "response_code": 202,++ "latency_ms": 84,++ "payload_bytes": 2048,++ "payload_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",++ "signature_version": "v2",++ "next_attempt_at": null,++ "attempted_at": "2026-10-29T09:00:00.000Z",++ "completed_at": "2026-10-29T09:00:00.084Z",++ "dead_at": null,++ "error": null,++ "replay_of": null,++ "created_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "name": "Insecure hook",++ "endpoint": "http://hooks.openvibe.website/h/hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "topics": [++ "github.push.received"++ ],++ "retry": {++ "window_seconds": 604800,++ "max_attempts": 30++ },++ "signature": {++ "version": "v2",++ "tolerance_seconds": 300++ },++ "status": "active",++ "secret_set": true,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,26 @@++{++ "id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "name": "GitHub pushes",++ "endpoint": "https://hooks.openvibe.website/h/hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "topics": [++ "github.push.received"++ ],++ "retry": {++ "window_seconds": 604800,++ "max_attempts": 30++ },++ "signature": {++ "version": "v2",++ "tolerance_seconds": 300++ },++ "status": "active",++ "secret_set": true,++ "secret": "whsec_this-value-never-belongs-in-a-contract",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,38 @@++{++ "id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "name": "GitHub pushes",++ "endpoint": "https://hooks.openvibe.website/h/hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",++ "topics": [++ "github.push.received",++ "github.pull_request.received"++ ],++ "filters": [++ {++ "path": "$.repository.full_name",++ "equals": "OpenVibers/OpenVibe.Contracts"++ }++ ],++ "transform": null,++ "retry": {++ "window_seconds": 604800,++ "max_attempts": 30++ },++ "signature": {++ "version": "v2",++ "tolerance_seconds": 300++ },++ "max_payload_bytes": 1048576,++ "status": "active",++ "secret_set": true,++ "secret_rotated_at": null,++ "last_delivery_at": "2026-10-29T09:00:00.000Z",++ "dead_at": null,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "id": "aud_01J8Z3Q4R5S6T7V8W9X0Y1Z2B2",++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "request_id": "req_01J8Z3Q4R5S6T7V8W9",++ "decision": "allowed",++ "at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "id": "aud_01J8Z3Q4R5S6T7V8W9X0Y1Z2B2",++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "request_id": "req_01J8Z3Q4R5S6T7V8W9",++ "decision": "allow",++ "at": "2026-10-29T09:00:00.000Z",++ "body": "{\"password\":\"hunter2\"}"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "aud_01J8Z3Q4R5S6T7V8W9X0Y1Z2B2",++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "request_id": "req_01J8Z3Q4R5S6T7V8W9",++ "subject": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "ip": "203.0.113.7",++ "country": "DE",++ "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)",++ "method": "GET",++ "path": "/",++ "status": 403,++ "decision": "deny",++ "reason": "policy.named_only",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "bytes_in": 412,++ "bytes_out": 0,++ "edge_id": "edge-eu-1",++ "region": "eu",++ "at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "mode": "invite_only"++}nnew file mode 100644@@@ -0,0 +1,33 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "name": "Launch demo",++ "mode": "expiry",++ "allowed_subjects": [],++ "allowed_projects": [],++ "ip_allow": [],++ "ip_deny": [],++ "country_allow": [++ "DE",++ "FR"++ ],++ "country_deny": [],++ "rate_limits": {++ "per_share_per_minute": 600,++ "per_ip_per_minute": 60,++ "max_connections": 100,++ "max_request_bytes": 10485760++ },++ "bot_check": "turnstile",++ "waf_preset": "standard",++ "posture_required": [],++ "password_set": false,++ "session_ttl_seconds": 3600,++ "version": 3,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,34 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "name": "Launch demo",++ "mode": "expiry",++ "allowed_subjects": [],++ "allowed_projects": [],++ "ip_allow": [],++ "ip_deny": [],++ "country_allow": [++ "DE",++ "FR"++ ],++ "country_deny": [],++ "rate_limits": {++ "per_share_per_minute": 600,++ "per_ip_per_minute": 60,++ "max_connections": 100,++ "max_request_bytes": 10485760++ },++ "bot_check": "turnstile",++ "waf_preset": "standard",++ "posture_required": [],++ "password_set": false,++ "session_ttl_seconds": 3600,++ "expires_at": null,++ "version": 3,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "mode": "password",++ "password_hash": "$argon2id$v=19$m=65536,t=3,p=4$example$example"++}nnew file mode 100644@@@ -0,0 +1,34 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "name": "Launch demo",++ "mode": "expiry",++ "allowed_subjects": [],++ "allowed_projects": [],++ "ip_allow": [],++ "ip_deny": [],++ "country_allow": [++ "DE",++ "FR"++ ],++ "country_deny": [],++ "rate_limits": {++ "per_share_per_minute": 600,++ "per_ip_per_minute": 60,++ "max_connections": 100,++ "max_request_bytes": 10485760++ },++ "bot_check": "turnstile",++ "waf_preset": "standard",++ "posture_required": [],++ "password_set": false,++ "session_ttl_seconds": 3600,++ "expires_at": "2026-11-01T00:00:00.000Z",++ "version": 3,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,36 @@++{++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "name": "Password for the demo",++ "mode": "password",++ "allowed_subjects": [],++ "allowed_projects": [],++ "ip_allow": [],++ "ip_deny": [],++ "country_allow": [++ "DE",++ "FR"++ ],++ "country_deny": [],++ "rate_limits": {++ "per_share_per_minute": 600,++ "per_ip_per_minute": 60,++ "max_connections": 100,++ "max_request_bytes": 10485760++ },++ "bot_check": "turnstile",++ "waf_preset": "standard",++ "posture_required": [],++ "password_set": true,++ "password_hint": "the one from the call",++ "one_time_max_uses": 1,++ "session_ttl_seconds": 3600,++ "expires_at": null,++ "version": 3,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "tunnel_token": "ovt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9_one_time",++ "transport": "websocket"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "tunnel_token": "ovt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9_one_time",++ "transport": "quic",++ "node_version": "0.42.0",++ "region": "eu"++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "-ab",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ },++ "created_at": "2026-10-29T08:00:00.000Z"++ }++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ },++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ },++ "password_set": false++ }++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ },++ "project_id": null++ }++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ },++ "updated_at": "2026-10-29T08:00:00.000Z"++ }++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ },++ "version": 1++ }++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "policy": {++ "mode": "signin"++ },++ "project_id": "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "ab-",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "name": "agents-alice",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "mode": "signin"++ }++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "a-------------------------------------------------------------z",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "a",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "name": "ab",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "policy": {++ "mode": "signin",++ "allowed_projects": [++ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"++ ],++ "rate_limits": {++ "per_ip_per_minute": 120++ }++ }++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "name": "agents-alice",++ "hostname": "agents-alice.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "deleted",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "-ab",++ "hostname": "-ab.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",++ "hostname": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "ab-",++ "hostname": "ab-.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "name": "Agents-Alice",++ "hostname": "agents-alice.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 1,++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "agents-alice",++ "hostname": "agents-alice.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "a-------------------------------------------------------------z",++ "hostname": "a-------------------------------------------------------------z.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "a",++ "hostname": "a.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",++ "owner": {++ "type": "user",++ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"++ },++ "project_id": null,++ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",++ "name": "ab",++ "hostname": "ab.openvibe.website",++ "protocol": "http",++ "local_target": "127.0.0.1:3000",++ "status": "active",++ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",++ "policy_version": 3,++ "region": "eu",++ "edge_id": "edge-eu-1",++ "expires_at": null,++ "last_seen_at": "2026-10-29T09:00:00.000Z",++ "created_at": "2026-10-29T08:00:00.000Z",++ "updated_at": "2026-10-29T09:00:00.000Z",++ "revoked_at": null,++ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"++}@@@ -97221,6 +97221,1504 @@}}}++ },++ "website.share": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/share.v1.json",++ "title": "WebsiteShare",++ "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "name",++ "hostname",++ "protocol",++ "local_target",++ "status",++ "policy_id",++ "policy_version",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who owns the share. A project share is still owned by a person; project_id says which project it belongs to."++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The project scope when the share belongs to one; null for a personal share. The token's project scope, never a caller-supplied body field, decides access."++ },++ "node_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The Network node principal expected to serve the tunnel; null until one is bound."++ },++ "name": {++ "type": "string",++ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",++ "description": "One DNS label under openvibe.website. The first proof uses the single label agents-<owner> because Universal SSL covers one label only."++ },++ "hostname": {++ "type": "string",++ "minLength": 4,++ "maxLength": 253,++ "description": "The fully qualified public host, lowercased; the default is name.openvibe.website, and a verified custom domain may replace it."++ },++ "protocol": {++ "enum": [++ "http",++ "https",++ "tcp",++ "udp"++ ],++ "description": "What the share carries. http and https are HTTP/WS/SSE shares; tcp and udp are port shares."++ },++ "local_target": {++ "type": "string",++ "minLength": 3,++ "maxLength": 300,++ "description": "The local target the Node may forward to, e.g. 127.0.0.1:3000, tcp://127.0.0.1:25565 or udp://127.0.0.1:19132. Loopback by default; a LAN target is allowed only when the owner enabled it locally."++ },++ "status": {++ "enum": [++ "draft",++ "review",++ "active",++ "paused",++ "revoked"++ ],++ "description": "review holds a public share for the abuse screen; revoked is terminal and closes the route and tunnel."++ },++ "policy_id": {++ "type": "string",++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "policy_version": {++ "type": "integer",++ "minimum": 1,++ "description": "The signed policy version the edge enforces. A request is checked against this version; revocation bumps it and closes the tunnel."++ },++ "region": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "The edge region the share is pinned to, e.g. eu or us-east; null before placement."++ },++ "edge_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80,++ "description": "The edge instance serving the active tunnel; null while the share is offline."++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When the share revokes itself; null for no expiry."++ },++ "last_seen_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "The last successful Node liveness check; null until the first tunnel."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "href": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://"++ }++ }++ }++ },++ "website.share-create-request": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/share-create-request.v1.json",++ "title": "WebsiteShareCreateRequest",++ "description": "website.share-create-request@1: the body of website.share.create (POST /api/v1/shares). The caller names one DNS label, the protocol, the local target and the policy; the project scope comes from the token, never from this body. Connect returns only website.share@1; CON-5 defines a separate authenticated exchange for the one-time tunnel token.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "name",++ "protocol",++ "local_target",++ "policy"++ ],++ "properties": {++ "name": {++ "type": "string",++ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",++ "description": "The requested single label under openvibe.website. A taken or reserved label is refused; the UI may suggest name-2 rather than changing it silently."++ },++ "protocol": {++ "enum": [++ "http",++ "https",++ "tcp",++ "udp"++ ]++ },++ "local_target": {++ "type": "string",++ "minLength": 3,++ "maxLength": 300,++ "description": "The local port the Node exposes, e.g. 127.0.0.1:3000; never an arbitrary internet host."++ },++ "policy": {++ "allOf": [++ {++ "$ref": "policy.v1.json"++ },++ {++ "type": "object",++ "properties": {++ "owner": false,++ "project_id": false,++ "version": false,++ "created_at": false,++ "updated_at": false,++ "password_set": false++ }++ }++ ],++ "description": "The policy to enforce. An id binds an existing owner-checked policy; without one, Connect creates it. Server-owned fields are refused in this caller body. Credential and link secrets are configured separately."++ },++ "node_id": {++ "type": "string",++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects."++ },++ "region": {++ "type": "string",++ "maxLength": 40,++ "description": "Optional preferred edge region; placement may ignore it when capacity or policy requires."++ }++ }++ }++ },++ "website.share-connect-request": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/share-connect-request.v1.json",++ "title": "WebsiteShareConnectRequest",++ "description": "website.share-connect-request@1: what a Node presents to open the tunnel for one share (POST /api/v1/shares/:id/tunnel, capability website.share.connect). The Node proves its Network node principal with a client-credentials token whose audience is openvibe.website, and this short-lived, single-use tunnel_token minted by Connect binds the connection to one share. The edge checks both; neither alone opens the route. The token is presented once and never stored, logged or returned.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "share_id",++ "node_id",++ "tunnel_token"++ ],++ "properties": {++ "share_id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "node_id": {++ "type": "string",++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Must equal the node principal in the Network token; a mismatch is refused and audited."++ },++ "tunnel_token": {++ "type": "string",++ "minLength": 20,++ "maxLength": 4096,++ "description": "A short-lived, single-use, share-scoped token minted by Connect. Send it once over TLS; never put it in a URL, log, audit entry or event."++ },++ "transport": {++ "enum": [++ "quic",++ "http2-ws"++ ],++ "default": "quic",++ "description": "The data-plane transport the Node is trying. quic is ALPN ovconnect/1; http2-ws is the 443 fallback when UDP is blocked."++ },++ "node_version": {++ "type": "string",++ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$",++ "description": "The Node release, for posture and diagnostics; never used for authorization."++ },++ "region": {++ "type": "string",++ "maxLength": 40,++ "description": "The edge region the Node is dialing; the edge refuses a region it does not serve."++ }++ }++ }++ },++ "website.policy": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/policy.v1.json",++ "title": "WebsitePolicy",++ "description": "website.policy@1: how a Share decides who may reach it and under what conditions (public, OpenVibe sign-in, named people or projects, password, one-time link, expiry), plus IP/country rules, rate limits, a bot check, a WAF preset and required device posture. mode is the only field required except that expiry also requires a non-null expires_at. id, owner and version are present when Connect returns a stored policy. Password and one-time policies stay inactive until their credential or link is configured through the CON-5 API. A password is stored as a one-way hash beside this record and is never part of this document; password_set only says one exists.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "mode"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Present on a stored policy; omitted when the policy is written inline with a new share."++ },++ "owner": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who owns the policy; set from the token when a policy is created."++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The project scope, when the policy belongs to one."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "mode": {++ "enum": [++ "public",++ "signin",++ "named",++ "password",++ "one_time",++ "expiry"++ ],++ "description": "public: anyone with the link after the abuse screen; signin: any OpenVibe account (optionally a project); named: allowed_subjects/allowed_projects only; password: a shared password; one_time: single-use links; expiry: public access only until expires_at. An expires_at may also cap any other mode."++ },++ "allowed_subjects": {++ "type": "array",++ "maxItems": 500,++ "uniqueItems": true,++ "items": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "description": "Explicit people or agents allowed in named mode; a project's members are allowed through allowed_projects instead."++ },++ "allowed_projects": {++ "type": "array",++ "maxItems": 100,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "description": "Projects whose members may reach a named or signin share."++ },++ "ip_allow": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "CIDR ranges allowed before any other rule; empty means no IP allowlist."++ },++ "ip_deny": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "CIDR ranges always refused; evaluated before the allowlist."++ },++ "country_allow": {++ "type": "array",++ "maxItems": 250,++ "items": {++ "type": "string",++ "pattern": "^[A-Z]{2}$"++ },++ "description": "ISO 3166-1 alpha-2 country codes allowed; empty means all."++ },++ "country_deny": {++ "type": "array",++ "maxItems": 250,++ "items": {++ "type": "string",++ "pattern": "^[A-Z]{2}$"++ }++ },++ "rate_limits": {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "per_share_per_minute": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000000++ },++ "per_ip_per_minute": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100000++ },++ "max_connections": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100000++ },++ "max_request_bytes": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1073741824++ }++ }++ },++ "bot_check": {++ "enum": [++ "none",++ "turnstile"++ ],++ "default": "none"++ },++ "waf_preset": {++ "enum": [++ "none",++ "standard",++ "strict",++ "api"++ ],++ "default": "standard",++ "description": "Defense in depth at the edge; Cloudflare's managed rules run in front of it for HTTP/WS."++ },++ "posture_required": {++ "type": "array",++ "maxItems": 10,++ "uniqueItems": true,++ "items": {++ "enum": [++ "node_current",++ "os_patched",++ "firewall_on",++ "disk_encryption",++ "no_risky_ports"++ ]++ },++ "description": "Self-reported device posture a share may require; a signal, not a guarantee."++ },++ "password_set": {++ "type": "boolean",++ "description": "Read-only: true when a password hash exists. The hash itself never leaves Connect."++ },++ "password_hint": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 120,++ "description": "An optional hint shown on the password page; never the password."++ },++ "one_time_max_uses": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1000,++ "description": "For one_time mode; 1 unless the owner allows more."++ },++ "session_ttl_seconds": {++ "type": "integer",++ "minimum": 60,++ "maximum": 604800,++ "description": "How long a host-only __Host-ov_session on the share host stays valid after sign-in."++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "The absolute end of the policy; required and non-null for expiry mode, optional for other modes."++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "The signed policy version the edge caches and checks on every request; present on a stored policy."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "if": {++ "properties": {++ "mode": {++ "const": "expiry"++ }++ },++ "required": [++ "mode"++ ],++ "type": "object"++ },++ "then": {++ "required": [++ "expires_at"++ ],++ "properties": {++ "expires_at": {++ "type": "string"++ }++ },++ "type": "object"++ }++ }++ },++ "website.audit-entry": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/website/audit-entry.v1.json",++ "title": "WebsiteAuditEntry",++ "description": "website.audit-entry@1: one decision row for a Share, as the owner or a moderator sees it. It answers who reached (or tried to reach) a share, from where, what method and path, what the policy decided and how much crossed the edge. It deliberately carries no request or response body, headers, cookies, bearer or tunnel tokens, query string, password or hook payload: query strings are stripped before this record is made and bodies are captured, if at all, by the separate opt-in inspector with its own redaction.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "share_id",++ "request_id",++ "decision",++ "at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^aud_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "share_id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "request_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64,++ "description": "The edge request id, for joining a row to a captured request and a trace; never a secret."++ },++ "subject": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "Who the policy identified; absent for an anonymous request."++ },++ "ip": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 45,++ "description": "The observed client address (IPv4 or IPv6). Behind Cloudflare it is CF-Connecting-IP, trusted only from Cloudflare's ranges."++ },++ "country": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{2}$",++ "description": "ISO 3166-1 alpha-2 country from the edge's trusted lookup."++ },++ "user_agent": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 512++ },++ "method": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3,10}$"++ },++ "path": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2048,++ "description": "Path only, with the query string removed. No body, header, cookie or token is ever stored here."++ },++ "status": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 100,++ "maximum": 599++ },++ "decision": {++ "enum": [++ "allow",++ "deny",++ "challenge",++ "redirect",++ "error"++ ],++ "description": "The policy outcome: allow, deny, a bot challenge, an OIDC/sign-in redirect, or an edge error."++ },++ "reason": {++ "type": "string",++ "maxLength": 120,++ "description": "A stable, short reason such as policy.denied or policy.password_required; never free-form request data."++ },++ "policy_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "policy_version": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "bytes_in": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "bytes_out": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "edge_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80++ },++ "region": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40++ },++ "at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "connect.device": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/connect/device.v1.json",++ "title": "ConnectDevice",++ "description": "connect.device@1: one device in a private Connect mesh, as its owner sees it. A device belongs to exactly one tenant (the person's own network or a project's), is identified by a Network node principal, and carries only self-reported posture. The mesh control-plane's internal node id, pre-auth key and WireGuard keys are not part of this document.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "tenant",++ "name",++ "status",++ "os",++ "version",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who owns the device."++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The project whose mesh the device joins; null for a personal mesh."++ },++ "tenant": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A person's own private network."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "project"++ },++ "id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A project's private network."++ }++ ],++ "description": "The isolation boundary: one headscale tenant per workspace in v1, never one shared tailnet for all consumer devices."++ },++ "node_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The Network node principal the device presents; null for an agent or server that joins through a pre-auth key."++ },++ "name": {++ "type": "string",++ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",++ "description": "One DNS label in the mesh, shown as name.owner (e.g. pc.alice); never a public hostname."++ },++ "status": {++ "enum": [++ "pending",++ "online",++ "offline",++ "removed"++ ],++ "description": "pending waits for the owner's approval of a pre-auth join; removed is terminal and its keys are revoked."++ },++ "os": {++ "enum": [++ "linux",++ "macos",++ "windows",++ "ios",++ "android",++ "other"++ ]++ },++ "arch": {++ "enum": [++ "amd64",++ "arm64",++ "386",++ "arm",++ "other"++ ]++ },++ "version": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "The Node release running on the device."++ },++ "addresses": {++ "type": "array",++ "maxItems": 8,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "The device's private mesh addresses; never a public address."++ },++ "tags": {++ "type": "array",++ "maxItems": 50,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{0,62}$"++ }++ },++ "posture": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "Self-reported posture, a signal not a guarantee; checked_at says when it was read.",++ "properties": {++ "node_current": {++ "type": "boolean"++ },++ "os_patched": {++ "type": "boolean"++ },++ "firewall_on": {++ "type": "boolean"++ },++ "disk_encryption": {++ "type": "boolean"++ },++ "risky_ports": {++ "type": "array",++ "maxItems": 200,++ "uniqueItems": true,++ "items": {++ "type": "integer",++ "minimum": 1,++ "maximum": 65535++ }++ },++ "checked_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "required": [++ "node_current",++ "os_patched",++ "firewall_on",++ "disk_encryption",++ "risky_ports",++ "checked_at"++ ]++ },++ "routes": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ },++ "description": "Subnet routes the device advertises, e.g. 192.168.1.0/24; empty for none."++ },++ "exit_node": {++ "type": "boolean",++ "description": "Whether the owner allows the device to act as an exit node for this tenant only; never a public proxy."++ },++ "approved_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "approved_by": {++ "allOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ }++ ],++ "description": "The person who approved the join; null while pending."++ },++ "last_seen_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When an unapproved or temporary device stops being valid; null for no expiry."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ },++ "connect.policy": {++ "version": "1.0.0",++ "owner": "connect",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/connect/policy.v1.json",++ "title": "ConnectPolicy",++ "description": "connect.policy@1: the grants, routes and exit-node settings of one private Connect tenant. It is deliberately tenant-scoped: a rule never names a device, person or tag from another tenant, and default_action is deny unless the owner opens a path. It describes intent, not the control plane's internal ACL file or keys.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "tenant",++ "version",++ "rules",++ "default_action",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "tenant": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A person's own private network."++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "type",++ "id"++ ],++ "properties": {++ "type": {++ "const": "project"++ },++ "id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "A project's private network."++ }++ ],++ "description": "The one tenant this policy can affect; a rule or route naming another tenant's id is invalid."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "integer",++ "minimum": 1,++ "description": "Bumped on every change; a device keeps the version it last applied."++ },++ "default_action": {++ "enum": [++ "allow",++ "deny"++ ],++ "description": "deny unless the owner explicitly opens a path."++ },++ "rules": {++ "type": "array",++ "maxItems": 200,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "action",++ "sources",++ "destinations"++ ],++ "properties": {++ "action": {++ "enum": [++ "allow",++ "deny"++ ]++ },++ "sources": {++ "type": "array",++ "minItems": 1,++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/selector"++ }++ },++ "destinations": {++ "type": "array",++ "minItems": 1,++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/selector"++ }++ },++ "ports": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/port"++ },++ "description": "TCP/UDP ports or ranges; omit for all ports."++ },++ "description": {++ "type": "string",++ "maxLength": 200++ }++ }++ }++ },++ "exit_node": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "properties": {++ "enabled": {++ "type": "boolean"++ },++ "device_ids": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ "required": [++ "enabled",++ "device_ids"++ ],++ "description": "Exit nodes are usable only by this tenant's devices; no public share may target one."++ },++ "subnet_routes": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "device_id",++ "cidr"++ ],++ "properties": {++ "device_id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "cidr": {++ "type": "string",++ "minLength": 2,++ "maxLength": 64++ }++ }++ }++ },++ "updated_by": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ },++ "$defs": {++ "selector": {++ "type": "string",++ "pattern": "^(?:\\*|(?:user|device|tag|project):[A-Za-z0-9_.:-]+)$",++ "description": "*, or user:<usr_…>, device:<dev_…>, tag:<name> or project:<prj_…>; all within this tenant."++ },++ "port": {++ "type": "string",++ "pattern": "^(?:\\*|[0-9]{1,5}(?:-[0-9]{1,5})?)$",++ "description": "* for any port, 443, or a range such as 8000-8100."++ }++ }++ }++ },++ "events.hook": {++ "version": "1.0.0",++ "owner": "events",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/hook.v1.json",++ "title": "EventsHook",++ "description": "events.hook@1: one durable webhook subscription managed through OpenVibe.Website Connect, delivered by OpenVibe.Events. The hook names its share, the public ingress path, the event topics it accepts, filters/transform, the retry window and the signature version. The signing secret is shown once when the hook is created or its secret is rotated; secret_set says one exists, and no field of this document ever carries the value.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "owner",++ "share_id",++ "name",++ "endpoint",++ "topics",++ "status",++ "signature",++ "retry",++ "secret_set",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "owner": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "share_id": {++ "type": "string",++ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The share whose tunnel receives deliveries; delivery never goes to an arbitrary host."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "endpoint": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://",++ "description": "The public ingress, https://hooks.openvibe.website/h/<id>; Connect also records the on-share path https://<share host>/__ov/hook/<id>."++ },++ "topics": {++ "type": "array",++ "minItems": 1,++ "maxItems": 50,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Event types this hook accepts, e.g. github.push.received or stripe.invoice.paid; Event's topic patterns may be used by the delivery filter."++ },++ "filters": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "path"++ ],++ "properties": {++ "path": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "A JSONPath into the event payload."++ },++ "equals": {++ "oneOf": [++ {++ "type": "string",++ "maxLength": 500++ },++ {++ "type": "number"++ },++ {++ "type": "boolean"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ }++ },++ "transform": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "properties": {++ "headers": {++ "type": "object",++ "maxProperties": 20,++ "additionalProperties": {++ "type": "string",++ "maxLength": 500++ },++ "description": "Headers added to the delivery; never the hook's signing secret."++ },++ "body_template": {++ "type": "string",++ "maxLength": 10000,++ "description": "An optional template applied to the event; the raw payload is used when absent."++ }++ }++ },++ "retry": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "window_seconds",++ "max_attempts"++ ],++ "properties": {++ "window_seconds": {++ "type": "integer",++ "minimum": 60,++ "maximum": 604800,++ "description": "How long Events keeps retrying while the share is offline; seven days is the hook default."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100++ }++ }++ },++ "signature": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "version",++ "tolerance_seconds"++ ],++ "properties": {++ "version": {++ "const": "v2"++ },++ "tolerance_seconds": {++ "type": "integer",++ "minimum": 1,++ "maximum": 3600,++ "description": "How far a signed timestamp may drift before the Node refuses it; 300 is the default."++ }++ }++ },++ "max_payload_bytes": {++ "type": "integer",++ "minimum": 1,++ "maximum": 1048576,++ "default": 1048576++ },++ "status": {++ "enum": [++ "active",++ "paused",++ "revoked"++ ]++ },++ "secret_set": {++ "type": "boolean",++ "description": "True once a signing secret exists. The secret itself is returned only in the create/rotate answer and is never stored in this contract, a delivery record or an audit entry."++ },++ "secret_rotated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "last_delivery_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "dead_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When the hook stopped receiving deliveries after its retry window; null while it is live."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ },++ "href": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://"++ }++ }++ }++ },++ "events.hook-delivery": {++ "version": "1.0.0",++ "owner": "events",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/hook-delivery.v1.json",++ "title": "EventsHookDelivery",++ "description": "events.hook-delivery@1: one attempt by OpenVibe.Events to deliver an event to a hook. It carries the outcome, timing, size and hash of the payload, never the payload itself, its headers, the signature or the hook's secret. A delivery is retried inside its hook's window and dead-lettered after it; replay creates a new attempt that names the one it replays.",++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "hook_id",++ "event_id",++ "event_type",++ "attempt",++ "status",++ "payload_bytes",++ "payload_sha256",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "hook_id": {++ "type": "string",++ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "event_id": {++ "type": "string",++ "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "event_type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "attempt": {++ "type": "integer",++ "minimum": 1,++ "maximum": 100++ },++ "status": {++ "enum": [++ "queued",++ "delivering",++ "delivered",++ "failed",++ "dead"++ ],++ "description": "failed is a retryable attempt; dead is the last, after the hook's window. A 2xx answer marks delivered."++ },++ "request_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 64,++ "description": "The delivery request id, for tracing; not a secret."++ },++ "response_code": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 100,++ "maximum": 599++ },++ "latency_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "payload_bytes": {++ "type": "integer",++ "minimum": 0,++ "maximum": 1048576++ },++ "payload_sha256": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$",++ "description": "The SHA-256 of the signed body, so a consumer can check it without the body being stored here."++ },++ "signature_version": {++ "const": "v2"++ },++ "next_attempt_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "attempted_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "completed_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "dead_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "error": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 400,++ "description": "A short transport or status reason; never a response body."++ },++ "replay_of": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The failed or dead attempt this one replays."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }}}}@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Actor API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bench API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bot API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Domains API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Food API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Inventory API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.MediaHub API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRestream API",-- "version": "0.278.0",-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Play API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Quest API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Report API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Run API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Services API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Space API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Today API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Vision API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Watch API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Work API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Zone API",-- "version": "0.278.0",-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.279.0",++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -282192,3 +282192,2284 @@ export interface TodayReminderResult {next_at?: string | null;error?: string;}++++/** [email protected] (owner: connect) */++/**++ * website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.++ */++export interface WebsiteShare {++ id: string;++ /**++ * The person who owns the share. A project share is still owned by a person; project_id says which project it belongs to.++ */++ owner: SubjectRef;++ /**++ * The project scope when the share belongs to one; null for a personal share. The token's project scope, never a caller-supplied body field, decides access.++ */++ project_id?: string | null;++ /**++ * The Network node principal expected to serve the tunnel; null until one is bound.++ */++ node_id?: string | null;++ /**++ * One DNS label under openvibe.website. The first proof uses the single label agents-<owner> because Universal SSL covers one label only.++ */++ name: string;++ /**++ * The fully qualified public host, lowercased; the default is name.openvibe.website, and a verified custom domain may replace it.++ */++ hostname: string;++ /**++ * What the share carries. http and https are HTTP/WS/SSE shares; tcp and udp are port shares.++ */++ protocol: "http" | "https" | "tcp" | "udp";++ /**++ * The local target the Node may forward to, e.g. 127.0.0.1:3000, tcp://127.0.0.1:25565 or udp://127.0.0.1:19132. Loopback by default; a LAN target is allowed only when the owner enabled it locally.++ */++ local_target: string;++ /**++ * review holds a public share for the abuse screen; revoked is terminal and closes the route and tunnel.++ */++ status: "draft" | "review" | "active" | "paused" | "revoked";++ policy_id: string;++ /**++ * The signed policy version the edge enforces. A request is checked against this version; revocation bumps it and closes the tunnel.++ */++ policy_version: number;++ /**++ * The edge region the share is pinned to, e.g. eu or us-east; null before placement.++ */++ region?: string | null;++ /**++ * The edge instance serving the active tunnel; null while the share is offline.++ */++ edge_id?: string | null;++ /**++ * When the share revokes itself; null for no expiry.++ */++ expires_at?: string | null;++ /**++ * The last successful Node liveness check; null until the first tunnel.++ */++ last_seen_at?: string | null;++ created_at: string;++ updated_at: string;++ revoked_at?: string | null;++ href?: string;++}++++/** [email protected] (owner: connect) */++/**++ * website.share-create-request@1: the body of website.share.create (POST /api/v1/shares). The caller names one DNS label, the protocol, the local target and the policy; the project scope comes from the token, never from this body. Connect returns only website.share@1; CON-5 defines a separate authenticated exchange for the one-time tunnel token.++ */++export interface WebsiteShareCreateRequest {++ /**++ * The requested single label under openvibe.website. A taken or reserved label is refused; the UI may suggest name-2 rather than changing it silently.++ */++ name: string;++ protocol: "http" | "https" | "tcp" | "udp";++ /**++ * The local port the Node exposes, e.g. 127.0.0.1:3000; never an arbitrary internet host.++ */++ local_target: string;++ /**++ * The policy to enforce. An id binds an existing owner-checked policy; without one, Connect creates it. Server-owned fields are refused in this caller body. Credential and link secrets are configured separately.++ */++ policy: WebsitePolicy & {++ owner?: never;++ project_id?: never;++ version?: never;++ created_at?: never;++ updated_at?: never;++ password_set?: never;++ };++ /**++ * Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects.++ */++ node_id?: string;++ /**++ * Optional preferred edge region; placement may ignore it when capacity or policy requires.++ */++ region?: string;++}++++/** [email protected] (owner: connect) */++/**++ * website.share-connect-request@1: what a Node presents to open the tunnel for one share (POST /api/v1/shares/:id/tunnel, capability website.share.connect). The Node proves its Network node principal with a client-credentials token whose audience is openvibe.website, and this short-lived, single-use tunnel_token minted by Connect binds the connection to one share. The edge checks both; neither alone opens the route. The token is presented once and never stored, logged or returned.++ */++export interface WebsiteShareConnectRequest {++ share_id: string;++ /**++ * Must equal the node principal in the Network token; a mismatch is refused and audited.++ */++ node_id: string;++ /**++ * A short-lived, single-use, share-scoped token minted by Connect. Send it once over TLS; never put it in a URL, log, audit entry or event.++ */++ tunnel_token: string;++ /**++ * The data-plane transport the Node is trying. quic is ALPN ovconnect/1; http2-ws is the 443 fallback when UDP is blocked.++ */++ transport?: "quic" | "http2-ws";++ /**++ * The Node release, for posture and diagnostics; never used for authorization.++ */++ node_version?: string;++ /**++ * The edge region the Node is dialing; the edge refuses a region it does not serve.++ */++ region?: string;++}++++/** [email protected] (owner: connect) */++/**++ * website.policy@1: how a Share decides who may reach it and under what conditions (public, OpenVibe sign-in, named people or projects, password, one-time link, expiry), plus IP/country rules, rate limits, a bot check, a WAF preset and required device posture. mode is the only field required except that expiry also requires a non-null expires_at. id, owner and version are present when Connect returns a stored policy. Password and one-time policies stay inactive until their credential or link is configured through the CON-5 API. A password is stored as a one-way hash beside this record and is never part of this document; password_set only says one exists.++ */++export interface WebsitePolicy {++ /**++ * Present on a stored policy; omitted when the policy is written inline with a new share.++ */++ id?: string;++ /**++ * The person who owns the policy; set from the token when a policy is created.++ */++ owner?: SubjectRef;++ /**++ * The project scope, when the policy belongs to one.++ */++ project_id?: string | null;++ name?: string;++ /**++ * public: anyone with the link after the abuse screen; signin: any OpenVibe account (optionally a project); named: allowed_subjects/allowed_projects only; password: a shared password; one_time: single-use links; expiry: public access only until expires_at. An expires_at may also cap any other mode.++ */++ mode: "public" | "signin" | "named" | "password" | "one_time" | "expiry";++ /**++ * Explicit people or agents allowed in named mode; a project's members are allowed through allowed_projects instead.++ *++ * @maxItems 500++ */++ allowed_subjects?: SubjectRef[];++ /**++ * Projects whose members may reach a named or signin share.++ *++ * @maxItems 100++ */++ allowed_projects?: string[];++ /**++ * CIDR ranges allowed before any other rule; empty means no IP allowlist.++ *++ * @maxItems 100++ */++ ip_allow?: string[];++ /**++ * CIDR ranges always refused; evaluated before the allowlist.++ *++ * @maxItems 100++ */++ ip_deny?: string[];++ /**++ * ISO 3166-1 alpha-2 country codes allowed; empty means all.++ *++ * @maxItems 250++ */++ country_allow?: string[];++ /**++ * @maxItems 250++ */++ country_deny?: string[];++ rate_limits?: {++ per_share_per_minute?: number;++ per_ip_per_minute?: number;++ max_connections?: number;++ max_request_bytes?: number;++ };++ bot_check?: "none" | "turnstile";++ /**++ * Defense in depth at the edge; Cloudflare's managed rules run in front of it for HTTP/WS.++ */++ waf_preset?: "none" | "standard" | "strict" | "api";++ /**++ * Self-reported device posture a share may require; a signal, not a guarantee.++ *++ * @maxItems 10++ */++ posture_required?:++ | []++ | ["node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ]++ | [++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",++ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"++ ];++ /**++ * Read-only: true when a password hash exists. The hash itself never leaves Connect.++ */++ password_set?: boolean;++ /**++ * An optional hint shown on the password page; never the password.++ */++ password_hint?: string | null;++ /**++ * For one_time mode; 1 unless the owner allows more.++ */++ one_time_max_uses?: number;++ /**++ * How long a host-only __Host-ov_session on the share host stays valid after sign-in.++ */++ session_ttl_seconds?: number;++ /**++ * The absolute end of the policy; required and non-null for expiry mode, optional for other modes.++ */++ expires_at?: string | null;++ /**++ * The signed policy version the edge caches and checks on every request; present on a stored policy.++ */++ version?: number;++ created_at?: string;++ updated_at?: string;++}++++/** [email protected] (owner: connect) */++/**++ * website.audit-entry@1: one decision row for a Share, as the owner or a moderator sees it. It answers who reached (or tried to reach) a share, from where, what method and path, what the policy decided and how much crossed the edge. It deliberately carries no request or response body, headers, cookies, bearer or tunnel tokens, query string, password or hook payload: query strings are stripped before this record is made and bodies are captured, if at all, by the separate opt-in inspector with its own redaction.++ */++export interface WebsiteAuditEntry {++ id: string;++ share_id: string;++ /**++ * The edge request id, for joining a row to a captured request and a trace; never a secret.++ */++ request_id: string;++ /**++ * Who the policy identified; absent for an anonymous request.++ */++ subject?: SubjectRef;++ /**++ * The observed client address (IPv4 or IPv6). Behind Cloudflare it is CF-Connecting-IP, trusted only from Cloudflare's ranges.++ */++ ip?: string | null;++ /**++ * ISO 3166-1 alpha-2 country from the edge's trusted lookup.++ */++ country?: string | null;++ user_agent?: string | null;++ method?: string | null;++ /**++ * Path only, with the query string removed. No body, header, cookie or token is ever stored here.++ */++ path?: string | null;++ status?: number | null;++ /**++ * The policy outcome: allow, deny, a bot challenge, an OIDC/sign-in redirect, or an edge error.++ */++ decision: "allow" | "deny" | "challenge" | "redirect" | "error";++ /**++ * A stable, short reason such as policy.denied or policy.password_required; never free-form request data.++ */++ reason?: string;++ policy_id?: string | null;++ policy_version?: number | null;++ bytes_in?: number | null;++ bytes_out?: number | null;++ edge_id?: string | null;++ region?: string | null;++ at: string;++}++++/** [email protected] (owner: connect) */++/**++ * connect.device@1: one device in a private Connect mesh, as its owner sees it. A device belongs to exactly one tenant (the person's own network or a project's), is identified by a Network node principal, and carries only self-reported posture. The mesh control-plane's internal node id, pre-auth key and WireGuard keys are not part of this document.++ */++export interface ConnectDevice {++ id: string;++ /**++ * The person who owns the device.++ */++ owner: SubjectRef;++ /**++ * The project whose mesh the device joins; null for a personal mesh.++ */++ project_id?: string | null;++ /**++ * The isolation boundary: one headscale tenant per workspace in v1, never one shared tailnet for all consumer devices.++ */++ tenant:++ | {++ type: "user";++ id: string;++ }++ | {++ type: "project";++ id: string;++ };++ /**++ * The Network node principal the device presents; null for an agent or server that joins through a pre-auth key.++ */++ node_id?: string | null;++ /**++ * One DNS label in the mesh, shown as name.owner (e.g. pc.alice); never a public hostname.++ */++ name: string;++ /**++ * pending waits for the owner's approval of a pre-auth join; removed is terminal and its keys are revoked.++ */++ status: "pending" | "online" | "offline" | "removed";++ os: "linux" | "macos" | "windows" | "ios" | "android" | "other";++ arch?: "amd64" | "arm64" | "386" | "arm" | "other";++ /**++ * The Node release running on the device.++ */++ version: string;++ /**++ * The device's private mesh addresses; never a public address.++ *++ * @maxItems 8++ */++ addresses?:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string];++ /**++ * @maxItems 50++ */++ tags?: string[];++ /**++ * Self-reported posture, a signal not a guarantee; checked_at says when it was read.++ */++ posture?: {++ node_current: boolean;++ os_patched: boolean;++ firewall_on: boolean;++ disk_encryption: boolean;++ /**++ * @maxItems 200++ */++ risky_ports: number[];++ checked_at: string;++ } | null;++ /**++ * Subnet routes the device advertises, e.g. 192.168.1.0/24; empty for none.++ *++ * @maxItems 50++ */++ routes?: string[];++ /**++ * Whether the owner allows the device to act as an exit node for this tenant only; never a public proxy.++ */++ exit_node?: boolean;++ approved_at?: string | null;++ /**++ * The person who approved the join; null while pending.++ */++ approved_by?: SubjectRef;++ last_seen_at?: string | null;++ /**++ * When an unapproved or temporary device stops being valid; null for no expiry.++ */++ expires_at?: string | null;++ created_at: string;++ updated_at: string;++ revoked_at?: string | null;++}++++/** [email protected] (owner: connect) */++/**++ * connect.policy@1: the grants, routes and exit-node settings of one private Connect tenant. It is deliberately tenant-scoped: a rule never names a device, person or tag from another tenant, and default_action is deny unless the owner opens a path. It describes intent, not the control plane's internal ACL file or keys.++ */++export interface ConnectPolicy {++ id: string;++ owner: SubjectRef;++ project_id?: string | null;++ /**++ * The one tenant this policy can affect; a rule or route naming another tenant's id is invalid.++ */++ tenant:++ | {++ type: "user";++ id: string;++ }++ | {++ type: "project";++ id: string;++ };++ name?: string;++ /**++ * Bumped on every change; a device keeps the version it last applied.++ */++ version: number;++ /**++ * deny unless the owner explicitly opens a path.++ */++ default_action: "allow" | "deny";++ /**++ * @maxItems 200++ */++ rules: {++ action: "allow" | "deny";++ /**++ * @minItems 1++ * @maxItems 50++ */++ sources: [string, ...string[]];++ /**++ * @minItems 1++ * @maxItems 50++ */++ destinations: [string, ...string[]];++ /**++ * TCP/UDP ports or ranges; omit for all ports.++ *++ * @maxItems 50++ */++ ports?: string[];++ description?: string;++ }[];++ /**++ * Exit nodes are usable only by this tenant's devices; no public share may target one.++ */++ exit_node?: {++ enabled: boolean;++ /**++ * @maxItems 20++ */++ device_ids:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ } | null;++ /**++ * @maxItems 100++ */++ subnet_routes?: {++ device_id: string;++ cidr: string;++ }[];++ updated_by?: SubjectRef;++ created_at: string;++ updated_at: string;++}++++/** [email protected] (owner: events) */++/**++ * events.hook@1: one durable webhook subscription managed through OpenVibe.Website Connect, delivered by OpenVibe.Events. The hook names its share, the public ingress path, the event topics it accepts, filters/transform, the retry window and the signature version. The signing secret is shown once when the hook is created or its secret is rotated; secret_set says one exists, and no field of this document ever carries the value.++ */++export interface EventsHook {++ id: string;++ owner: SubjectRef;++ project_id?: string | null;++ /**++ * The share whose tunnel receives deliveries; delivery never goes to an arbitrary host.++ */++ share_id: string;++ name: string;++ /**++ * The public ingress, https://hooks.openvibe.website/h/<id>; Connect also records the on-share path https://<share host>/__ov/hook/<id>.++ */++ endpoint: string;++ /**++ * Event types this hook accepts, e.g. github.push.received or stripe.invoice.paid; Event's topic patterns may be used by the delivery filter.++ *++ * @minItems 1++ * @maxItems 50++ */++ topics: [string, ...string[]];++ /**++ * @maxItems 20++ */++ filters?:++ | []++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ]++ | [++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ },++ {++ /**++ * A JSONPath into the event payload.++ */++ path: string;++ equals?: string | number | boolean | null;++ }++ ];++ transform?: {++ /**++ * Headers added to the delivery; never the hook's signing secret.++ */++ headers?: {++ [k: string]: string | undefined;++ };++ /**++ * An optional template applied to the event; the raw payload is used when absent.++ */++ body_template?: string;++ } | null;++ retry: {++ /**++ * How long Events keeps retrying while the share is offline; seven days is the hook default.++ */++ window_seconds: number;++ max_attempts: number;++ };++ signature: {++ version: "v2";++ /**++ * How far a signed timestamp may drift before the Node refuses it; 300 is the default.++ */++ tolerance_seconds: number;++ };++ max_payload_bytes?: number;++ status: "active" | "paused" | "revoked";++ /**++ * True once a signing secret exists. The secret itself is returned only in the create/rotate answer and is never stored in this contract, a delivery record or an audit entry.++ */++ secret_set: boolean;++ secret_rotated_at?: string | null;++ last_delivery_at?: string | null;++ /**++ * When the hook stopped receiving deliveries after its retry window; null while it is live.++ */++ dead_at?: string | null;++ created_at: string;++ updated_at: string;++ href?: string;++}++++/** [email protected] (owner: events) */++/**++ * events.hook-delivery@1: one attempt by OpenVibe.Events to deliver an event to a hook. It carries the outcome, timing, size and hash of the payload, never the payload itself, its headers, the signature or the hook's secret. A delivery is retried inside its hook's window and dead-lettered after it; replay creates a new attempt that names the one it replays.++ */++export interface EventsHookDelivery {++ id: string;++ hook_id: string;++ event_id: string;++ event_type: string;++ attempt: number;++ /**++ * failed is a retryable attempt; dead is the last, after the hook's window. A 2xx answer marks delivered.++ */++ status: "queued" | "delivering" | "delivered" | "failed" | "dead";++ /**++ * The delivery request id, for tracing; not a secret.++ */++ request_id?: string | null;++ response_code?: number | null;++ latency_ms?: number | null;++ payload_bytes: number;++ /**++ * The SHA-256 of the signed body, so a consumer can check it without the body being stored here.++ */++ payload_sha256: string;++ signature_version?: "v2";++ next_attempt_at?: string | null;++ attempted_at?: string | null;++ completed_at?: string | null;++ dead_at?: string | null;++ /**++ * A short transport or status reason; never a response body.++ */++ error?: string | null;++ /**++ * The failed or dead attempt this one replays.++ */++ replay_of?: string | null;++ created_at: string;++}@@@ -6,7 +6,7 @@const crypto = require('crypto');const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';--const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', consent: 'cns', job: 'job', task: 'tsk', watch: 'wch', wait: 'wai', observation: 'wco', check: 'ckr', subscription: 'sub', deploy: 'dpl', domain: 'dom', manifest: 'mfs', release: 'rel', site: 'sit', robot: 'rob', stream: 'std', webhook_delivery: 'whd' };++const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', consent: 'cns', job: 'job', task: 'tsk', watch: 'wch', wait: 'wai', observation: 'wco', check: 'ckr', subscription: 'sub', deploy: 'dpl', domain: 'dom', manifest: 'mfs', release: 'rel', site: 'sit', robot: 'rob', stream: 'std', webhook_delivery: 'whd', share: 'shr', device: 'dev', hook: 'hok', policy: 'pol', audit: 'aud' };const SLUG_TYPES = new Set(['service', 'system']);const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "connect.device.manage",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): approve a pending device, change its name, tags, routes or exit-node setting, or remove it (POST /api/v1/devices/:id/approve, PATCH /api/v1/devices/:id, DELETE /api/v1/devices/:id) as connect.device@1. Removal revokes the mesh key at the control plane at once; an approval never admits a device to another tenant. Pre-auth keys and WireGuard keys are never returned or logged.",++ "permissions": [++ "connect:devices:manage"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "connect-device",++ "events": [],++ "implementedBy": [++ "POST /api/v1/devices/:id/approve",++ "PATCH /api/v1/devices/:id",++ "DELETE /api/v1/devices/:id"++ ],++ "inputSchema": "connect.device@1",++ "outputSchema": "connect.device@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "connect.device.read",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): list the devices in the caller's private mesh and read one by id (GET /api/v1/devices and GET /api/v1/devices/:id) as connect.device@1. Every device belongs to exactly one tenant, so a read never crosses a tenant boundary; a device in another tenant is 404. Posture is the self-reported signal last checked, never a claim the service verifies.",++ "permissions": [++ "connect:devices:read"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "connect-device",++ "events": [],++ "implementedBy": [++ "GET /api/v1/devices",++ "GET /api/v1/devices/:id"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "connect.device@1"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "connect.policy.read",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "first-party",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): read one tenant's mesh policy (GET /api/v1/connect/policies/:id) as connect.policy@1, with its grants, default action, exit-node setting and subnet routes. A policy is readable only inside its own tenant; the answer carries no pre-auth key, WireGuard key or control-plane ACL file.",++ "permissions": [++ "connect:policies:read"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "connect-policy",++ "events": [],++ "implementedBy": [++ "GET /api/v1/connect/policies/:id"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "connect.policy@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "connect.policy.write",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "first-party",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): write one tenant's mesh grants, routes and exit-node setting (PUT /api/v1/connect/policies/:id) from a connect.policy@1. The tenant in the body must be the caller's tenant; a rule or route naming another tenant's device is refused. Exit-node traffic is opened only to this tenant's devices and never to a public share. A write bumps the policy version the devices apply.",++ "permissions": [++ "connect:policies:write"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "connect-policy",++ "events": [],++ "implementedBy": [++ "PUT /api/v1/connect/policies/:id"++ ],++ "inputSchema": "connect.policy@1",++ "outputSchema": "connect.policy@1"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "events.hook.manage",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): create, change, pause, resume, rotate the secret of or revoke a durable hook (POST /api/v1/hooks, PATCH /api/v1/hooks/:id, DELETE /api/v1/hooks/:id) as events.hook@1. The signing secret is shown once in the create or rotate answer and stored by Events; it is never in a read, a delivery record, an event or an audit entry. The delivery endpoint is fixed to the hook's own share host, so a hook cannot be pointed at another host.",++ "permissions": [++ "events:hooks:manage"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-hook",++ "events": [],++ "implementedBy": [++ "POST /api/v1/hooks",++ "PATCH /api/v1/hooks/:id",++ "DELETE /api/v1/hooks/:id"++ ],++ "inputSchema": "events.hook@1",++ "outputSchema": "events.hook@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "events.hook.read",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): read a hook and its delivery log (GET /api/v1/hooks/:id and GET /api/v1/hooks/:id/deliveries) as events.hook@1 and events.hook-delivery@1. The delivery log says attempt, status, response code, latency, size and payload hash; it never returns the payload, its headers, the signature or the hook's secret. Replay is a write and needs events.hook.manage.",++ "permissions": [++ "events:hooks:read"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-hook",++ "events": [],++ "implementedBy": [++ "GET /api/v1/hooks/:id",++ "GET /api/v1/hooks/:id/deliveries"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "events.hook-delivery@1"++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "website.abuse.report",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, any signed-in caller): report a share (POST /api/v1/shares/:id/report) with a short reason and, optionally, a public evidence URL; a report never carries a password, cookie or private message. Reports go to the moderation queue with the share's captured evidence and the reporter's subject. The request and answer bodies are added with CON-5's abuse contracts; the capability is registered here so the queue and the grant exist before code.",++ "permissions": [++ "website:abuse:report"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "website-abuse",++ "events": [],++ "implementedBy": [++ "POST /api/v1/shares/:id/report"++ ]++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "website.abuse.review",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "internal",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, staff or moderator only): read the abuse queue and decide one report (GET /api/v1/admin/abuse, POST /api/v1/admin/abuse/:id/:action) — hold, activate, pause or takedown a share. A staff decision is audited with the moderator's subject and the evidence, not the request body; a takedown is also announced to Domains for a custom hostname. This is an internal moderation capability, never delegated to user or app tokens.",++ "permissions": [++ "website:abuse:review"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "website-abuse",++ "events": [++ "website.share.review",++ "website.share.revoked"++ ],++ "implementedBy": [++ "GET /api/v1/admin/abuse",++ "POST /api/v1/admin/abuse/:id/:action"++ ]++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "website.admin.manage",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "internal",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, staff or operations only): act on a share from the operator view (POST /api/v1/admin/shares/:id/:action) — pause it, revoke it, release an abuse hold or override a quota — as a website.share@1 action. Every action writes website.share.paused or website.share.revoked and an audit row naming the operator; it can close an edge tunnel at once. Internal, never delegated, and never a way to read a share's traffic or secrets.",++ "permissions": [++ "website:admin:manage"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "website-admin",++ "events": [++ "website.share.paused",++ "website.share.revoked"++ ],++ "implementedBy": [++ "POST /api/v1/admin/shares/:id/:action"++ ],++ "inputSchema": "website.share@1",++ "outputSchema": "website.share@1"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "website.admin.read",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "internal",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, staff or operations only): read the operator view of shares, tunnels, hooks and quota counters (GET /api/v1/admin/shares and GET /api/v1/admin/shares/:id), beyond what the owner sees but still without tunnel tokens, hook secrets, request bodies or message content. This is the read half of the internal admin surface; a grant is never issued to user, app or agent tokens.",++ "permissions": [++ "website:admin:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "website-admin",++ "events": [],++ "implementedBy": [++ "GET /api/v1/admin/shares",++ "GET /api/v1/admin/shares/:id"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "website.share@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "website.domain.manage",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): attach, verify or remove a custom domain for a share with POST /api/v1/domains from a domains.proposal@1. Connect delegates the DNS change to OpenVibe.Domains — the owner approves it in the Action Center, Domains writes the TXT/CNAME and answers with the applied proposal — and Connect never writes DNS itself or asks for the provider credential. Apex CNAME flattening, Cloudflare for SaaS and the certificate path are CON-2 decisions; the first proof uses a single-label openvibe.website host only.",++ "permissions": [++ "website:domains:manage"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-domain",++ "events": [],++ "implementedBy": [++ "POST /api/v1/domains"++ ],++ "inputSchema": "domains.proposal@1",++ "outputSchema": "domains.proposal@1"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "website.policy.read",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): read one share policy by id (GET /api/v1/policies/:id) as website.policy@1. The answer says which mode is enforced, its allowances and limits, and the signed version the edge caches; it never includes a password hash or any secret. Another owner's policy is 404.",++ "permissions": [++ "website:policies:read"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-policy",++ "events": [],++ "implementedBy": [++ "GET /api/v1/policies/:id"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "website.policy@1"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "website.policy.write",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): create or change a share policy with POST /api/v1/policies or PATCH /api/v1/policies/:id from a website.policy@1. A password arrives in the request body, is hashed with Argon2id and is stored beside the policy, never in the policy document; a write bumps the signed version and tells the edge. The owner and project come from the token, so a body cannot move a policy to another project.",++ "permissions": [++ "website:policies:write"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-policy",++ "events": [],++ "implementedBy": [++ "POST /api/v1/policies",++ "PATCH /api/v1/policies/:id"++ ],++ "inputSchema": "website.policy@1",++ "outputSchema": "website.policy@1"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "website.share.connect",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "first-party",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, node principal): a Node opens one share's tunnel with POST /api/v1/shares/:id/tunnel and a website.share-connect-request@1. The edge checks the Network node token (client credentials, subject node:nod_…, audience openvibe.website) and the short-lived, single-use tunnel token minted by Connect; neither alone opens the route. The node token's subject must equal node:<node_id> from the request, and a mismatch is refused and audited. Not delegated to user or app tokens.",++ "permissions": [++ "website:tunnels:connect"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-tunnel",++ "events": [++ "website.share.activated"++ ],++ "implementedBy": [++ "POST /api/v1/shares/:id/tunnel"++ ],++ "inputSchema": "website.share-connect-request@1",++ "outputSchema": "website.share@1"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "website.share.create",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): create a Share with POST /api/v1/shares from a website.share-create-request@1 — one DNS label, protocol, local target and policy. The POST returns only website.share@1 in draft, with no tunnel token. CON-5 defines the separate authenticated exchange that gives the Node a short-lived, single-use, share-scoped tunnel token; it is never stored, logged or put in an event. A public share stays in review until the abuse screen passes; no capability is granted by this contract.",++ "permissions": [++ "website:shares:create"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-share",++ "events": [++ "website.share.created"++ ],++ "implementedBy": [++ "POST /api/v1/shares"++ ],++ "inputSchema": "website.share-create-request@1",++ "outputSchema": "website.share@1"++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "website.share.delete",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): revoke and delete a share with DELETE /api/v1/shares/:id. Connect writes website.share.revoked, closes the edge route and tunnel within its 5-second revocation budget and keeps the audit trail; a public hostname returns 410, not a cached page. Deleting a node in Network closes every share for that node through network.node.revoked.",++ "permissions": [++ "website:shares:delete"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-share",++ "events": [++ "website.share.revoked"++ ],++ "implementedBy": [++ "DELETE /api/v1/shares/:id"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "common.no-body@1"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "website.share.inspect",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or granted app): read the request inspector for a share (GET /api/v1/shares/:id/requests), a bounded ring buffer of the last N requests as website.audit-entry@1 rows plus captured metadata. Bodies, headers and cookies are captured only when the owner opted in, under a separate redaction pass; query strings and tokens are stripped, and this read never returns a secret. The buffer is 24 hours on free and 7 days on paid plans.",++ "permissions": [++ "website:inspector:read"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-inspect",++ "events": [],++ "implementedBy": [++ "GET /api/v1/shares/:id/requests"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "website.audit-entry@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "website.share.read",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): list the caller's shares and read one by id (GET /api/v1/shares and GET /api/v1/shares/:id). A user token sees only its own shares and the projects it is a member of; another owner's share is 404, never 403. The list answers website.share@1 rows without tunnel tokens or captured bodies.",++ "permissions": [++ "website:shares:read"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-share",++ "events": [],++ "implementedBy": [++ "GET /api/v1/shares",++ "GET /api/v1/shares/:id"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "website.share@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "website.share.replay",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or granted app): replay one captured request through the same share (POST /api/v1/shares/:id/requests/:rid/replay). Replay is allowed only while the share and its tunnel are active and only to that share's own local target; it never becomes a general proxy and never changes the share's policy. The replay is audited as a new request with its own request_id.",++ "permissions": [++ "website:inspector:replay"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-inspect",++ "events": [],++ "implementedBy": [++ "POST /api/v1/shares/:id/requests/:rid/replay"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "common.no-body@1"++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "website.share.update",++ "version": "1.0",++ "owner": "connect",++ "status": "planned",++ "visibility": "public",++ "sensitive": true,++ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): rename a share, retarget its local port or change its policy with PATCH /api/v1/shares/:id. A policy change bumps website.share.policy_version so the edge's signed cache is stale at once; changing the public label needs the name registry, and a taken or reserved label is refused. Project scope comes from the token, never the body.",++ "permissions": [++ "website:shares:update"++ ],++ "resourceConstraints": [++ "owner",++ "project"++ ],++ "quotaClass": "website-share",++ "events": [],++ "implementedBy": [++ "PATCH /api/v1/shares/:id"++ ],++ "inputSchema": "website.share@1",++ "outputSchema": "website.share@1"++}nnew file mode 100644@@@ -0,0 +1,82 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": ">=0.279.0 <1.0.0"++ },++ "id": "connect",++ "name": "OpenVibe Connect",++ "status": "alpha",++ "repository": "OpenVibers/OpenVibe.Connect",++ "domains": [++ "connect.openvibe.website"++ ],++ "publicOrigin": "https://connect.openvibe.website",++ "exposure": {++ "state": "live",++ "publicSite": "service"++ },++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "website.*",++ "connect.*"++ ],++ "lifecycle": {++ "liveness": {++ "endpoint": "/api/health",++ "means": "the process is up and answers HTTP, with how many computers are connected; the database, Network's JWKS and the OAuth client are what /api/ready checks"++ },++ "shutdown": {++ "signal": "SIGTERM",++ "deadlineSeconds": 10,++ "drains": [++ "HTTP server closed and in-flight requests drained",++ "every tunnel closed with 1001 (the computers reconnect to the next process by themselves)",++ "the request counters written, the one-time codes dropped",++ "JWKS refresher stopped, telemetry flushed, PostgreSQL pool closed, then the Events subscriptions stopped"++ ]++ },++ "startupRecovery": {++ "resumes": [++ {++ "kind": "sessions",++ "what": "connected computers reconnect with backoff (1 s to 30 s) and say hello again; nothing about a tunnel is kept across a restart"++ },++ {++ "kind": "consumer",++ "what": "the account export and deletion subscriptions at OpenVibe.Events are renewed at boot; a delivery missed while stopped is redelivered and applied once (account_data_events keeps it idempotent)"++ }++ ]++ },++ "rollback": {++ "conditions": [++ "automatic: ovhost deploy puts the previous sha back and restarts again when the ready URL does not answer 2xx within ready.timeoutSeconds after the restart (exit 3)",++ "manual: ovhost rollback <service> --to <sha>"++ ],++ "window": "automatic only while ovhost waits for readiness after the restart; manual afterwards, as long as no blocker applies",++ "blockers": {++ "none": "migration 0001 only creates Connect's own tables (shares, share_visits) plus the SDK's; an earlier release reads none of them"++ }++ },++ "contracts": {++ "range": ">=0.279.0 <1.0.0"++ },++ "leases": {++ "none": "one process holds every tunnel; a second process would need the share-to-tunnel routing CON-3 adds"++ }++ },++ "notes": "Port 5240, unit openvibe-connect.service, PostgreSQL database ov_connect. Website Connect v0 (plan 2026-10-28 §5.9b; ADR-057 as reviewed 2026-11-02): a share is one port on a person's computer at https://<label>--<user>.openvibe.website behind OpenVibe sign-in, carried by a one-file client (Node 22) over one WebSocket on 443 (ovconnect-ws/1). Space site names never contain \"--\", so share hosts and Space sites never collide; nginx sends *--*.openvibe.website here and every other *.openvibe.website to Space. Modes owner, named and signin (staff-first); no public shares until CON-9 and CON-21. The share API is a person's (session or Network user token): GET/POST /api/v1/shares, GET/PATCH/DELETE /api/v1/shares/:id, POST /api/v1/shares/:id/key, GET /api/v1/shares/:id/visits. The website.*/connect.*/events.hook* capabilities stay planned until the QUIC edge, the Node plugin and the hooks land (CON-3/4/5/15). OAuth client `connect`; answers account export and deletion (ADR-033) and subscribes to those Events topics.",++ "health": "/api/health",++ "ready": "/api/ready",++ "internalOrigin": "http://127.0.0.1:5240",++ "site": {++ "name": "Connect",++ "icon": "connect",++ "tagline": "Your computer, on the web, privately",++ "what": "Share one port on your computer at its own HTTPS address on openvibe.website. Only the people you pick can open it, signed in with OpenVibe. One file and Node: no router settings, no open ports.",++ "legalProfile": "ugc",++ "position": 51,++ "neighbourhood": "make"++ }++}@@@ -1,12 +1,12 @@{"name": "openvibe-contracts",-- "version": "0.268.0",++ "version": "0.279.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-contracts",-- "version": "0.268.0",++ "version": "0.279.0","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.278.0",++ "version": "0.279.0","description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.","main": "index.js","types": "generated/typescript/index.d.ts",@@@ -1,6 +1,7 @@'use strict';// ADR-005 amendment 1: people's presence is ephemeral, in Chat's delivery plane.--// ADR-043 records robot connectivity in Events; it is not a person's presence.++// ADR-043 records robot connectivity in Events; ADR-057 records Connect device connectivity;++// neither is a person's presence.// The machines hub (plan L34) records the same for a person's machines: a machine coming online is the// device changing state, never the person being present. Any other `.online`, `.offline` or presence event// type is still refused.
@@ -1,5 +1,21 @@
@@ -1,5 +1,21 @@
# Changelog
# Changelog
+## 0.279.0 — 2026-11-02
+
+- **Website Connect v0: OpenVibe Connect is live** (CON-1 landed with the OQ-13 review; ADR-057 accepted). The nine
+ CON-1 contracts join the catalog, all additive: `website.share@1`, `website.share-create-request@1`,
+ `website.share-connect-request@1`, `website.policy@1`, `website.audit-entry@1`, `connect.device@1`,
+ `connect.policy@1`, `events.hook@1` and `events.hook-delivery@1`, with valid and invalid fixtures. Id prefixes
+ `shr`, `dev`, `hok`, `pol` and `aud`.
+- **`manifests/services/connect.json`** (live, connect.openvibe.website, port 5240, ov_connect, site position 51 in
+ "make") replaces CON-1's placeholder `website.json`: one id, `connect`, for the repository, ovhost, the database and
+ the OAuth client. It owns `website.*` and `connect.*`; the 20 capabilities stay `planned`, now owned by `connect`,
+ and none is served or granted.
+- **ADR-057's review** decides v0: the WebSocket fallback through Cloudflare inside Connect (no UDP, no DNS-only
+ record), names `<label>--<user>.openvibe.website` (Space names never contain "--"), a share-scoped connect key until
+ the Node plugin, the gate's host-only signed cookie, modes owner/named and staff-first signin, no public shares
+ until CON-9 and CON-21.
+
## 0.278.0 — 2026-11-02
## 0.278.0 — 2026-11-02
- **`live.index_document.upserted` v1 takes type `doc`** (expand only): one of Live's guides, `openvibe.live/docs/<name>`
- **`live.index_document.upserted` v1 takes type `doc`** (expand only): one of Live's guides, `openvibe.live/docs/<name>`
@@ -8734,6 +8734,87 @@
@@ -8734,6 +8734,87 @@
"visibility": "first-party",
"visibility": "first-party",
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
+ },
+ {
+ "id": "website.share",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "website/share.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "website.share-create-request",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "website/share-create-request.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "website.share-connect-request",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "website/share-connect-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "website.policy",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "website/policy.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "website.audit-entry",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "website/audit-entry.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "connect.device",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "connect/device.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "connect.policy",
+ "version": "1.0.0",
+ "owner": "connect",
+ "schema": "connect/policy.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "events.hook",
+ "version": "1.0.0",
+ "owner": "events",
+ "schema": "events/hook.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "events.hook-delivery",
+ "version": "1.0.0",
+ "owner": "events",
+ "schema": "events/hook-delivery.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
}
}
]
]
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,251 @@
@@ -0,0 +1,251 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/connect/device.v1.json",
+ "title": "ConnectDevice",
+ "description": "connect.device@1: one device in a private Connect mesh, as its owner sees it. A device belongs to exactly one tenant (the person's own network or a project's), is identified by a Network node principal, and carries only self-reported posture. The mesh control-plane's internal node id, pre-auth key and WireGuard keys are not part of this document.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "tenant",
+ "name",
+ "status",
+ "os",
+ "version",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who owns the device."
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The project whose mesh the device joins; null for a personal mesh."
+ },
+ "tenant": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A person's own private network."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "project"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A project's private network."
+ }
+ ],
+ "description": "The isolation boundary: one headscale tenant per workspace in v1, never one shared tailnet for all consumer devices."
+ },
+ "node_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The Network node principal the device presents; null for an agent or server that joins through a pre-auth key."
+ },
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
+ "description": "One DNS label in the mesh, shown as name.owner (e.g. pc.alice); never a public hostname."
+ },
+ "status": {
+ "enum": [
+ "pending",
+ "online",
+ "offline",
+ "removed"
+ ],
+ "description": "pending waits for the owner's approval of a pre-auth join; removed is terminal and its keys are revoked."
+ },
+ "os": {
+ "enum": [
+ "linux",
+ "macos",
+ "windows",
+ "ios",
+ "android",
+ "other"
+ ]
+ },
+ "arch": {
+ "enum": [
+ "amd64",
+ "arm64",
+ "386",
+ "arm",
+ "other"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "The Node release running on the device."
+ },
+ "addresses": {
+ "type": "array",
+ "maxItems": 8,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "The device's private mesh addresses; never a public address."
+ },
+ "tags": {
+ "type": "array",
+ "maxItems": 50,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,62}$"
+ }
+ },
+ "posture": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "Self-reported posture, a signal not a guarantee; checked_at says when it was read.",
+ "properties": {
+ "node_current": {
+ "type": "boolean"
+ },
+ "os_patched": {
+ "type": "boolean"
+ },
+ "firewall_on": {
+ "type": "boolean"
+ },
+ "disk_encryption": {
+ "type": "boolean"
+ },
+ "risky_ports": {
+ "type": "array",
+ "maxItems": 200,
+ "uniqueItems": true,
+ "items": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ }
+ },
+ "checked_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "node_current",
+ "os_patched",
+ "firewall_on",
+ "disk_encryption",
+ "risky_ports",
+ "checked_at"
+ ]
+ },
+ "routes": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "Subnet routes the device advertises, e.g. 192.168.1.0/24; empty for none."
+ },
+ "exit_node": {
+ "type": "boolean",
+ "description": "Whether the owner allows the device to act as an exit node for this tenant only; never a public proxy."
+ },
+ "approved_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "approved_by": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who approved the join; null while pending."
+ },
+ "last_seen_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When an unapproved or temporary device stops being valid; null for no expiry."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,213 @@
@@ -0,0 +1,213 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/connect/policy.v1.json",
+ "title": "ConnectPolicy",
+ "description": "connect.policy@1: the grants, routes and exit-node settings of one private Connect tenant. It is deliberately tenant-scoped: a rule never names a device, person or tag from another tenant, and default_action is deny unless the owner opens a path. It describes intent, not the control plane's internal ACL file or keys.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "tenant",
+ "version",
+ "rules",
+ "default_action",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "tenant": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A person's own private network."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "project"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A project's private network."
+ }
+ ],
+ "description": "The one tenant this policy can affect; a rule or route naming another tenant's id is invalid."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Bumped on every change; a device keeps the version it last applied."
+ },
+ "default_action": {
+ "enum": [
+ "allow",
+ "deny"
+ ],
+ "description": "deny unless the owner explicitly opens a path."
+ },
+ "rules": {
+ "type": "array",
+ "maxItems": 200,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "action",
+ "sources",
+ "destinations"
+ ],
+ "properties": {
+ "action": {
+ "enum": [
+ "allow",
+ "deny"
+ ]
+ },
+ "sources": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/selector"
+ }
+ },
+ "destinations": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/selector"
+ }
+ },
+ "ports": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/port"
+ },
+ "description": "TCP/UDP ports or ranges; omit for all ports."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 200
+ }
+ }
+ }
+ },
+ "exit_node": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "device_ids": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ "required": [
+ "enabled",
+ "device_ids"
+ ],
+ "description": "Exit nodes are usable only by this tenant's devices; no public share may target one."
+ },
+ "subnet_routes": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "device_id",
+ "cidr"
+ ],
+ "properties": {
+ "device_id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "cidr": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ }
+ }
+ }
+ },
+ "updated_by": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "selector": {
+ "type": "string",
+ "pattern": "^(?:\\*|(?:user|device|tag|project):[A-Za-z0-9_.:-]+)$",
+ "description": "*, or user:<usr_\u2026>, device:<dev_\u2026>, tag:<name> or project:<prj_\u2026>; all within this tenant."
+ },
+ "port": {
+ "type": "string",
+ "pattern": "^(?:\\*|[0-9]{1,5}(?:-[0-9]{1,5})?)$",
+ "description": "* for any port, 443, or a range such as 8000-8100."
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/hook-delivery.v1.json",
+ "title": "EventsHookDelivery",
+ "description": "events.hook-delivery@1: one attempt by OpenVibe.Events to deliver an event to a hook. It carries the outcome, timing, size and hash of the payload, never the payload itself, its headers, the signature or the hook's secret. A delivery is retried inside its hook's window and dead-lettered after it; replay creates a new attempt that names the one it replays.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "hook_id",
+ "event_id",
+ "event_type",
+ "attempt",
+ "status",
+ "payload_bytes",
+ "payload_sha256",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "hook_id": {
+ "type": "string",
+ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "event_id": {
+ "type": "string",
+ "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "event_type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "attempt": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "status": {
+ "enum": [
+ "queued",
+ "delivering",
+ "delivered",
+ "failed",
+ "dead"
+ ],
+ "description": "failed is a retryable attempt; dead is the last, after the hook's window. A 2xx answer marks delivered."
+ },
+ "request_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 64,
+ "description": "The delivery request id, for tracing; not a secret."
+ },
+ "response_code": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 100,
+ "maximum": 599
+ },
+ "latency_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "payload_bytes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1048576
+ },
+ "payload_sha256": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "description": "The SHA-256 of the signed body, so a consumer can check it without the body being stored here."
+ },
+ "signature_version": {
+ "const": "v2"
+ },
+ "next_attempt_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "attempted_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "completed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "dead_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "error": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 400,
+ "description": "A short transport or status reason; never a response body."
+ },
+ "replay_of": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The failed or dead attempt this one replays."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,216 @@
@@ -0,0 +1,216 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/hook.v1.json",
+ "title": "EventsHook",
+ "description": "events.hook@1: one durable webhook subscription managed through OpenVibe.Website Connect, delivered by OpenVibe.Events. The hook names its share, the public ingress path, the event topics it accepts, filters/transform, the retry window and the signature version. The signing secret is shown once when the hook is created or its secret is rotated; secret_set says one exists, and no field of this document ever carries the value.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "share_id",
+ "name",
+ "endpoint",
+ "topics",
+ "status",
+ "signature",
+ "retry",
+ "secret_set",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "share_id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The share whose tunnel receives deliveries; delivery never goes to an arbitrary host."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "endpoint": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://",
+ "description": "The public ingress, https://hooks.openvibe.website/h/<id>; Connect also records the on-share path https://<share host>/__ov/hook/<id>."
+ },
+ "topics": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Event types this hook accepts, e.g. github.push.received or stripe.invoice.paid; Event's topic patterns may be used by the delivery filter."
+ },
+ "filters": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "path"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "A JSONPath into the event payload."
+ },
+ "equals": {
+ "oneOf": [
+ {
+ "type": "string",
+ "maxLength": 500
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "transform": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "type": "object",
+ "maxProperties": 20,
+ "additionalProperties": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "description": "Headers added to the delivery; never the hook's signing secret."
+ },
+ "body_template": {
+ "type": "string",
+ "maxLength": 10000,
+ "description": "An optional template applied to the event; the raw payload is used when absent."
+ }
+ }
+ },
+ "retry": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "window_seconds",
+ "max_attempts"
+ ],
+ "properties": {
+ "window_seconds": {
+ "type": "integer",
+ "minimum": 60,
+ "maximum": 604800,
+ "description": "How long Events keeps retrying while the share is offline; seven days is the hook default."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
+ }
+ },
+ "signature": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "version",
+ "tolerance_seconds"
+ ],
+ "properties": {
+ "version": {
+ "const": "v2"
+ },
+ "tolerance_seconds": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 3600,
+ "description": "How far a signed timestamp may drift before the Node refuses it; 300 is the default."
+ }
+ }
+ },
+ "max_payload_bytes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1048576,
+ "default": 1048576
+ },
+ "status": {
+ "enum": [
+ "active",
+ "paused",
+ "revoked"
+ ]
+ },
+ "secret_set": {
+ "type": "boolean",
+ "description": "True once a signing secret exists. The secret itself is returned only in the create/rotate answer and is never stored in this contract, a delivery record or an audit entry."
+ },
+ "secret_rotated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "last_delivery_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "dead_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the hook stopped receiving deliveries after its retry window; null while it is live."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "href": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/audit-entry.v1.json",
+ "title": "WebsiteAuditEntry",
+ "description": "website.audit-entry@1: one decision row for a Share, as the owner or a moderator sees it. It answers who reached (or tried to reach) a share, from where, what method and path, what the policy decided and how much crossed the edge. It deliberately carries no request or response body, headers, cookies, bearer or tunnel tokens, query string, password or hook payload: query strings are stripped before this record is made and bodies are captured, if at all, by the separate opt-in inspector with its own redaction.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "share_id",
+ "request_id",
+ "decision",
+ "at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^aud_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "share_id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "request_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "The edge request id, for joining a row to a captured request and a trace; never a secret."
+ },
+ "subject": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "Who the policy identified; absent for an anonymous request."
+ },
+ "ip": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 45,
+ "description": "The observed client address (IPv4 or IPv6). Behind Cloudflare it is CF-Connecting-IP, trusted only from Cloudflare's ranges."
+ },
+ "country": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{2}$",
+ "description": "ISO 3166-1 alpha-2 country from the edge's trusted lookup."
+ },
+ "user_agent": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 512
+ },
+ "method": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3,10}$"
+ },
+ "path": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2048,
+ "description": "Path only, with the query string removed. No body, header, cookie or token is ever stored here."
+ },
+ "status": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 100,
+ "maximum": 599
+ },
+ "decision": {
+ "enum": [
+ "allow",
+ "deny",
+ "challenge",
+ "redirect",
+ "error"
+ ],
+ "description": "The policy outcome: allow, deny, a bot challenge, an OIDC/sign-in redirect, or an edge error."
+ },
+ "reason": {
+ "type": "string",
+ "maxLength": 120,
+ "description": "A stable, short reason such as policy.denied or policy.password_required; never free-form request data."
+ },
+ "policy_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "policy_version": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "bytes_in": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "bytes_out": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "edge_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80
+ },
+ "region": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40
+ },
+ "at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,231 @@
@@ -0,0 +1,231 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/policy.v1.json",
+ "title": "WebsitePolicy",
+ "description": "website.policy@1: how a Share decides who may reach it and under what conditions (public, OpenVibe sign-in, named people or projects, password, one-time link, expiry), plus IP/country rules, rate limits, a bot check, a WAF preset and required device posture. mode is the only field required except that expiry also requires a non-null expires_at. id, owner and version are present when Connect returns a stored policy. Password and one-time policies stay inactive until their credential or link is configured through the CON-5 API. A password is stored as a one-way hash beside this record and is never part of this document; password_set only says one exists.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "mode"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Present on a stored policy; omitted when the policy is written inline with a new share."
+ },
+ "owner": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who owns the policy; set from the token when a policy is created."
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The project scope, when the policy belongs to one."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "mode": {
+ "enum": [
+ "public",
+ "signin",
+ "named",
+ "password",
+ "one_time",
+ "expiry"
+ ],
+ "description": "public: anyone with the link after the abuse screen; signin: any OpenVibe account (optionally a project); named: allowed_subjects/allowed_projects only; password: a shared password; one_time: single-use links; expiry: public access only until expires_at. An expires_at may also cap any other mode."
+ },
+ "allowed_subjects": {
+ "type": "array",
+ "maxItems": 500,
+ "uniqueItems": true,
+ "items": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "description": "Explicit people or agents allowed in named mode; a project's members are allowed through allowed_projects instead."
+ },
+ "allowed_projects": {
+ "type": "array",
+ "maxItems": 100,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "description": "Projects whose members may reach a named or signin share."
+ },
+ "ip_allow": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "CIDR ranges allowed before any other rule; empty means no IP allowlist."
+ },
+ "ip_deny": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "CIDR ranges always refused; evaluated before the allowlist."
+ },
+ "country_allow": {
+ "type": "array",
+ "maxItems": 250,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Z]{2}$"
+ },
+ "description": "ISO 3166-1 alpha-2 country codes allowed; empty means all."
+ },
+ "country_deny": {
+ "type": "array",
+ "maxItems": 250,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Z]{2}$"
+ }
+ },
+ "rate_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_share_per_minute": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000000
+ },
+ "per_ip_per_minute": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100000
+ },
+ "max_connections": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100000
+ },
+ "max_request_bytes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1073741824
+ }
+ }
+ },
+ "bot_check": {
+ "enum": [
+ "none",
+ "turnstile"
+ ],
+ "default": "none"
+ },
+ "waf_preset": {
+ "enum": [
+ "none",
+ "standard",
+ "strict",
+ "api"
+ ],
+ "default": "standard",
+ "description": "Defense in depth at the edge; Cloudflare's managed rules run in front of it for HTTP/WS."
+ },
+ "posture_required": {
+ "type": "array",
+ "maxItems": 10,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "node_current",
+ "os_patched",
+ "firewall_on",
+ "disk_encryption",
+ "no_risky_ports"
+ ]
+ },
+ "description": "Self-reported device posture a share may require; a signal, not a guarantee."
+ },
+ "password_set": {
+ "type": "boolean",
+ "description": "Read-only: true when a password hash exists. The hash itself never leaves Connect."
+ },
+ "password_hint": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 120,
+ "description": "An optional hint shown on the password page; never the password."
+ },
+ "one_time_max_uses": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000,
+ "description": "For one_time mode; 1 unless the owner allows more."
+ },
+ "session_ttl_seconds": {
+ "type": "integer",
+ "minimum": 60,
+ "maximum": 604800,
+ "description": "How long a host-only __Host-ov_session on the share host stays valid after sign-in."
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "The absolute end of the policy; required and non-null for expiry mode, optional for other modes."
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The signed policy version the edge caches and checks on every request; present on a stored policy."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "if": {
+ "properties": {
+ "mode": {
+ "const": "expiry"
+ }
+ },
+ "required": [
+ "mode"
+ ],
+ "type": "object"
+ },
+ "then": {
+ "required": [
+ "expires_at"
+ ],
+ "properties": {
+ "expires_at": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/share-connect-request.v1.json",
+ "title": "WebsiteShareConnectRequest",
+ "description": "website.share-connect-request@1: what a Node presents to open the tunnel for one share (POST /api/v1/shares/:id/tunnel, capability website.share.connect). The Node proves its Network node principal with a client-credentials token whose audience is openvibe.website, and this short-lived, single-use tunnel_token minted by Connect binds the connection to one share. The edge checks both; neither alone opens the route. The token is presented once and never stored, logged or returned.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "share_id",
+ "node_id",
+ "tunnel_token"
+ ],
+ "properties": {
+ "share_id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Must equal the node principal in the Network token; a mismatch is refused and audited."
+ },
+ "tunnel_token": {
+ "type": "string",
+ "minLength": 20,
+ "maxLength": 4096,
+ "description": "A short-lived, single-use, share-scoped token minted by Connect. Send it once over TLS; never put it in a URL, log, audit entry or event."
+ },
+ "transport": {
+ "enum": [
+ "quic",
+ "http2-ws"
+ ],
+ "default": "quic",
+ "description": "The data-plane transport the Node is trying. quic is ALPN ovconnect/1; http2-ws is the 443 fallback when UDP is blocked."
+ },
+ "node_version": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$",
+ "description": "The Node release, for posture and diagnostics; never used for authorization."
+ },
+ "region": {
+ "type": "string",
+ "maxLength": 40,
+ "description": "The edge region the Node is dialing; the edge refuses a region it does not serve."
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/share-create-request.v1.json",
+ "title": "WebsiteShareCreateRequest",
+ "description": "website.share-create-request@1: the body of website.share.create (POST /api/v1/shares). The caller names one DNS label, the protocol, the local target and the policy; the project scope comes from the token, never from this body. Connect returns only website.share@1; CON-5 defines a separate authenticated exchange for the one-time tunnel token.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "protocol",
+ "local_target",
+ "policy"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
+ "description": "The requested single label under openvibe.website. A taken or reserved label is refused; the UI may suggest name-2 rather than changing it silently."
+ },
+ "protocol": {
+ "enum": [
+ "http",
+ "https",
+ "tcp",
+ "udp"
+ ]
+ },
+ "local_target": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "The local port the Node exposes, e.g. 127.0.0.1:3000; never an arbitrary internet host."
+ },
+ "policy": {
+ "allOf": [
+ {
+ "$ref": "policy.v1.json"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "owner": false,
+ "project_id": false,
+ "version": false,
+ "created_at": false,
+ "updated_at": false,
+ "password_set": false
+ }
+ }
+ ],
+ "description": "The policy to enforce. An id binds an existing owner-checked policy; without one, Connect creates it. Server-owned fields are refused in this caller body. Credential and link secrets are configured separately."
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects."
+ },
+ "region": {
+ "type": "string",
+ "maxLength": 40,
+ "description": "Optional preferred edge region; placement may ignore it when capacity or policy requires."
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/share.v1.json",
+ "title": "WebsiteShare",
+ "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "name",
+ "hostname",
+ "protocol",
+ "local_target",
+ "status",
+ "policy_id",
+ "policy_version",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who owns the share. A project share is still owned by a person; project_id says which project it belongs to."
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The project scope when the share belongs to one; null for a personal share. The token's project scope, never a caller-supplied body field, decides access."
+ },
+ "node_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The Network node principal expected to serve the tunnel; null until one is bound."
+ },
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
+ "description": "One DNS label under openvibe.website. The first proof uses the single label agents-<owner> because Universal SSL covers one label only."
+ },
+ "hostname": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 253,
+ "description": "The fully qualified public host, lowercased; the default is name.openvibe.website, and a verified custom domain may replace it."
+ },
+ "protocol": {
+ "enum": [
+ "http",
+ "https",
+ "tcp",
+ "udp"
+ ],
+ "description": "What the share carries. http and https are HTTP/WS/SSE shares; tcp and udp are port shares."
+ },
+ "local_target": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "The local target the Node may forward to, e.g. 127.0.0.1:3000, tcp://127.0.0.1:25565 or udp://127.0.0.1:19132. Loopback by default; a LAN target is allowed only when the owner enabled it locally."
+ },
+ "status": {
+ "enum": [
+ "draft",
+ "review",
+ "active",
+ "paused",
+ "revoked"
+ ],
+ "description": "review holds a public share for the abuse screen; revoked is terminal and closes the route and tunnel."
+ },
+ "policy_id": {
+ "type": "string",
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "policy_version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The signed policy version the edge enforces. A request is checked against this version; revocation bumps it and closes the tunnel."
+ },
+ "region": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "The edge region the share is pinned to, e.g. eu or us-east; null before placement."
+ },
+ "edge_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80,
+ "description": "The edge instance serving the active tunnel; null while the share is offline."
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the share revokes itself; null for no expiry."
+ },
+ "last_seen_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "The last successful Node liveness check; null until the first tunnel."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "href": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,259 @@
@@ -0,0 +1,259 @@
+# ADR-057: Website Connect — a QUIC edge for Share, a private mesh for Connect, Events for hooks
+
+**Status:** Accepted 2026-11-02 with the OQ-13 review below, which decides what v0 ships (proposed 2026-10-29, CON-1; plan 2026-10-28 §5.9b; research
+`research-2026-10-29/website-connect.md` §3.3, §4.3, §4.4 and §4.12). For the owner's review. Builds on ADR-053
+(the Website/Host boundary), ADR-045 (the Node is the universal runtime), ADR-042 (the events fabric), ADR-048 (one
+authority per resource) and ADR-056 (project secrets: write-only, used by name).
+
+## Context and evidence
+
+- **The product is bigger than a site builder.** openvibe.website today is a product/redirect manifest
+ (`manifests/products/openvibe.website.json`, `kind: "moved"`) that opens OpenVibe.Space. Plan 2026-10-28 §5.9b adds
+ the other ways to put something on the web: a local port at a stable HTTPS name (Share), a private mesh of the
+ person's machines (Connect), policies and audit (Protect), custom domains through Domains, and durable webhooks
+ through Events. It is explicitly P1 and its first proof is the owner's agent portal on a phone over 5G.
+- **The reuse study is CON-0.** `research-2026-10-29/website-connect.md` checked the candidates on 2026-10-29:
+ headscale/`tsnet` (BSD-3), NetBird (client BSD-3, management AGPL-3), zrok/OpenZiti (Apache-2.0), frp (Apache-2.0),
+ sish (MIT), Pangolin (mixed AGPL-3/commercial), boringproxy, bore, wstunnel, `quic-go` (MIT) and cloudflared. Its
+ verdict is one path per pillar: build a focused relay on `quic-go` for Share, adopt headscale + `tsnet` with a
+ tenant boundary for Connect, build the policy/abuse layer on top of Cloudflare and Network identity, and extend
+ Events for hooks. §3.3 and §3.4 name what to build and what not to reuse.
+- **The data plane is Go; the control plane is Node.** The Node agent is Go (ADR-045). QUIC, HTTP/3, datagrams,
+ half-close and backpressure are native there and awkward in Node.js. The control API can stay Node/Express, like
+ every other network service. The edge is a separate, exposed, bandwidth-heavy tier and must not share a host with
+ the control plane (research §4.1).
+- **Node already has a principal.** Network owns node principals (`nod_…`, `ids.newId('node')`) and pairs machines;
+ `network.node.revoked` already exists in Network's manifest. Connect must reuse that principal, not mint a second
+ machine identity. Network also owns OIDC, project membership and grants; Domains owns DNS proposals; Events owns
+ durable delivery and v2 signatures (ADR-042); Report and Billing consume usage.
+- **Space already uses `<name>.openvibe.website`.** A single-label name is in use for tenant sites. A new share cannot
+ silently take the same label: the name registry must be shared (or the share namespace reserved) before public
+ launch. Research §4.3 recommends one registry and single-label share names for v1, with a reserved list
+ (`www`, `api`, `app`, `agents`, `hooks`, `edge`, `share`, `status`, `admin`, `mail`, `space`).
+- **Universal SSL covers one label, not two.** The existing wildcard certificate covers
+ `agents-alice.openvibe.website`; it does **not** cover `agents.alice.openvibe.website`. A deep-label design needs
+ a separately provisioned certificate (`*.agents.openvibe.website` via Advanced Certificate Manager/Total TLS, or
+ Cloudflare for SaaS) and a namespace decision. The first proof must use the single label and does not wait for it.
+- **The PSL is a launch gate, not a follow-up.** Until `openvibe.website` is on the Public Suffix List, host-only
+ `__Host-` cookies, no `Domain=.openvibe.website`, and the cross-share cookie test are mandatory. Public shares stay
+ closed until the entry lands (research §4.3, §4.12; plan §5.9b CON-21).
+- **The contracts are the first deliverable.** CON-1 is the ADR plus the additive v1 contracts and the service
+ manifest. No service exists yet: this ADR registers the shape, not a deployment. `contracts/catalog.json` gains
+ `website.share@1`, `website.share-create-request@1`, `website.share-connect-request@1`, `website.policy@1`,
+ `connect.device@1`, `connect.policy@1`, `events.hook@1`, `events.hook-delivery@1` and
+ `website.audit-entry@1`; the service manifest (`manifests/services/connect.json` since the review) lists none of the 20 planned capabilities as served. Nothing is granted.
+
+## Decision
+
+1. **Share is a focused Go edge relay, `openvibe-edge`, plus a Node `connect` core in OpenVibe.Node.** The relay is
+ built on `quic-go` (MIT): one QUIC connection per Node, ALPN `ovconnect/1`, TLS 1.3, streams for HTTP/WebSocket/TCP,
+ datagrams for UDP, and an HTTP/2 + WebSocket fallback on 443 for networks that block UDP. It routes by SNI/Host to a
+ `share_id` from Connect's name registry, never from a user-controlled header, checks the signed policy, and then
+ opens a stream to the Node. It does not own users, shares, policies or audit. No frp, sish, zrok, Pangolin or
+ cloudflared code is forked or embedded.
+2. **The control API is a new Node service, OpenVibe.Connect.** It owns shares, share policies, devices, mesh policies,
+ hooks, the name registry, abuse holds, audit and quota counters in `ov_connect` PostgreSQL; it mints tunnel tokens
+ and signs the short-lived policy cache the edge reads. It is a peer of Domains, Events, Network, Report and Billing,
+ not a module of Bot. Bot keeps robots, the device link and command safety (ADR-043); Connect keeps shares, the mesh,
+ policies and hooks.
+3. **Connect mesh is headscale + embedded `tsnet`, with a tenant boundary that must be proven before consumer
+ multi-tenancy.** v1 maps one workspace to one headscale tenant: a person's own network for a personal account, a
+ project's network for a team. Node embeds `tsnet` (userspace, no root, no TUN) for the private mesh; the public
+ ingress stays Share. headscale does not implement Funnel or Serve and Connect must not promise Funnel-compatible
+ behaviour. The tenant-isolation proof must cover: no cross-tenant MagicDNS resolution, no cross-tenant route or
+ exit-node leak, no shared pre-auth key, and revocation in one tenant leaving the others untouched. If the proof
+ fails, the fallback is a dedicated control plane per paying tenant or NetBird's management (AGPL, heavier), decided
+ before "unlimited devices for everyone" is promised. Mobile clients: the official Tailscale apps may not accept a
+ custom control server on every platform; for v1 the phone reaches services through Share in a browser, and Connect
+ is for PCs, NAS and servers. DERP is self-hosted (`derper`) so relay traffic stays on OpenVibe infrastructure;
+ Live's TURN is for WebRTC and is not a WireGuard relay.
+4. **Identity and the tunnel handshake use two credentials, both checked.** The Node presents a Network
+ client-credentials token whose subject is its node principal (`node:nod_…`) and whose audience is
+ `openvibe.website`; the request body is `website.share-connect-request@1` with the `share_id`, the same `node_id`
+ and a short-lived, single-use, share-scoped `tunnel_token` minted by Connect. The edge validates the token against
+ Network's JWKS, requires the subject to equal `node:<node_id>` (for example `node:nod_…`), redeems the
+ tunnel token with Connect exactly once and binds the connection to that share. One credential is never
+ enough. The tunnel token is sent once over TLS and is
+ never stored, logged, audited or published in an event. OIDC at the share host uses Authorization Code + PKCE
+ against Network; the session is a host-only `__Host-ov_session` (Secure, Path `=`, no `Domain`) carrying
+ `share_id`, `sub` and `policy_version`.
+ `POST /api/v1/shares` returns only `website.share@1`; its response has no tunnel token. The Node obtains the
+ short-lived, single-use token through a separate authenticated credential exchange before connecting. CON-5
+ defines that exchange's endpoint, request and response schemas, authorization, and delivery to the Node.
+5. **Revocation is immediate and has one owner per trigger.** `DELETE /api/v1/shares/:id` writes
+ `website.share.revoked`, closes the edge route and tunnel, removes the DNS route if Connect added one and makes the
+ public host return 410 within the 5-second budget. `network.node.revoked` closes every share bound to that node.
+ A policy change bumps `policy_version`, so the edge's cached signed policy is stale on the next request. Removing a
+ device revokes its mesh key at the control plane. No capability, route or cached policy survives a revoked share.
+6. **Policy is enforced at the edge, authored and audited by Connect, and fronted by Cloudflare.** Cloudflare
+ terminates TLS for HTTP/WS and applies DDoS/WAF/bot rules; the edge performs share lookup, auth, the
+ `website.policy@1` decision (public, signin, named, password, one_time, expiry, IP/CIDR, country, rate limits, bot
+ check, WAF preset, posture), request size/timeouts and audit; the Node restricts the local target to the owner's
+ chosen loopback/LAN port and never proxies arbitrary hosts. Connect owns ownership, policy CRUD, abuse review,
+ quotas and revocation. Passwords are Argon2id hashes stored beside the policy and never in the contract; audit
+ entries never carry bodies, headers, cookies, tokens, query strings or passwords. Public shares require a
+ signed-in creator and pass the abuse screen (Vision UI/look-alike plus Safe Browsing/Web Risk) before activation.
+ Exit nodes never become public proxies, and a public share may not target an exit node.
+ `expiry` means public access only until `expires_at`; `expires_at` can also cap any other mode. Password and
+ one-time policies remain inactive until their credential or link is configured. CON-5 defines the credential/link
+ API and activation rules; neither the password nor the link secret belongs in `website.policy@1`.
+7. **Hooks are owned by Events and only managed by Connect.** Connect creates a per-hook subscription whose endpoint is
+ the hook's own share host (`https://<share>/__ov/hook/<id>`), never an arbitrary URL, and mints a `whsec_…` secret
+ shown once. `hooks.openvibe.website/h/<id>` is a thin ingress: size cap, rate limit, optional provider signature
+ check, raw body stored at rest by reference, and an event published; Events stores the event, signs every attempt
+ with v2, retries for up to seven days while the laptop is offline, dead-letters and replays. The Node verifies the
+ signature and 300-second timestamp tolerance, dedupes on `event_id`, acks 2xx and may buffer locally; a 503 asks
+ Events to retry. Events produces `events.hook.received|delivered|dead`; Connect writes none of them. No smee or
+ Hookdeck is adopted.
+8. **The SDK stays MIT and thin.** `openvibe-sdk` (MIT) gets at most a thin `openvibe-sdk/connect` client that talks
+ to the local Node daemon over HTTP/Unix socket and to the control API over HTTP. The QUIC data plane, the mesh
+ (`tsnet`) and the policy engine stay in the AGPL Node/Connect/edge code. No AGPL/GPL tunnel code is copied into the
+ SDK or `openvibe-shared` (MIT). This keeps the permissive client boundary and the copyleft in the agent.
+9. **Naming and the first proof use one label.** The first proof is the owner's agent portal at
+ `https://agents-<owner>.openvibe.website` (single label), behind OpenVibe sign-in, opened on a phone over 5G with a
+ working microphone (secure context) and WebSocket. Cloudflare Universal SSL does not cover
+ `agents.owner.openvibe.website`; the two-label form, a `*.agents.openvibe.website` certificate or Cloudflare for
+ SaaS is deferred to CON-2. Deep labels do not gate the proof. Public shares remain blocked until the PSL entry, the
+ abuse queue and the DMCA/runbook gates land (CON-21).
+10. **The contracts are additive v1 and the manifest claims nothing yet.** The nine schemas above are public or
+ first-party as noted in the catalog; the 20 capabilities in `manifests/services/website.json` (now `connect.json`, see the review) are `planned`, owned
+ by `website`, and listed in the manifest so consumers see the shape. No token is granted, no service is deployed,
+ and no DNS record changes. The separate product/redirect manifest `manifests/products/openvibe.website.json` is
+ preserved as the domain's product home and still points at Space; `website.json` has `domains: []` and
+ `exposure.state: placeholder`. The service repository is `OpenVibers/OpenVibe.Connect`; the edge may be a second
+ repository (`openvibe-edge`) under CON-3.
+
+## Service boundaries
+
+| Concern | Owner | Not this service |
+|---|---|---|
+| Share record, name registry, policy, tunnel token, inspector metadata, audit | OpenVibe.Connect (Node control API, `ov_connect`) | the edge keeps active session state and a signed policy cache only |
+| Public QUIC/HTTP/WS/SSE/TCP/UDP ingress, routing, edge policy check, rate limits | `openvibe-edge` (Go, `quic-go`) | it does not own users, shares, policies, audit or hooks |
+| Node tunnel, local forwarding, `ov share`, `tsnet` | OpenVibe.Node (`internal/connect`) | it does not decide policy or open arbitrary hosts |
+| Account, node principal, OIDC, grants, revocation event | OpenVibe.Network | Connect never mints identity |
+| DNS proposal, TXT/CNAME, certificate path | OpenVibe.Domains | Connect never writes DNS or holds a provider credential |
+| Durable event store, v2 signature, retries, DLQ, replay | OpenVibe.Events | Connect creates the subscription; the edge does not queue |
+| Usage, abuse analytics, billing | OpenVibe.Report / OpenVibe.Billing | Connect emits usage; it does not price or bill |
+| Firewall helper, local posture read | OpenVibe.Node, local approval | no remote "open this port" API |
+| Thin client for apps | `openvibe-sdk/connect` (MIT) | no tunnel or mesh code in the SDK |
+
+## Rejected reuse paths
+
+| Path | Why not |
+|---|---|
+| frp as the product | Great data plane, wrong abstraction: static proxies and a server-wide token; per-user policy/audit would be a fork. |
+| sish | MIT and simple, but SSH-first UX, no UDP and no per-share policy model. |
+| zrok / OpenZiti | Right feature list, but operating the OpenZiti controller/router/edge is a second platform. |
+| NetBird wholesale | Complete mesh and reverse proxy, but the management stack is AGPL and heavy, Reverse Proxy is beta, and it duplicates Share. |
+| Pangolin | Strong SASE, but mixed AGPL/commercial files and a full product to reskin; not embeddable without a file-by-file audit. |
+| cloudflared | Apache-2.0 client, but the protocol is tied to Cloudflare's edge; it is not a self-hostable relay. |
+| rathole / bore / wstunnel | Good minimal transports with no identity, policy, control API or multi-tenancy. |
+| Tailscale SaaS | Excellent product, but a competitor cannot be built on its control plane; headscale + `tsnet` is the reusable path. |
+| smee / Hookdeck | smee is development-only (unauthenticated channels, no storage); Hookdeck is closed and per-event priced. Events already has the store, signatures, retries, DLQ and replay. |
+
+## Consequences
+
+- **Positive:** the exposed data plane is small and Go-native; the control plane reuses Network identity, Domains
+ proposals, Events delivery and the existing operational stack; the SDK stays MIT; every contract is additive and
+ consumers can build against it before the service exists.
+- **Negative:** a new service and a new edge fleet to run; a headscale/`derper`/PostgreSQL/ValKey footprint; a public
+ ingress that will attract abuse, so quotas, the abuse queue and the PSL gate are launch blockers; deep-label URLs
+ wait for CON-2; the TCP/UDP port pool and DDoS capacity are real cost.
+- **Compatibility:** all nine contracts are new v1 files; no existing contract changes. The manifest is new. The
+ generated TypeScript/bundle and OpenAPI outputs are regenerated; no existing route changes. Consumers pin the next
+ release tag (v0.279.0) when they start.
+- **Rollback:** remove the new catalog rows, schemas, fixtures, capabilities and manifest, and regenerate. No deployed
+ service or grant exists to roll back in CON-1; CON-3/CON-5 must add expand/migrate/contract rules before their first
+ deploy (ADR-028).
+- **Tests:** every schema has a valid and an invalid fixture; the catalog `$id`/path/major checks, manifest and
+ capability validation, capability-schema resolution and OpenAPI generation run in `npm test`. The narrow service
+ check is `npx openvibe-contracts-check --service website`; because no service checkout exists yet it can only prove
+ the manifest resolves and the installed contracts satisfy its range, not that routes enforce the capabilities.
+ CON-3/CON-5 add the code check with `--src`.
+
+## Contracts and capabilities added
+
+| Contract | Visibility | Purpose |
+|---|---|---|
+| `website.share@1` | public | the share record: owner, project, node, label, host, protocol, local target, status, policy version, region/edge, timestamps |
+| `website.share-create-request@1` | public | name, protocol, local target and an inline policy constrained to caller-owned fields; project scope comes from the token |
+| `website.share-connect-request@1` | first-party | `share_id`, `node_id`, single-use `tunnel_token`, transport, node version, region |
+| `website.policy@1` | public | mode and rules; no password hash; version for the signed edge cache |
+| `website.audit-entry@1` | first-party | one decision row with subject/IP/country/UA/method/path/status/bytes and no bodies or secrets |
+| `connect.device@1` | public | one device in one tenant, with self-reported posture and routes |
+| `connect.policy@1` | first-party | one tenant's grants, default deny, exit node and subnet routes |
+| `events.hook@1` | public | a durable hook: share, ingress, topics, filters, retry, v2 signature, `secret_set` (never the secret) |
+| `events.hook-delivery@1` | public | one attempt: status, code, latency, size, hash, next attempt, dead/replay; never the body |
+
+The 20 planned capabilities are the exact §4.4 set: `website.share.create|read|update|delete|connect|inspect|replay`,
+`website.policy.read|write`, `connect.device.read|manage`, `connect.policy.read|write`,
+`events.hook.manage|read`, `website.abuse.report|review`, `website.domain.manage` and the internal
+`website.admin.read|manage`. The §4.4 wildcard `website.admin.*` is expanded into those two exact ids because a
+capability id is concrete; `website.abuse.review` stays the moderator queue. None is active and none is granted.
+
+## Open questions for CON-2 / CON-3
+
+- **CON-2 (namespace, TLS, DNS, PSL):** one shared name registry with Space vs a reserved share namespace; the exact
+ reserved-label list; `*.agents.openvibe.website` via ACM/Total TLS vs Cloudflare for SaaS; the PSL submission and
+ the cross-share cookie test; whether TCP/UDP use `game.openvibe.website` or per-region `edge-<region>` records and
+ which port ranges each region owns. The first proof needs only the single label and the existing wildcard.
+- **CON-3 (edge v0):** the QUIC stream framing and fallback mux; whether the signed policy cache is a JWT or a
+ Connect-signed document and its TTL; the edge's DERP/relay relationship; the exact ready/health contract and the
+ region pinning/reconnect budget; how the edge proves a tunnel token was redeemed only once without a round trip per
+ request.
+- **CON-5 (control API):** the abuse report request/response bodies (deliberately left out of CON-1), the
+ authenticated tunnel credential exchange and password/one-time credential/link setup API, the
+ `share-access`/role model, the name-registry table owner, and the `website.share.accessed|blocked` sampling policy
+ (per request would be too high-volume for Events).
+- **Owner:** the free-tier limits (shares, devices, bandwidth, hooks), whether custom domains are free at launch,
+ and the DMCA agent/takedown runbook owner (CON-21).
+
+## Sources
+
+- `research-2026-10-29/website-connect.md` §1.1–1.7, §3.2–3.4, §4.1–4.13 (checked 2026-10-29).
+- `OpenVibe_New_Domains_Plan_2026-10-28.md` §5.9b and the CON-1…CON-21 ledger.
+- ADR-053 (Website/Host boundary), ADR-045 (Node runtime), ADR-042 (events fabric), ADR-043 (Bot devices), ADR-048
+ (control plane), ADR-056 (project secrets), ADR-028 (expand/migrate/contract).
+- `manifests/products/openvibe.website.json` (the distinct product/redirect manifest, preserved), ADR-005 (realtime),
+ `manifests/services/events.json` and `manifests/services/network.json` (node revocation and hook events).
+
+## Review (OQ-13, 2026-11-02): what v0 ships
+
+Re-derived from the owner's ask (a port on your machine at a stable HTTPS name, opened on a phone over 5G, behind
+OpenVibe sign-in) and from what the network runs on today: one host, every name proxied by Cloudflare.
+
+1. **v0's data plane is the WebSocket fallback, inside OpenVibe.Connect.** A QUIC edge needs UDP to the origin.
+ Cloudflare proxies HTTP and WebSockets, not QUIC to an origin, so the edge would need a DNS-only record that shows the
+ host's address, and Decision 1 also wants it on a host of its own. Neither exists, and neither is needed for the
+ first proof. The relay ships as `server/edge/` in OpenVibe.Connect: the computer opens one WebSocket to
+ `wss://connect.openvibe.website/tunnel` through Cloudflare on 443 (`ovconnect-ws/1`: JSON control messages, binary
+ data frames per stream, per-stream backpressure), and a visitor's request on a share host goes nginx → Connect →
+ that WebSocket → `localhost:<port>`. WebSocket upgrades become a byte pipe to the local port; server-sent events
+ stream. The edge module is separate from the control code so CON-3 can move it to a QUIC edge on its own host
+ without changing the client's view of a share.
+2. **Names: `<label>--<user>.openvibe.website`.** One label, so the existing wildcard certificate covers it (Decision
+ 9). The person's handle is part of the name, so names are per person and nobody can take another's. OpenVibe.Space's
+ site names are `[a-z0-9]` with single hyphens (`server/domain/sites.js` NAME_RE), so no Space site can ever contain
+ "--", and no shared name registry is needed: nginx sends `~^[a-z0-9-]+--[a-z0-9-]+\.openvibe\.website$` to
+ Connect, and Space's server_name leaves those hosts out. `connect` is reserved in Space.
+3. **The computer's credential in v0 is a share-scoped connect key** (`ovc_` + 32 random bytes, stored as a SHA-256
+ hash, shown once, replaced on demand, which disconnects the old computer). Decision 4's two credentials (a node
+ principal token plus a single-use tunnel token) need the OpenVibe Node plugin (CON-4); the standalone client cannot
+ hold a node principal. The key opens one share's tunnel and nothing else.
+4. **The gate is Connect's, and every cookie is host-only.** `/grant` on connect.openvibe.website signs the visitor
+ in with OpenVibe, checks the share's policy, and hands the share host a one-time code (60 s, one use, bound to the
+ share); the share host sets `__Host-ovc`, HMAC-signed and bound to the share and its `policy_version`. A Set-Cookie
+ from the shared app loses its Domain attribute (the PSL entry is not in yet), the app never sees the gate's cookie,
+ and nothing a share sends may be kept by Cloudflare's cache (`Cloudflare-CDN-Cache-Control: no-store`, private
+ Cache-Control): a cached copy would be served past the gate.
+5. **Modes in v0: owner, named, and signin (staff-first).** Public is refused until CON-9 and CON-21, as Decision 6
+ says. Because anyone can make an OpenVibe account, "anyone signed in" is nearly public, so it is open to OpenVibe
+ staff (Network role admin) until the abuse check, as Space's public publishing is.
+6. **The service is `connect`.** One id everywhere: the repository OpenVibe.Connect, ovhost `connect` (port 5240),
+ database `ov_connect`, OAuth client `connect`, `manifests/services/connect.json` (domain connect.openvibe.website,
+ live), owning the `website.*` and `connect.*` namespaces. The 20 capabilities stay `planned` and owned by
+ `connect`; v0's API is a person's own (session or Network user token) and serves none of them. The share API's
+ shapes are Connect's own in v0; `website.share@1` conformance comes with CON-5.
+7. **What waits:** the QUIC edge (CON-3), the Node plugin and node principals (CON-4), custom domains (CON-7), the
+ inspector (CON-8), the abuse gate (CON-9), TCP/UDP shares (CON-11/12), the mesh (CON-13; the headscale tenant proof
+ stands), hooks (CON-15) and the launch gate for public shares (CON-21).
@@ -50,3 +50,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
@@ -50,3 +50,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-054](ADR-054-inventory-authority.md) | The OpenVibe inventory: a new authority, OpenVibe.Inventory (`inventory`, `ov_inventory`), owns item kinds (contract + renderer per surface), definitions (`itd_`), instances (`inv_`), the equipped set and an append-only ledger; issuers grant within their namespace; no selling, buying, trading or conversion until a later money ADR (ADR-012/ADR-025); Live's cosmetics are the first kinds, migrated convert-verify-delete | Accepted 2026-10-09 (plan T21) |
| [ADR-054](ADR-054-inventory-authority.md) | The OpenVibe inventory: a new authority, OpenVibe.Inventory (`inventory`, `ov_inventory`), owns item kinds (contract + renderer per surface), definitions (`itd_`), instances (`inv_`), the equipped set and an append-only ledger; issuers grant within their namespace; no selling, buying, trading or conversion until a later money ADR (ADR-012/ADR-025); Live's cosmetics are the first kinds, migrated convert-verify-delete | Accepted 2026-10-09 (plan T21) |
| [ADR-055](ADR-055-inventory-money.md) | Money and the inventory, in phases: A primary sales (an issuer prices an item in Vibes, booked by Billing as an ENTITLEMENT with a creator share, or in OpenCoins as a loyalty shop with no payout); B gifting between people for giftable items (an ADR-012 rule 6 amendment); C trading and a market stay closed (ADR-025); never paid chance (no cases, crates, loot boxes) | Proposed 2026-10-09 (plan T21), for the owner's decision |
| [ADR-055](ADR-055-inventory-money.md) | Money and the inventory, in phases: A primary sales (an issuer prices an item in Vibes, booked by Billing as an ENTITLEMENT with a creator share, or in OpenCoins as a loyalty shop with no payout); B gifting between people for giftable items (an ADR-012 rule 6 amendment); C trading and a market stay closed (ADR-025); never paid chance (no cases, crates, loot boxes) | Proposed 2026-10-09 (plan T21), for the owner's decision |
| [ADR-056](ADR-056-project-secrets.md) | Project secrets: OpenVibe.Network keeps them per project and environment, write-only (no API returns a value to a person), AES-256-GCM under `NETWORK_SECRETS_KEY`; a service uses one only when the project granted it, to `sign` (HMAC, the value never leaves Network) or to `reveal` it for a listed https origin, through first-party `network.secret.use`; archiving a project shreds its secrets | Accepted 2026-10-15 (plan T13/T17) |
| [ADR-056](ADR-056-project-secrets.md) | Project secrets: OpenVibe.Network keeps them per project and environment, write-only (no API returns a value to a person), AES-256-GCM under `NETWORK_SECRETS_KEY`; a service uses one only when the project granted it, to `sign` (HMAC, the value never leaves Network) or to `reveal` it for a listed https origin, through first-party `network.secret.use`; archiving a project shreds its secrets | Accepted 2026-10-15 (plan T13/T17) |
+| [ADR-057](ADR-057-website-connect.md) | Website Connect: a focused Go `quic-go` edge relay for Share, a Node OpenVibe.Connect control API, headscale + embedded `tsnet` for the private mesh with a tenant-isolation proof, and Events-owned durable hook delivery; two-credential node/share tunnel handshake, immediate revocation, edge policy enforcement, no secret or body in audit, a thin MIT SDK boundary, and the single-label `agents-<owner>.openvibe.website` first proof (Universal SSL does not cover `agents.owner.openvibe.website`; deep labels are CON-2); registers 9 v1 contracts and 20 planned capabilities, grants and deploys nothing | Proposed 2026-10-29 (CON-1; plan 2026-10-28 §5.9b), for the owner's review |
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "name": "workstation",
+ "status": "connected",
+ "os": "linux",
+ "version": "0.42.0",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "tenant": {
+ "type": "user",
+ "id": "team_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "name": "workstation",
+ "status": "online",
+ "os": "linux",
+ "version": "0.42.0",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "-ab",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "ab-",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "a-------------------------------------------------------------z",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "a",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "ab",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+{
+ "id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "workstation",
+ "status": "online",
+ "os": "linux",
+ "arch": "amd64",
+ "version": "0.42.0",
+ "addresses": [
+ "100.64.0.2"
+ ],
+ "tags": [
+ "owner",
+ "linux"
+ ],
+ "posture": {
+ "node_current": true,
+ "os_patched": true,
+ "firewall_on": true,
+ "disk_encryption": true,
+ "risky_ports": [],
+ "checked_at": "2026-10-29T09:00:00.000Z"
+ },
+ "routes": [],
+ "exit_node": false,
+ "approved_at": "2026-10-29T08:05:00.000Z",
+ "approved_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "expires_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "version": 1,
+ "default_action": "deny",
+ "rules": [
+ {
+ "action": "allow",
+ "sources": [
+ "*"
+ ],
+ "destinations": [
+ "*"
+ ],
+ "ports": [
+ "http"
+ ]
+ }
+ ],
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "version": 1,
+ "default_action": "deny",
+ "rules": [
+ {
+ "action": "allow",
+ "sources": [
+ "everyone"
+ ],
+ "destinations": [
+ "*"
+ ]
+ }
+ ],
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "tenant": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "name": "Home mesh",
+ "version": 2,
+ "default_action": "deny",
+ "rules": [
+ {
+ "action": "allow",
+ "sources": [
+ "device:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8"
+ ],
+ "destinations": [
+ "device:dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8"
+ ],
+ "ports": [
+ "443",
+ "8000-8100"
+ ],
+ "description": "The owner's own devices reach each other."
+ }
+ ],
+ "exit_node": {
+ "enabled": false,
+ "device_ids": []
+ },
+ "subnet_routes": [
+ {
+ "device_id": "dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2A8",
+ "cidr": "192.168.1.0/24"
+ }
+ ],
+ "updated_by": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",
+ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",
+ "event_type": "github.push.received",
+ "attempt": 1,
+ "status": "delivered",
+ "payload_bytes": 2048,
+ "payload_sha256": "not-a-sha256",
+ "created_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",
+ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",
+ "event_type": "github.push.received",
+ "attempt": 1,
+ "status": "delivered",
+ "payload_bytes": 2048,
+ "payload_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+ "body": "{\"ref\":\"refs/heads/main\"}",
+ "created_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",
+ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",
+ "event_type": "github.push.received",
+ "attempt": 30,
+ "status": "dead",
+ "response_code": null,
+ "latency_ms": null,
+ "payload_bytes": 2048,
+ "payload_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+ "next_attempt_at": null,
+ "attempted_at": "2026-11-05T09:00:00.000Z",
+ "completed_at": "2026-11-05T09:00:01.000Z",
+ "dead_at": "2026-11-05T09:00:01.000Z",
+ "error": "share offline",
+ "replay_of": null,
+ "created_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "whd_01J8Z3Q4R5S6T7V8W9X0Y1Z2B1",
+ "hook_id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "event_id": "evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2B0",
+ "event_type": "github.push.received",
+ "attempt": 1,
+ "status": "delivered",
+ "request_id": "req_01J8Z3Q4R5S6T7V8W9",
+ "response_code": 202,
+ "latency_ms": 84,
+ "payload_bytes": 2048,
+ "payload_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+ "signature_version": "v2",
+ "next_attempt_at": null,
+ "attempted_at": "2026-10-29T09:00:00.000Z",
+ "completed_at": "2026-10-29T09:00:00.084Z",
+ "dead_at": null,
+ "error": null,
+ "replay_of": null,
+ "created_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "name": "Insecure hook",
+ "endpoint": "http://hooks.openvibe.website/h/hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "topics": [
+ "github.push.received"
+ ],
+ "retry": {
+ "window_seconds": 604800,
+ "max_attempts": 30
+ },
+ "signature": {
+ "version": "v2",
+ "tolerance_seconds": 300
+ },
+ "status": "active",
+ "secret_set": true,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+{
+ "id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "name": "GitHub pushes",
+ "endpoint": "https://hooks.openvibe.website/h/hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "topics": [
+ "github.push.received"
+ ],
+ "retry": {
+ "window_seconds": 604800,
+ "max_attempts": 30
+ },
+ "signature": {
+ "version": "v2",
+ "tolerance_seconds": 300
+ },
+ "status": "active",
+ "secret_set": true,
+ "secret": "whsec_this-value-never-belongs-in-a-contract",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+{
+ "id": "hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "name": "GitHub pushes",
+ "endpoint": "https://hooks.openvibe.website/h/hok_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9",
+ "topics": [
+ "github.push.received",
+ "github.pull_request.received"
+ ],
+ "filters": [
+ {
+ "path": "$.repository.full_name",
+ "equals": "OpenVibers/OpenVibe.Contracts"
+ }
+ ],
+ "transform": null,
+ "retry": {
+ "window_seconds": 604800,
+ "max_attempts": 30
+ },
+ "signature": {
+ "version": "v2",
+ "tolerance_seconds": 300
+ },
+ "max_payload_bytes": 1048576,
+ "status": "active",
+ "secret_set": true,
+ "secret_rotated_at": null,
+ "last_delivery_at": "2026-10-29T09:00:00.000Z",
+ "dead_at": null,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "id": "aud_01J8Z3Q4R5S6T7V8W9X0Y1Z2B2",
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "request_id": "req_01J8Z3Q4R5S6T7V8W9",
+ "decision": "allowed",
+ "at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "id": "aud_01J8Z3Q4R5S6T7V8W9X0Y1Z2B2",
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "request_id": "req_01J8Z3Q4R5S6T7V8W9",
+ "decision": "allow",
+ "at": "2026-10-29T09:00:00.000Z",
+ "body": "{\"password\":\"hunter2\"}"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "aud_01J8Z3Q4R5S6T7V8W9X0Y1Z2B2",
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "request_id": "req_01J8Z3Q4R5S6T7V8W9",
+ "subject": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "ip": "203.0.113.7",
+ "country": "DE",
+ "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)",
+ "method": "GET",
+ "path": "/",
+ "status": 403,
+ "decision": "deny",
+ "reason": "policy.named_only",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "bytes_in": 412,
+ "bytes_out": 0,
+ "edge_id": "edge-eu-1",
+ "region": "eu",
+ "at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "mode": "invite_only"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "name": "Launch demo",
+ "mode": "expiry",
+ "allowed_subjects": [],
+ "allowed_projects": [],
+ "ip_allow": [],
+ "ip_deny": [],
+ "country_allow": [
+ "DE",
+ "FR"
+ ],
+ "country_deny": [],
+ "rate_limits": {
+ "per_share_per_minute": 600,
+ "per_ip_per_minute": 60,
+ "max_connections": 100,
+ "max_request_bytes": 10485760
+ },
+ "bot_check": "turnstile",
+ "waf_preset": "standard",
+ "posture_required": [],
+ "password_set": false,
+ "session_ttl_seconds": 3600,
+ "version": 3,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "name": "Launch demo",
+ "mode": "expiry",
+ "allowed_subjects": [],
+ "allowed_projects": [],
+ "ip_allow": [],
+ "ip_deny": [],
+ "country_allow": [
+ "DE",
+ "FR"
+ ],
+ "country_deny": [],
+ "rate_limits": {
+ "per_share_per_minute": 600,
+ "per_ip_per_minute": 60,
+ "max_connections": 100,
+ "max_request_bytes": 10485760
+ },
+ "bot_check": "turnstile",
+ "waf_preset": "standard",
+ "posture_required": [],
+ "password_set": false,
+ "session_ttl_seconds": 3600,
+ "expires_at": null,
+ "version": 3,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "mode": "password",
+ "password_hash": "$argon2id$v=19$m=65536,t=3,p=4$example$example"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "name": "Launch demo",
+ "mode": "expiry",
+ "allowed_subjects": [],
+ "allowed_projects": [],
+ "ip_allow": [],
+ "ip_deny": [],
+ "country_allow": [
+ "DE",
+ "FR"
+ ],
+ "country_deny": [],
+ "rate_limits": {
+ "per_share_per_minute": 600,
+ "per_ip_per_minute": 60,
+ "max_connections": 100,
+ "max_request_bytes": 10485760
+ },
+ "bot_check": "turnstile",
+ "waf_preset": "standard",
+ "posture_required": [],
+ "password_set": false,
+ "session_ttl_seconds": 3600,
+ "expires_at": "2026-11-01T00:00:00.000Z",
+ "version": 3,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+{
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "name": "Password for the demo",
+ "mode": "password",
+ "allowed_subjects": [],
+ "allowed_projects": [],
+ "ip_allow": [],
+ "ip_deny": [],
+ "country_allow": [
+ "DE",
+ "FR"
+ ],
+ "country_deny": [],
+ "rate_limits": {
+ "per_share_per_minute": 600,
+ "per_ip_per_minute": 60,
+ "max_connections": 100,
+ "max_request_bytes": 10485760
+ },
+ "bot_check": "turnstile",
+ "waf_preset": "standard",
+ "posture_required": [],
+ "password_set": true,
+ "password_hint": "the one from the call",
+ "one_time_max_uses": 1,
+ "session_ttl_seconds": 3600,
+ "expires_at": null,
+ "version": 3,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "tunnel_token": "ovt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9_one_time",
+ "transport": "websocket"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "share_id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "tunnel_token": "ovt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A9_one_time",
+ "transport": "quic",
+ "node_version": "0.42.0",
+ "region": "eu"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "-ab",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ },
+ "created_at": "2026-10-29T08:00:00.000Z"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ },
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ },
+ "password_set": false
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ },
+ "project_id": null
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ },
+ "updated_at": "2026-10-29T08:00:00.000Z"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ },
+ "version": 1
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "policy": {
+ "mode": "signin"
+ },
+ "project_id": "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "ab-",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "name": "agents-alice",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "mode": "signin"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "a-------------------------------------------------------------z",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "a",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "name": "ab",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "policy": {
+ "mode": "signin",
+ "allowed_projects": [
+ "prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A7"
+ ],
+ "rate_limits": {
+ "per_ip_per_minute": 120
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "name": "agents-alice",
+ "hostname": "agents-alice.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "deleted",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "-ab",
+ "hostname": "-ab.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "hostname": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "ab-",
+ "hostname": "ab-.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "name": "Agents-Alice",
+ "hostname": "agents-alice.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 1,
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "agents-alice",
+ "hostname": "agents-alice.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "a-------------------------------------------------------------z",
+ "hostname": "a-------------------------------------------------------------z.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "a",
+ "hostname": "a.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
+ "owner": {
+ "type": "user",
+ "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A6"
+ },
+ "project_id": null,
+ "node_id": "nod_01J8Z3Q4R5S6T7V8W9X0Y1Z2A5",
+ "name": "ab",
+ "hostname": "ab.openvibe.website",
+ "protocol": "http",
+ "local_target": "127.0.0.1:3000",
+ "status": "active",
+ "policy_id": "pol_01J8Z3Q4R5S6T7V8W9X0Y1Z2A4",
+ "policy_version": 3,
+ "region": "eu",
+ "edge_id": "edge-eu-1",
+ "expires_at": null,
+ "last_seen_at": "2026-10-29T09:00:00.000Z",
+ "created_at": "2026-10-29T08:00:00.000Z",
+ "updated_at": "2026-10-29T09:00:00.000Z",
+ "revoked_at": null,
+ "href": "https://connect.openvibe.website/shares/shr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
+}
@@ -97221,6 +97221,1504 @@
@@ -97221,6 +97221,1504 @@
}
}
}
}
}
}
+ },
+ "website.share": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/share.v1.json",
+ "title": "WebsiteShare",
+ "description": "website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "name",
+ "hostname",
+ "protocol",
+ "local_target",
+ "status",
+ "policy_id",
+ "policy_version",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who owns the share. A project share is still owned by a person; project_id says which project it belongs to."
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The project scope when the share belongs to one; null for a personal share. The token's project scope, never a caller-supplied body field, decides access."
+ },
+ "node_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The Network node principal expected to serve the tunnel; null until one is bound."
+ },
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
+ "description": "One DNS label under openvibe.website. The first proof uses the single label agents-<owner> because Universal SSL covers one label only."
+ },
+ "hostname": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 253,
+ "description": "The fully qualified public host, lowercased; the default is name.openvibe.website, and a verified custom domain may replace it."
+ },
+ "protocol": {
+ "enum": [
+ "http",
+ "https",
+ "tcp",
+ "udp"
+ ],
+ "description": "What the share carries. http and https are HTTP/WS/SSE shares; tcp and udp are port shares."
+ },
+ "local_target": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "The local target the Node may forward to, e.g. 127.0.0.1:3000, tcp://127.0.0.1:25565 or udp://127.0.0.1:19132. Loopback by default; a LAN target is allowed only when the owner enabled it locally."
+ },
+ "status": {
+ "enum": [
+ "draft",
+ "review",
+ "active",
+ "paused",
+ "revoked"
+ ],
+ "description": "review holds a public share for the abuse screen; revoked is terminal and closes the route and tunnel."
+ },
+ "policy_id": {
+ "type": "string",
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "policy_version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The signed policy version the edge enforces. A request is checked against this version; revocation bumps it and closes the tunnel."
+ },
+ "region": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "The edge region the share is pinned to, e.g. eu or us-east; null before placement."
+ },
+ "edge_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80,
+ "description": "The edge instance serving the active tunnel; null while the share is offline."
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the share revokes itself; null for no expiry."
+ },
+ "last_seen_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "The last successful Node liveness check; null until the first tunnel."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "href": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ }
+ }
+ },
+ "website.share-create-request": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/share-create-request.v1.json",
+ "title": "WebsiteShareCreateRequest",
+ "description": "website.share-create-request@1: the body of website.share.create (POST /api/v1/shares). The caller names one DNS label, the protocol, the local target and the policy; the project scope comes from the token, never from this body. Connect returns only website.share@1; CON-5 defines a separate authenticated exchange for the one-time tunnel token.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "protocol",
+ "local_target",
+ "policy"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
+ "description": "The requested single label under openvibe.website. A taken or reserved label is refused; the UI may suggest name-2 rather than changing it silently."
+ },
+ "protocol": {
+ "enum": [
+ "http",
+ "https",
+ "tcp",
+ "udp"
+ ]
+ },
+ "local_target": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "The local port the Node exposes, e.g. 127.0.0.1:3000; never an arbitrary internet host."
+ },
+ "policy": {
+ "allOf": [
+ {
+ "$ref": "policy.v1.json"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "owner": false,
+ "project_id": false,
+ "version": false,
+ "created_at": false,
+ "updated_at": false,
+ "password_set": false
+ }
+ }
+ ],
+ "description": "The policy to enforce. An id binds an existing owner-checked policy; without one, Connect creates it. Server-owned fields are refused in this caller body. Credential and link secrets are configured separately."
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects."
+ },
+ "region": {
+ "type": "string",
+ "maxLength": 40,
+ "description": "Optional preferred edge region; placement may ignore it when capacity or policy requires."
+ }
+ }
+ }
+ },
+ "website.share-connect-request": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/share-connect-request.v1.json",
+ "title": "WebsiteShareConnectRequest",
+ "description": "website.share-connect-request@1: what a Node presents to open the tunnel for one share (POST /api/v1/shares/:id/tunnel, capability website.share.connect). The Node proves its Network node principal with a client-credentials token whose audience is openvibe.website, and this short-lived, single-use tunnel_token minted by Connect binds the connection to one share. The edge checks both; neither alone opens the route. The token is presented once and never stored, logged or returned.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "share_id",
+ "node_id",
+ "tunnel_token"
+ ],
+ "properties": {
+ "share_id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Must equal the node principal in the Network token; a mismatch is refused and audited."
+ },
+ "tunnel_token": {
+ "type": "string",
+ "minLength": 20,
+ "maxLength": 4096,
+ "description": "A short-lived, single-use, share-scoped token minted by Connect. Send it once over TLS; never put it in a URL, log, audit entry or event."
+ },
+ "transport": {
+ "enum": [
+ "quic",
+ "http2-ws"
+ ],
+ "default": "quic",
+ "description": "The data-plane transport the Node is trying. quic is ALPN ovconnect/1; http2-ws is the 443 fallback when UDP is blocked."
+ },
+ "node_version": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$",
+ "description": "The Node release, for posture and diagnostics; never used for authorization."
+ },
+ "region": {
+ "type": "string",
+ "maxLength": 40,
+ "description": "The edge region the Node is dialing; the edge refuses a region it does not serve."
+ }
+ }
+ }
+ },
+ "website.policy": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/policy.v1.json",
+ "title": "WebsitePolicy",
+ "description": "website.policy@1: how a Share decides who may reach it and under what conditions (public, OpenVibe sign-in, named people or projects, password, one-time link, expiry), plus IP/country rules, rate limits, a bot check, a WAF preset and required device posture. mode is the only field required except that expiry also requires a non-null expires_at. id, owner and version are present when Connect returns a stored policy. Password and one-time policies stay inactive until their credential or link is configured through the CON-5 API. A password is stored as a one-way hash beside this record and is never part of this document; password_set only says one exists.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "mode"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Present on a stored policy; omitted when the policy is written inline with a new share."
+ },
+ "owner": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who owns the policy; set from the token when a policy is created."
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The project scope, when the policy belongs to one."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "mode": {
+ "enum": [
+ "public",
+ "signin",
+ "named",
+ "password",
+ "one_time",
+ "expiry"
+ ],
+ "description": "public: anyone with the link after the abuse screen; signin: any OpenVibe account (optionally a project); named: allowed_subjects/allowed_projects only; password: a shared password; one_time: single-use links; expiry: public access only until expires_at. An expires_at may also cap any other mode."
+ },
+ "allowed_subjects": {
+ "type": "array",
+ "maxItems": 500,
+ "uniqueItems": true,
+ "items": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "description": "Explicit people or agents allowed in named mode; a project's members are allowed through allowed_projects instead."
+ },
+ "allowed_projects": {
+ "type": "array",
+ "maxItems": 100,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "description": "Projects whose members may reach a named or signin share."
+ },
+ "ip_allow": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "CIDR ranges allowed before any other rule; empty means no IP allowlist."
+ },
+ "ip_deny": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "CIDR ranges always refused; evaluated before the allowlist."
+ },
+ "country_allow": {
+ "type": "array",
+ "maxItems": 250,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Z]{2}$"
+ },
+ "description": "ISO 3166-1 alpha-2 country codes allowed; empty means all."
+ },
+ "country_deny": {
+ "type": "array",
+ "maxItems": 250,
+ "items": {
+ "type": "string",
+ "pattern": "^[A-Z]{2}$"
+ }
+ },
+ "rate_limits": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_share_per_minute": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000000
+ },
+ "per_ip_per_minute": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100000
+ },
+ "max_connections": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100000
+ },
+ "max_request_bytes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1073741824
+ }
+ }
+ },
+ "bot_check": {
+ "enum": [
+ "none",
+ "turnstile"
+ ],
+ "default": "none"
+ },
+ "waf_preset": {
+ "enum": [
+ "none",
+ "standard",
+ "strict",
+ "api"
+ ],
+ "default": "standard",
+ "description": "Defense in depth at the edge; Cloudflare's managed rules run in front of it for HTTP/WS."
+ },
+ "posture_required": {
+ "type": "array",
+ "maxItems": 10,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "node_current",
+ "os_patched",
+ "firewall_on",
+ "disk_encryption",
+ "no_risky_ports"
+ ]
+ },
+ "description": "Self-reported device posture a share may require; a signal, not a guarantee."
+ },
+ "password_set": {
+ "type": "boolean",
+ "description": "Read-only: true when a password hash exists. The hash itself never leaves Connect."
+ },
+ "password_hint": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 120,
+ "description": "An optional hint shown on the password page; never the password."
+ },
+ "one_time_max_uses": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1000,
+ "description": "For one_time mode; 1 unless the owner allows more."
+ },
+ "session_ttl_seconds": {
+ "type": "integer",
+ "minimum": 60,
+ "maximum": 604800,
+ "description": "How long a host-only __Host-ov_session on the share host stays valid after sign-in."
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "The absolute end of the policy; required and non-null for expiry mode, optional for other modes."
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The signed policy version the edge caches and checks on every request; present on a stored policy."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "if": {
+ "properties": {
+ "mode": {
+ "const": "expiry"
+ }
+ },
+ "required": [
+ "mode"
+ ],
+ "type": "object"
+ },
+ "then": {
+ "required": [
+ "expires_at"
+ ],
+ "properties": {
+ "expires_at": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ }
+ },
+ "website.audit-entry": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/website/audit-entry.v1.json",
+ "title": "WebsiteAuditEntry",
+ "description": "website.audit-entry@1: one decision row for a Share, as the owner or a moderator sees it. It answers who reached (or tried to reach) a share, from where, what method and path, what the policy decided and how much crossed the edge. It deliberately carries no request or response body, headers, cookies, bearer or tunnel tokens, query string, password or hook payload: query strings are stripped before this record is made and bodies are captured, if at all, by the separate opt-in inspector with its own redaction.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "share_id",
+ "request_id",
+ "decision",
+ "at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^aud_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "share_id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "request_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "The edge request id, for joining a row to a captured request and a trace; never a secret."
+ },
+ "subject": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "Who the policy identified; absent for an anonymous request."
+ },
+ "ip": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 45,
+ "description": "The observed client address (IPv4 or IPv6). Behind Cloudflare it is CF-Connecting-IP, trusted only from Cloudflare's ranges."
+ },
+ "country": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{2}$",
+ "description": "ISO 3166-1 alpha-2 country from the edge's trusted lookup."
+ },
+ "user_agent": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 512
+ },
+ "method": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3,10}$"
+ },
+ "path": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2048,
+ "description": "Path only, with the query string removed. No body, header, cookie or token is ever stored here."
+ },
+ "status": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 100,
+ "maximum": 599
+ },
+ "decision": {
+ "enum": [
+ "allow",
+ "deny",
+ "challenge",
+ "redirect",
+ "error"
+ ],
+ "description": "The policy outcome: allow, deny, a bot challenge, an OIDC/sign-in redirect, or an edge error."
+ },
+ "reason": {
+ "type": "string",
+ "maxLength": 120,
+ "description": "A stable, short reason such as policy.denied or policy.password_required; never free-form request data."
+ },
+ "policy_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "policy_version": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "bytes_in": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "bytes_out": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "edge_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80
+ },
+ "region": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40
+ },
+ "at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "connect.device": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/connect/device.v1.json",
+ "title": "ConnectDevice",
+ "description": "connect.device@1: one device in a private Connect mesh, as its owner sees it. A device belongs to exactly one tenant (the person's own network or a project's), is identified by a Network node principal, and carries only self-reported posture. The mesh control-plane's internal node id, pre-auth key and WireGuard keys are not part of this document.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "tenant",
+ "name",
+ "status",
+ "os",
+ "version",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who owns the device."
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The project whose mesh the device joins; null for a personal mesh."
+ },
+ "tenant": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A person's own private network."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "project"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A project's private network."
+ }
+ ],
+ "description": "The isolation boundary: one headscale tenant per workspace in v1, never one shared tailnet for all consumer devices."
+ },
+ "node_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^nod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The Network node principal the device presents; null for an agent or server that joins through a pre-auth key."
+ },
+ "name": {
+ "type": "string",
+ "pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
+ "description": "One DNS label in the mesh, shown as name.owner (e.g. pc.alice); never a public hostname."
+ },
+ "status": {
+ "enum": [
+ "pending",
+ "online",
+ "offline",
+ "removed"
+ ],
+ "description": "pending waits for the owner's approval of a pre-auth join; removed is terminal and its keys are revoked."
+ },
+ "os": {
+ "enum": [
+ "linux",
+ "macos",
+ "windows",
+ "ios",
+ "android",
+ "other"
+ ]
+ },
+ "arch": {
+ "enum": [
+ "amd64",
+ "arm64",
+ "386",
+ "arm",
+ "other"
+ ]
+ },
+ "version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "The Node release running on the device."
+ },
+ "addresses": {
+ "type": "array",
+ "maxItems": 8,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "The device's private mesh addresses; never a public address."
+ },
+ "tags": {
+ "type": "array",
+ "maxItems": 50,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{0,62}$"
+ }
+ },
+ "posture": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "Self-reported posture, a signal not a guarantee; checked_at says when it was read.",
+ "properties": {
+ "node_current": {
+ "type": "boolean"
+ },
+ "os_patched": {
+ "type": "boolean"
+ },
+ "firewall_on": {
+ "type": "boolean"
+ },
+ "disk_encryption": {
+ "type": "boolean"
+ },
+ "risky_ports": {
+ "type": "array",
+ "maxItems": 200,
+ "uniqueItems": true,
+ "items": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ }
+ },
+ "checked_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "node_current",
+ "os_patched",
+ "firewall_on",
+ "disk_encryption",
+ "risky_ports",
+ "checked_at"
+ ]
+ },
+ "routes": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ },
+ "description": "Subnet routes the device advertises, e.g. 192.168.1.0/24; empty for none."
+ },
+ "exit_node": {
+ "type": "boolean",
+ "description": "Whether the owner allows the device to act as an exit node for this tenant only; never a public proxy."
+ },
+ "approved_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "approved_by": {
+ "allOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ }
+ ],
+ "description": "The person who approved the join; null while pending."
+ },
+ "last_seen_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When an unapproved or temporary device stops being valid; null for no expiry."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "connect.policy": {
+ "version": "1.0.0",
+ "owner": "connect",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/connect/policy.v1.json",
+ "title": "ConnectPolicy",
+ "description": "connect.policy@1: the grants, routes and exit-node settings of one private Connect tenant. It is deliberately tenant-scoped: a rule never names a device, person or tag from another tenant, and default_action is deny unless the owner opens a path. It describes intent, not the control plane's internal ACL file or keys.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "tenant",
+ "version",
+ "rules",
+ "default_action",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^pol_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "tenant": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A person's own private network."
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "type",
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "project"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "A project's private network."
+ }
+ ],
+ "description": "The one tenant this policy can affect; a rule or route naming another tenant's id is invalid."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Bumped on every change; a device keeps the version it last applied."
+ },
+ "default_action": {
+ "enum": [
+ "allow",
+ "deny"
+ ],
+ "description": "deny unless the owner explicitly opens a path."
+ },
+ "rules": {
+ "type": "array",
+ "maxItems": 200,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "action",
+ "sources",
+ "destinations"
+ ],
+ "properties": {
+ "action": {
+ "enum": [
+ "allow",
+ "deny"
+ ]
+ },
+ "sources": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/selector"
+ }
+ },
+ "destinations": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/selector"
+ }
+ },
+ "ports": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/port"
+ },
+ "description": "TCP/UDP ports or ranges; omit for all ports."
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 200
+ }
+ }
+ }
+ },
+ "exit_node": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "device_ids": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ "required": [
+ "enabled",
+ "device_ids"
+ ],
+ "description": "Exit nodes are usable only by this tenant's devices; no public share may target one."
+ },
+ "subnet_routes": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "device_id",
+ "cidr"
+ ],
+ "properties": {
+ "device_id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "cidr": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 64
+ }
+ }
+ }
+ },
+ "updated_by": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "$defs": {
+ "selector": {
+ "type": "string",
+ "pattern": "^(?:\\*|(?:user|device|tag|project):[A-Za-z0-9_.:-]+)$",
+ "description": "*, or user:<usr_…>, device:<dev_…>, tag:<name> or project:<prj_…>; all within this tenant."
+ },
+ "port": {
+ "type": "string",
+ "pattern": "^(?:\\*|[0-9]{1,5}(?:-[0-9]{1,5})?)$",
+ "description": "* for any port, 443, or a range such as 8000-8100."
+ }
+ }
+ }
+ },
+ "events.hook": {
+ "version": "1.0.0",
+ "owner": "events",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/hook.v1.json",
+ "title": "EventsHook",
+ "description": "events.hook@1: one durable webhook subscription managed through OpenVibe.Website Connect, delivered by OpenVibe.Events. The hook names its share, the public ingress path, the event topics it accepts, filters/transform, the retry window and the signature version. The signing secret is shown once when the hook is created or its secret is rotated; secret_set says one exists, and no field of this document ever carries the value.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "owner",
+ "share_id",
+ "name",
+ "endpoint",
+ "topics",
+ "status",
+ "signature",
+ "retry",
+ "secret_set",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "owner": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "share_id": {
+ "type": "string",
+ "pattern": "^shr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The share whose tunnel receives deliveries; delivery never goes to an arbitrary host."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "endpoint": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://",
+ "description": "The public ingress, https://hooks.openvibe.website/h/<id>; Connect also records the on-share path https://<share host>/__ov/hook/<id>."
+ },
+ "topics": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 50,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Event types this hook accepts, e.g. github.push.received or stripe.invoice.paid; Event's topic patterns may be used by the delivery filter."
+ },
+ "filters": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "path"
+ ],
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "A JSONPath into the event payload."
+ },
+ "equals": {
+ "oneOf": [
+ {
+ "type": "string",
+ "maxLength": 500
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "transform": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "type": "object",
+ "maxProperties": 20,
+ "additionalProperties": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "description": "Headers added to the delivery; never the hook's signing secret."
+ },
+ "body_template": {
+ "type": "string",
+ "maxLength": 10000,
+ "description": "An optional template applied to the event; the raw payload is used when absent."
+ }
+ }
+ },
+ "retry": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "window_seconds",
+ "max_attempts"
+ ],
+ "properties": {
+ "window_seconds": {
+ "type": "integer",
+ "minimum": 60,
+ "maximum": 604800,
+ "description": "How long Events keeps retrying while the share is offline; seven days is the hook default."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
+ }
+ },
+ "signature": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "version",
+ "tolerance_seconds"
+ ],
+ "properties": {
+ "version": {
+ "const": "v2"
+ },
+ "tolerance_seconds": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 3600,
+ "description": "How far a signed timestamp may drift before the Node refuses it; 300 is the default."
+ }
+ }
+ },
+ "max_payload_bytes": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 1048576,
+ "default": 1048576
+ },
+ "status": {
+ "enum": [
+ "active",
+ "paused",
+ "revoked"
+ ]
+ },
+ "secret_set": {
+ "type": "boolean",
+ "description": "True once a signing secret exists. The secret itself is returned only in the create/rotate answer and is never stored in this contract, a delivery record or an audit entry."
+ },
+ "secret_rotated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "last_delivery_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "dead_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the hook stopped receiving deliveries after its retry window; null while it is live."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "href": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://"
+ }
+ }
+ }
+ },
+ "events.hook-delivery": {
+ "version": "1.0.0",
+ "owner": "events",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/hook-delivery.v1.json",
+ "title": "EventsHookDelivery",
+ "description": "events.hook-delivery@1: one attempt by OpenVibe.Events to deliver an event to a hook. It carries the outcome, timing, size and hash of the payload, never the payload itself, its headers, the signature or the hook's secret. A delivery is retried inside its hook's window and dead-lettered after it; replay creates a new attempt that names the one it replays.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "hook_id",
+ "event_id",
+ "event_type",
+ "attempt",
+ "status",
+ "payload_bytes",
+ "payload_sha256",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "hook_id": {
+ "type": "string",
+ "pattern": "^hok_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "event_id": {
+ "type": "string",
+ "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "event_type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "attempt": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "status": {
+ "enum": [
+ "queued",
+ "delivering",
+ "delivered",
+ "failed",
+ "dead"
+ ],
+ "description": "failed is a retryable attempt; dead is the last, after the hook's window. A 2xx answer marks delivered."
+ },
+ "request_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 64,
+ "description": "The delivery request id, for tracing; not a secret."
+ },
+ "response_code": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 100,
+ "maximum": 599
+ },
+ "latency_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "payload_bytes": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1048576
+ },
+ "payload_sha256": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "description": "The SHA-256 of the signed body, so a consumer can check it without the body being stored here."
+ },
+ "signature_version": {
+ "const": "v2"
+ },
+ "next_attempt_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "attempted_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "completed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "dead_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "error": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 400,
+ "description": "A short transport or status reason; never a response body."
+ },
+ "replay_of": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^whd_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The failed or dead attempt this one replays."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
}
}
}
}
}
}
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Actor API",
"title": "OpenVibe.Actor API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bench API",
"title": "OpenVibe.Bench API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bot API",
"title": "OpenVibe.Bot API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Domains API",
"title": "OpenVibe.Domains API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Food API",
"title": "OpenVibe.Food API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Inventory API",
"title": "OpenVibe.Inventory API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.MediaHub API",
"title": "OpenVibe.MediaHub API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRestream API",
"title": "OpenRestream API",
- "version": "0.278.0",
- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Play API",
"title": "OpenVibe.Play API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Quest API",
"title": "OpenVibe.Quest API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Report API",
"title": "OpenVibe.Report API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Run API",
"title": "OpenVibe.Run API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Services API",
"title": "OpenVibe.Services API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Space API",
"title": "OpenVibe.Space API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Today API",
"title": "OpenVibe.Today API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Vision API",
"title": "OpenVibe.Vision API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Watch API",
"title": "OpenVibe.Watch API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Work API",
"title": "OpenVibe.Work API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Zone API",
"title": "OpenVibe.Zone API",
- "version": "0.278.0",
- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.278.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.279.0",
+ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.279.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -282192,3 +282192,2284 @@ export interface TodayReminderResult {
@@ -282192,3 +282192,2284 @@ export interface TodayReminderResult {
next_at?: string | null;
next_at?: string | null;
error?: string;
error?: string;
}
}
+
+/** [email protected] (owner: connect) */
+/**
+ * website.share@1: one Share on OpenVibe.Website Connect, a local port published at a stable hostname through the edge relay. The record says who owns it, which project scopes it, which Node tunnel serves it, which local target it may reach, which policy version the edge enforces and where it is in its life (draft, review, active, paused, revoked). The tunnel token, the request bodies the inspector captures and the policy's password hash are never part of this document.
+ */
+export interface WebsiteShare {
+ id: string;
+ /**
+ * The person who owns the share. A project share is still owned by a person; project_id says which project it belongs to.
+ */
+ owner: SubjectRef;
+ /**
+ * The project scope when the share belongs to one; null for a personal share. The token's project scope, never a caller-supplied body field, decides access.
+ */
+ project_id?: string | null;
+ /**
+ * The Network node principal expected to serve the tunnel; null until one is bound.
+ */
+ node_id?: string | null;
+ /**
+ * One DNS label under openvibe.website. The first proof uses the single label agents-<owner> because Universal SSL covers one label only.
+ */
+ name: string;
+ /**
+ * The fully qualified public host, lowercased; the default is name.openvibe.website, and a verified custom domain may replace it.
+ */
+ hostname: string;
+ /**
+ * What the share carries. http and https are HTTP/WS/SSE shares; tcp and udp are port shares.
+ */
+ protocol: "http" | "https" | "tcp" | "udp";
+ /**
+ * The local target the Node may forward to, e.g. 127.0.0.1:3000, tcp://127.0.0.1:25565 or udp://127.0.0.1:19132. Loopback by default; a LAN target is allowed only when the owner enabled it locally.
+ */
+ local_target: string;
+ /**
+ * review holds a public share for the abuse screen; revoked is terminal and closes the route and tunnel.
+ */
+ status: "draft" | "review" | "active" | "paused" | "revoked";
+ policy_id: string;
+ /**
+ * The signed policy version the edge enforces. A request is checked against this version; revocation bumps it and closes the tunnel.
+ */
+ policy_version: number;
+ /**
+ * The edge region the share is pinned to, e.g. eu or us-east; null before placement.
+ */
+ region?: string | null;
+ /**
+ * The edge instance serving the active tunnel; null while the share is offline.
+ */
+ edge_id?: string | null;
+ /**
+ * When the share revokes itself; null for no expiry.
+ */
+ expires_at?: string | null;
+ /**
+ * The last successful Node liveness check; null until the first tunnel.
+ */
+ last_seen_at?: string | null;
+ created_at: string;
+ updated_at: string;
+ revoked_at?: string | null;
+ href?: string;
+}
+
+/** [email protected] (owner: connect) */
+/**
+ * website.share-create-request@1: the body of website.share.create (POST /api/v1/shares). The caller names one DNS label, the protocol, the local target and the policy; the project scope comes from the token, never from this body. Connect returns only website.share@1; CON-5 defines a separate authenticated exchange for the one-time tunnel token.
+ */
+export interface WebsiteShareCreateRequest {
+ /**
+ * The requested single label under openvibe.website. A taken or reserved label is refused; the UI may suggest name-2 rather than changing it silently.
+ */
+ name: string;
+ protocol: "http" | "https" | "tcp" | "udp";
+ /**
+ * The local port the Node exposes, e.g. 127.0.0.1:3000; never an arbitrary internet host.
+ */
+ local_target: string;
+ /**
+ * The policy to enforce. An id binds an existing owner-checked policy; without one, Connect creates it. Server-owned fields are refused in this caller body. Credential and link secrets are configured separately.
+ */
+ policy: WebsitePolicy & {
+ owner?: never;
+ project_id?: never;
+ version?: never;
+ created_at?: never;
+ updated_at?: never;
+ password_set?: never;
+ };
+ /**
+ * Optional: bind the share to this Node principal now. Omit to bind it when the first tunnel connects.
+ */
+ node_id?: string;
+ /**
+ * Optional preferred edge region; placement may ignore it when capacity or policy requires.
+ */
+ region?: string;
+}
+
+/** [email protected] (owner: connect) */
+/**
+ * website.share-connect-request@1: what a Node presents to open the tunnel for one share (POST /api/v1/shares/:id/tunnel, capability website.share.connect). The Node proves its Network node principal with a client-credentials token whose audience is openvibe.website, and this short-lived, single-use tunnel_token minted by Connect binds the connection to one share. The edge checks both; neither alone opens the route. The token is presented once and never stored, logged or returned.
+ */
+export interface WebsiteShareConnectRequest {
+ share_id: string;
+ /**
+ * Must equal the node principal in the Network token; a mismatch is refused and audited.
+ */
+ node_id: string;
+ /**
+ * A short-lived, single-use, share-scoped token minted by Connect. Send it once over TLS; never put it in a URL, log, audit entry or event.
+ */
+ tunnel_token: string;
+ /**
+ * The data-plane transport the Node is trying. quic is ALPN ovconnect/1; http2-ws is the 443 fallback when UDP is blocked.
+ */
+ transport?: "quic" | "http2-ws";
+ /**
+ * The Node release, for posture and diagnostics; never used for authorization.
+ */
+ node_version?: string;
+ /**
+ * The edge region the Node is dialing; the edge refuses a region it does not serve.
+ */
+ region?: string;
+}
+
+/** [email protected] (owner: connect) */
+/**
+ * website.policy@1: how a Share decides who may reach it and under what conditions (public, OpenVibe sign-in, named people or projects, password, one-time link, expiry), plus IP/country rules, rate limits, a bot check, a WAF preset and required device posture. mode is the only field required except that expiry also requires a non-null expires_at. id, owner and version are present when Connect returns a stored policy. Password and one-time policies stay inactive until their credential or link is configured through the CON-5 API. A password is stored as a one-way hash beside this record and is never part of this document; password_set only says one exists.
+ */
+export interface WebsitePolicy {
+ /**
+ * Present on a stored policy; omitted when the policy is written inline with a new share.
+ */
+ id?: string;
+ /**
+ * The person who owns the policy; set from the token when a policy is created.
+ */
+ owner?: SubjectRef;
+ /**
+ * The project scope, when the policy belongs to one.
+ */
+ project_id?: string | null;
+ name?: string;
+ /**
+ * public: anyone with the link after the abuse screen; signin: any OpenVibe account (optionally a project); named: allowed_subjects/allowed_projects only; password: a shared password; one_time: single-use links; expiry: public access only until expires_at. An expires_at may also cap any other mode.
+ */
+ mode: "public" | "signin" | "named" | "password" | "one_time" | "expiry";
+ /**
+ * Explicit people or agents allowed in named mode; a project's members are allowed through allowed_projects instead.
+ *
+ * @maxItems 500
+ */
+ allowed_subjects?: SubjectRef[];
+ /**
+ * Projects whose members may reach a named or signin share.
+ *
+ * @maxItems 100
+ */
+ allowed_projects?: string[];
+ /**
+ * CIDR ranges allowed before any other rule; empty means no IP allowlist.
+ *
+ * @maxItems 100
+ */
+ ip_allow?: string[];
+ /**
+ * CIDR ranges always refused; evaluated before the allowlist.
+ *
+ * @maxItems 100
+ */
+ ip_deny?: string[];
+ /**
+ * ISO 3166-1 alpha-2 country codes allowed; empty means all.
+ *
+ * @maxItems 250
+ */
+ country_allow?: string[];
+ /**
+ * @maxItems 250
+ */
+ country_deny?: string[];
+ rate_limits?: {
+ per_share_per_minute?: number;
+ per_ip_per_minute?: number;
+ max_connections?: number;
+ max_request_bytes?: number;
+ };
+ bot_check?: "none" | "turnstile";
+ /**
+ * Defense in depth at the edge; Cloudflare's managed rules run in front of it for HTTP/WS.
+ */
+ waf_preset?: "none" | "standard" | "strict" | "api";
+ /**
+ * Self-reported device posture a share may require; a signal, not a guarantee.
+ *
+ * @maxItems 10
+ */
+ posture_required?:
+ | []
+ | ["node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ]
+ | [
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports",
+ "node_current" | "os_patched" | "firewall_on" | "disk_encryption" | "no_risky_ports"
+ ];
+ /**
+ * Read-only: true when a password hash exists. The hash itself never leaves Connect.
+ */
+ password_set?: boolean;
+ /**
+ * An optional hint shown on the password page; never the password.
+ */
+ password_hint?: string | null;
+ /**
+ * For one_time mode; 1 unless the owner allows more.
+ */
+ one_time_max_uses?: number;
+ /**
+ * How long a host-only __Host-ov_session on the share host stays valid after sign-in.
+ */
+ session_ttl_seconds?: number;
+ /**
+ * The absolute end of the policy; required and non-null for expiry mode, optional for other modes.
+ */
+ expires_at?: string | null;
+ /**
+ * The signed policy version the edge caches and checks on every request; present on a stored policy.
+ */
+ version?: number;
+ created_at?: string;
+ updated_at?: string;
+}
+
+/** [email protected] (owner: connect) */
+/**
+ * website.audit-entry@1: one decision row for a Share, as the owner or a moderator sees it. It answers who reached (or tried to reach) a share, from where, what method and path, what the policy decided and how much crossed the edge. It deliberately carries no request or response body, headers, cookies, bearer or tunnel tokens, query string, password or hook payload: query strings are stripped before this record is made and bodies are captured, if at all, by the separate opt-in inspector with its own redaction.
+ */
+export interface WebsiteAuditEntry {
+ id: string;
+ share_id: string;
+ /**
+ * The edge request id, for joining a row to a captured request and a trace; never a secret.
+ */
+ request_id: string;
+ /**
+ * Who the policy identified; absent for an anonymous request.
+ */
+ subject?: SubjectRef;
+ /**
+ * The observed client address (IPv4 or IPv6). Behind Cloudflare it is CF-Connecting-IP, trusted only from Cloudflare's ranges.
+ */
+ ip?: string | null;
+ /**
+ * ISO 3166-1 alpha-2 country from the edge's trusted lookup.
+ */
+ country?: string | null;
+ user_agent?: string | null;
+ method?: string | null;
+ /**
+ * Path only, with the query string removed. No body, header, cookie or token is ever stored here.
+ */
+ path?: string | null;
+ status?: number | null;
+ /**
+ * The policy outcome: allow, deny, a bot challenge, an OIDC/sign-in redirect, or an edge error.
+ */
+ decision: "allow" | "deny" | "challenge" | "redirect" | "error";
+ /**
+ * A stable, short reason such as policy.denied or policy.password_required; never free-form request data.
+ */
+ reason?: string;
+ policy_id?: string | null;
+ policy_version?: number | null;
+ bytes_in?: number | null;
+ bytes_out?: number | null;
+ edge_id?: string | null;
+ region?: string | null;
+ at: string;
+}
+
+/** [email protected] (owner: connect) */
+/**
+ * connect.device@1: one device in a private Connect mesh, as its owner sees it. A device belongs to exactly one tenant (the person's own network or a project's), is identified by a Network node principal, and carries only self-reported posture. The mesh control-plane's internal node id, pre-auth key and WireGuard keys are not part of this document.
+ */
+export interface ConnectDevice {
+ id: string;
+ /**
+ * The person who owns the device.
+ */
+ owner: SubjectRef;
+ /**
+ * The project whose mesh the device joins; null for a personal mesh.
+ */
+ project_id?: string | null;
+ /**
+ * The isolation boundary: one headscale tenant per workspace in v1, never one shared tailnet for all consumer devices.
+ */
+ tenant:
+ | {
+ type: "user";
+ id: string;
+ }
+ | {
+ type: "project";
+ id: string;
+ };
+ /**
+ * The Network node principal the device presents; null for an agent or server that joins through a pre-auth key.
+ */
+ node_id?: string | null;
+ /**
+ * One DNS label in the mesh, shown as name.owner (e.g. pc.alice); never a public hostname.
+ */
+ name: string;
+ /**
+ * pending waits for the owner's approval of a pre-auth join; removed is terminal and its keys are revoked.
+ */
+ status: "pending" | "online" | "offline" | "removed";
+ os: "linux" | "macos" | "windows" | "ios" | "android" | "other";
+ arch?: "amd64" | "arm64" | "386" | "arm" | "other";
+ /**
+ * The Node release running on the device.
+ */
+ version: string;
+ /**
+ * The device's private mesh addresses; never a public address.
+ *
+ * @maxItems 8
+ */
+ addresses?:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string];
+ /**
+ * @maxItems 50
+ */
+ tags?: string[];
+ /**
+ * Self-reported posture, a signal not a guarantee; checked_at says when it was read.
+ */
+ posture?: {
+ node_current: boolean;
+ os_patched: boolean;
+ firewall_on: boolean;
+ disk_encryption: boolean;
+ /**
+ * @maxItems 200
+ */
+ risky_ports: number[];
+ checked_at: string;
+ } | null;
+ /**
+ * Subnet routes the device advertises, e.g. 192.168.1.0/24; empty for none.
+ *
+ * @maxItems 50
+ */
+ routes?: string[];
+ /**
+ * Whether the owner allows the device to act as an exit node for this tenant only; never a public proxy.
+ */
+ exit_node?: boolean;
+ approved_at?: string | null;
+ /**
+ * The person who approved the join; null while pending.
+ */
+ approved_by?: SubjectRef;
+ last_seen_at?: string | null;
+ /**
+ * When an unapproved or temporary device stops being valid; null for no expiry.
+ */
+ expires_at?: string | null;
+ created_at: string;
+ updated_at: string;
+ revoked_at?: string | null;
+}
+
+/** [email protected] (owner: connect) */
+/**
+ * connect.policy@1: the grants, routes and exit-node settings of one private Connect tenant. It is deliberately tenant-scoped: a rule never names a device, person or tag from another tenant, and default_action is deny unless the owner opens a path. It describes intent, not the control plane's internal ACL file or keys.
+ */
+export interface ConnectPolicy {
+ id: string;
+ owner: SubjectRef;
+ project_id?: string | null;
+ /**
+ * The one tenant this policy can affect; a rule or route naming another tenant's id is invalid.
+ */
+ tenant:
+ | {
+ type: "user";
+ id: string;
+ }
+ | {
+ type: "project";
+ id: string;
+ };
+ name?: string;
+ /**
+ * Bumped on every change; a device keeps the version it last applied.
+ */
+ version: number;
+ /**
+ * deny unless the owner explicitly opens a path.
+ */
+ default_action: "allow" | "deny";
+ /**
+ * @maxItems 200
+ */
+ rules: {
+ action: "allow" | "deny";
+ /**
+ * @minItems 1
+ * @maxItems 50
+ */
+ sources: [string, ...string[]];
+ /**
+ * @minItems 1
+ * @maxItems 50
+ */
+ destinations: [string, ...string[]];
+ /**
+ * TCP/UDP ports or ranges; omit for all ports.
+ *
+ * @maxItems 50
+ */
+ ports?: string[];
+ description?: string;
+ }[];
+ /**
+ * Exit nodes are usable only by this tenant's devices; no public share may target one.
+ */
+ exit_node?: {
+ enabled: boolean;
+ /**
+ * @maxItems 20
+ */
+ device_ids:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ } | null;
+ /**
+ * @maxItems 100
+ */
+ subnet_routes?: {
+ device_id: string;
+ cidr: string;
+ }[];
+ updated_by?: SubjectRef;
+ created_at: string;
+ updated_at: string;
+}
+
+/** [email protected] (owner: events) */
+/**
+ * events.hook@1: one durable webhook subscription managed through OpenVibe.Website Connect, delivered by OpenVibe.Events. The hook names its share, the public ingress path, the event topics it accepts, filters/transform, the retry window and the signature version. The signing secret is shown once when the hook is created or its secret is rotated; secret_set says one exists, and no field of this document ever carries the value.
+ */
+export interface EventsHook {
+ id: string;
+ owner: SubjectRef;
+ project_id?: string | null;
+ /**
+ * The share whose tunnel receives deliveries; delivery never goes to an arbitrary host.
+ */
+ share_id: string;
+ name: string;
+ /**
+ * The public ingress, https://hooks.openvibe.website/h/<id>; Connect also records the on-share path https://<share host>/__ov/hook/<id>.
+ */
+ endpoint: string;
+ /**
+ * Event types this hook accepts, e.g. github.push.received or stripe.invoice.paid; Event's topic patterns may be used by the delivery filter.
+ *
+ * @minItems 1
+ * @maxItems 50
+ */
+ topics: [string, ...string[]];
+ /**
+ * @maxItems 20
+ */
+ filters?:
+ | []
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ]
+ | [
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ },
+ {
+ /**
+ * A JSONPath into the event payload.
+ */
+ path: string;
+ equals?: string | number | boolean | null;
+ }
+ ];
+ transform?: {
+ /**
+ * Headers added to the delivery; never the hook's signing secret.
+ */
+ headers?: {
+ [k: string]: string | undefined;
+ };
+ /**
+ * An optional template applied to the event; the raw payload is used when absent.
+ */
+ body_template?: string;
+ } | null;
+ retry: {
+ /**
+ * How long Events keeps retrying while the share is offline; seven days is the hook default.
+ */
+ window_seconds: number;
+ max_attempts: number;
+ };
+ signature: {
+ version: "v2";
+ /**
+ * How far a signed timestamp may drift before the Node refuses it; 300 is the default.
+ */
+ tolerance_seconds: number;
+ };
+ max_payload_bytes?: number;
+ status: "active" | "paused" | "revoked";
+ /**
+ * True once a signing secret exists. The secret itself is returned only in the create/rotate answer and is never stored in this contract, a delivery record or an audit entry.
+ */
+ secret_set: boolean;
+ secret_rotated_at?: string | null;
+ last_delivery_at?: string | null;
+ /**
+ * When the hook stopped receiving deliveries after its retry window; null while it is live.
+ */
+ dead_at?: string | null;
+ created_at: string;
+ updated_at: string;
+ href?: string;
+}
+
+/** [email protected] (owner: events) */
+/**
+ * events.hook-delivery@1: one attempt by OpenVibe.Events to deliver an event to a hook. It carries the outcome, timing, size and hash of the payload, never the payload itself, its headers, the signature or the hook's secret. A delivery is retried inside its hook's window and dead-lettered after it; replay creates a new attempt that names the one it replays.
+ */
+export interface EventsHookDelivery {
+ id: string;
+ hook_id: string;
+ event_id: string;
+ event_type: string;
+ attempt: number;
+ /**
+ * failed is a retryable attempt; dead is the last, after the hook's window. A 2xx answer marks delivered.
+ */
+ status: "queued" | "delivering" | "delivered" | "failed" | "dead";
+ /**
+ * The delivery request id, for tracing; not a secret.
+ */
+ request_id?: string | null;
+ response_code?: number | null;
+ latency_ms?: number | null;
+ payload_bytes: number;
+ /**
+ * The SHA-256 of the signed body, so a consumer can check it without the body being stored here.
+ */
+ payload_sha256: string;
+ signature_version?: "v2";
+ next_attempt_at?: string | null;
+ attempted_at?: string | null;
+ completed_at?: string | null;
+ dead_at?: string | null;
+ /**
+ * A short transport or status reason; never a response body.
+ */
+ error?: string | null;
+ /**
+ * The failed or dead attempt this one replays.
+ */
+ replay_of?: string | null;
+ created_at: string;
+}
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
const crypto = require('crypto');
const crypto = require('crypto');
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
-const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', consent: 'cns', job: 'job', task: 'tsk', watch: 'wch', wait: 'wai', observation: 'wco', check: 'ckr', subscription: 'sub', deploy: 'dpl', domain: 'dom', manifest: 'mfs', release: 'rel', site: 'sit', robot: 'rob', stream: 'std', webhook_delivery: 'whd' };
+const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', consent: 'cns', job: 'job', task: 'tsk', watch: 'wch', wait: 'wai', observation: 'wco', check: 'ckr', subscription: 'sub', deploy: 'dpl', domain: 'dom', manifest: 'mfs', release: 'rel', site: 'sit', robot: 'rob', stream: 'std', webhook_delivery: 'whd', share: 'shr', device: 'dev', hook: 'hok', policy: 'pol', audit: 'aud' };
const SLUG_TYPES = new Set(['service', 'system']);
const SLUG_TYPES = new Set(['service', 'system']);
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "connect.device.manage",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): approve a pending device, change its name, tags, routes or exit-node setting, or remove it (POST /api/v1/devices/:id/approve, PATCH /api/v1/devices/:id, DELETE /api/v1/devices/:id) as connect.device@1. Removal revokes the mesh key at the control plane at once; an approval never admits a device to another tenant. Pre-auth keys and WireGuard keys are never returned or logged.",
+ "permissions": [
+ "connect:devices:manage"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "connect-device",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/devices/:id/approve",
+ "PATCH /api/v1/devices/:id",
+ "DELETE /api/v1/devices/:id"
+ ],
+ "inputSchema": "connect.device@1",
+ "outputSchema": "connect.device@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "connect.device.read",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): list the devices in the caller's private mesh and read one by id (GET /api/v1/devices and GET /api/v1/devices/:id) as connect.device@1. Every device belongs to exactly one tenant, so a read never crosses a tenant boundary; a device in another tenant is 404. Posture is the self-reported signal last checked, never a claim the service verifies.",
+ "permissions": [
+ "connect:devices:read"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "connect-device",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/devices",
+ "GET /api/v1/devices/:id"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "connect.device@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "connect.policy.read",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "first-party",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): read one tenant's mesh policy (GET /api/v1/connect/policies/:id) as connect.policy@1, with its grants, default action, exit-node setting and subnet routes. A policy is readable only inside its own tenant; the answer carries no pre-auth key, WireGuard key or control-plane ACL file.",
+ "permissions": [
+ "connect:policies:read"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "connect-policy",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/connect/policies/:id"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "connect.policy@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "connect.policy.write",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "first-party",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): write one tenant's mesh grants, routes and exit-node setting (PUT /api/v1/connect/policies/:id) from a connect.policy@1. The tenant in the body must be the caller's tenant; a rule or route naming another tenant's device is refused. Exit-node traffic is opened only to this tenant's devices and never to a public share. A write bumps the policy version the devices apply.",
+ "permissions": [
+ "connect:policies:write"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "connect-policy",
+ "events": [],
+ "implementedBy": [
+ "PUT /api/v1/connect/policies/:id"
+ ],
+ "inputSchema": "connect.policy@1",
+ "outputSchema": "connect.policy@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "events.hook.manage",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): create, change, pause, resume, rotate the secret of or revoke a durable hook (POST /api/v1/hooks, PATCH /api/v1/hooks/:id, DELETE /api/v1/hooks/:id) as events.hook@1. The signing secret is shown once in the create or rotate answer and stored by Events; it is never in a read, a delivery record, an event or an audit entry. The delivery endpoint is fixed to the hook's own share host, so a hook cannot be pointed at another host.",
+ "permissions": [
+ "events:hooks:manage"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-hook",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/hooks",
+ "PATCH /api/v1/hooks/:id",
+ "DELETE /api/v1/hooks/:id"
+ ],
+ "inputSchema": "events.hook@1",
+ "outputSchema": "events.hook@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "events.hook.read",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): read a hook and its delivery log (GET /api/v1/hooks/:id and GET /api/v1/hooks/:id/deliveries) as events.hook@1 and events.hook-delivery@1. The delivery log says attempt, status, response code, latency, size and payload hash; it never returns the payload, its headers, the signature or the hook's secret. Replay is a write and needs events.hook.manage.",
+ "permissions": [
+ "events:hooks:read"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-hook",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/hooks/:id",
+ "GET /api/v1/hooks/:id/deliveries"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "events.hook-delivery@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "website.abuse.report",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, any signed-in caller): report a share (POST /api/v1/shares/:id/report) with a short reason and, optionally, a public evidence URL; a report never carries a password, cookie or private message. Reports go to the moderation queue with the share's captured evidence and the reporter's subject. The request and answer bodies are added with CON-5's abuse contracts; the capability is registered here so the queue and the grant exist before code.",
+ "permissions": [
+ "website:abuse:report"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "website-abuse",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/shares/:id/report"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "website.abuse.review",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "internal",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, staff or moderator only): read the abuse queue and decide one report (GET /api/v1/admin/abuse, POST /api/v1/admin/abuse/:id/:action) — hold, activate, pause or takedown a share. A staff decision is audited with the moderator's subject and the evidence, not the request body; a takedown is also announced to Domains for a custom hostname. This is an internal moderation capability, never delegated to user or app tokens.",
+ "permissions": [
+ "website:abuse:review"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "website-abuse",
+ "events": [
+ "website.share.review",
+ "website.share.revoked"
+ ],
+ "implementedBy": [
+ "GET /api/v1/admin/abuse",
+ "POST /api/v1/admin/abuse/:id/:action"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "website.admin.manage",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "internal",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, staff or operations only): act on a share from the operator view (POST /api/v1/admin/shares/:id/:action) — pause it, revoke it, release an abuse hold or override a quota — as a website.share@1 action. Every action writes website.share.paused or website.share.revoked and an audit row naming the operator; it can close an edge tunnel at once. Internal, never delegated, and never a way to read a share's traffic or secrets.",
+ "permissions": [
+ "website:admin:manage"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "website-admin",
+ "events": [
+ "website.share.paused",
+ "website.share.revoked"
+ ],
+ "implementedBy": [
+ "POST /api/v1/admin/shares/:id/:action"
+ ],
+ "inputSchema": "website.share@1",
+ "outputSchema": "website.share@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "website.admin.read",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "internal",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, staff or operations only): read the operator view of shares, tunnels, hooks and quota counters (GET /api/v1/admin/shares and GET /api/v1/admin/shares/:id), beyond what the owner sees but still without tunnel tokens, hook secrets, request bodies or message content. This is the read half of the internal admin surface; a grant is never issued to user, app or agent tokens.",
+ "permissions": [
+ "website:admin:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "website-admin",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/admin/shares",
+ "GET /api/v1/admin/shares/:id"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "website.share@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "website.domain.manage",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): attach, verify or remove a custom domain for a share with POST /api/v1/domains from a domains.proposal@1. Connect delegates the DNS change to OpenVibe.Domains — the owner approves it in the Action Center, Domains writes the TXT/CNAME and answers with the applied proposal — and Connect never writes DNS itself or asks for the provider credential. Apex CNAME flattening, Cloudflare for SaaS and the certificate path are CON-2 decisions; the first proof uses a single-label openvibe.website host only.",
+ "permissions": [
+ "website:domains:manage"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-domain",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/domains"
+ ],
+ "inputSchema": "domains.proposal@1",
+ "outputSchema": "domains.proposal@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "website.policy.read",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): read one share policy by id (GET /api/v1/policies/:id) as website.policy@1. The answer says which mode is enforced, its allowances and limits, and the signed version the edge caches; it never includes a password hash or any secret. Another owner's policy is 404.",
+ "permissions": [
+ "website:policies:read"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-policy",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/policies/:id"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "website.policy@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "website.policy.write",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): create or change a share policy with POST /api/v1/policies or PATCH /api/v1/policies/:id from a website.policy@1. A password arrives in the request body, is hashed with Argon2id and is stored beside the policy, never in the policy document; a write bumps the signed version and tells the edge. The owner and project come from the token, so a body cannot move a policy to another project.",
+ "permissions": [
+ "website:policies:write"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-policy",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/policies",
+ "PATCH /api/v1/policies/:id"
+ ],
+ "inputSchema": "website.policy@1",
+ "outputSchema": "website.policy@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "website.share.connect",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "first-party",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, node principal): a Node opens one share's tunnel with POST /api/v1/shares/:id/tunnel and a website.share-connect-request@1. The edge checks the Network node token (client credentials, subject node:nod_…, audience openvibe.website) and the short-lived, single-use tunnel token minted by Connect; neither alone opens the route. The node token's subject must equal node:<node_id> from the request, and a mismatch is refused and audited. Not delegated to user or app tokens.",
+ "permissions": [
+ "website:tunnels:connect"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-tunnel",
+ "events": [
+ "website.share.activated"
+ ],
+ "implementedBy": [
+ "POST /api/v1/shares/:id/tunnel"
+ ],
+ "inputSchema": "website.share-connect-request@1",
+ "outputSchema": "website.share@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "website.share.create",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): create a Share with POST /api/v1/shares from a website.share-create-request@1 — one DNS label, protocol, local target and policy. The POST returns only website.share@1 in draft, with no tunnel token. CON-5 defines the separate authenticated exchange that gives the Node a short-lived, single-use, share-scoped tunnel token; it is never stored, logged or put in an event. A public share stays in review until the abuse screen passes; no capability is granted by this contract.",
+ "permissions": [
+ "website:shares:create"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-share",
+ "events": [
+ "website.share.created"
+ ],
+ "implementedBy": [
+ "POST /api/v1/shares"
+ ],
+ "inputSchema": "website.share-create-request@1",
+ "outputSchema": "website.share@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "website.share.delete",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): revoke and delete a share with DELETE /api/v1/shares/:id. Connect writes website.share.revoked, closes the edge route and tunnel within its 5-second revocation budget and keeps the audit trail; a public hostname returns 410, not a cached page. Deleting a node in Network closes every share for that node through network.node.revoked.",
+ "permissions": [
+ "website:shares:delete"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-share",
+ "events": [
+ "website.share.revoked"
+ ],
+ "implementedBy": [
+ "DELETE /api/v1/shares/:id"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "common.no-body@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "website.share.inspect",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or granted app): read the request inspector for a share (GET /api/v1/shares/:id/requests), a bounded ring buffer of the last N requests as website.audit-entry@1 rows plus captured metadata. Bodies, headers and cookies are captured only when the owner opted in, under a separate redaction pass; query strings and tokens are stripped, and this read never returns a secret. The buffer is 24 hours on free and 7 days on paid plans.",
+ "permissions": [
+ "website:inspector:read"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-inspect",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/shares/:id/requests"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "website.audit-entry@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "website.share.read",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): list the caller's shares and read one by id (GET /api/v1/shares and GET /api/v1/shares/:id). A user token sees only its own shares and the projects it is a member of; another owner's share is 404, never 403. The list answers website.share@1 rows without tunnel tokens or captured bodies.",
+ "permissions": [
+ "website:shares:read"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-share",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/shares",
+ "GET /api/v1/shares/:id"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "website.share@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "website.share.replay",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or granted app): replay one captured request through the same share (POST /api/v1/shares/:id/requests/:rid/replay). Replay is allowed only while the share and its tunnel are active and only to that share's own local target; it never becomes a general proxy and never changes the share's policy. The replay is audited as a new request with its own request_id.",
+ "permissions": [
+ "website:inspector:replay"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-inspect",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/shares/:id/requests/:rid/replay"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "common.no-body@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "website.share.update",
+ "version": "1.0",
+ "owner": "connect",
+ "status": "planned",
+ "visibility": "public",
+ "sensitive": true,
+ "description": "PLANNED for OpenVibe.Website Connect (audience openvibe.website, owner or project scope): rename a share, retarget its local port or change its policy with PATCH /api/v1/shares/:id. A policy change bumps website.share.policy_version so the edge's signed cache is stale at once; changing the public label needs the name registry, and a taken or reserved label is refused. Project scope comes from the token, never the body.",
+ "permissions": [
+ "website:shares:update"
+ ],
+ "resourceConstraints": [
+ "owner",
+ "project"
+ ],
+ "quotaClass": "website-share",
+ "events": [],
+ "implementedBy": [
+ "PATCH /api/v1/shares/:id"
+ ],
+ "inputSchema": "website.share@1",
+ "outputSchema": "website.share@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": ">=0.279.0 <1.0.0"
+ },
+ "id": "connect",
+ "name": "OpenVibe Connect",
+ "status": "alpha",
+ "repository": "OpenVibers/OpenVibe.Connect",
+ "domains": [
+ "connect.openvibe.website"
+ ],
+ "publicOrigin": "https://connect.openvibe.website",
+ "exposure": {
+ "state": "live",
+ "publicSite": "service"
+ },
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "website.*",
+ "connect.*"
+ ],
+ "lifecycle": {
+ "liveness": {
+ "endpoint": "/api/health",
+ "means": "the process is up and answers HTTP, with how many computers are connected; the database, Network's JWKS and the OAuth client are what /api/ready checks"
+ },
+ "shutdown": {
+ "signal": "SIGTERM",
+ "deadlineSeconds": 10,
+ "drains": [
+ "HTTP server closed and in-flight requests drained",
+ "every tunnel closed with 1001 (the computers reconnect to the next process by themselves)",
+ "the request counters written, the one-time codes dropped",
+ "JWKS refresher stopped, telemetry flushed, PostgreSQL pool closed, then the Events subscriptions stopped"
+ ]
+ },
+ "startupRecovery": {
+ "resumes": [
+ {
+ "kind": "sessions",
+ "what": "connected computers reconnect with backoff (1 s to 30 s) and say hello again; nothing about a tunnel is kept across a restart"
+ },
+ {
+ "kind": "consumer",
+ "what": "the account export and deletion subscriptions at OpenVibe.Events are renewed at boot; a delivery missed while stopped is redelivered and applied once (account_data_events keeps it idempotent)"
+ }
+ ]
+ },
+ "rollback": {
+ "conditions": [
+ "automatic: ovhost deploy puts the previous sha back and restarts again when the ready URL does not answer 2xx within ready.timeoutSeconds after the restart (exit 3)",
+ "manual: ovhost rollback <service> --to <sha>"
+ ],
+ "window": "automatic only while ovhost waits for readiness after the restart; manual afterwards, as long as no blocker applies",
+ "blockers": {
+ "none": "migration 0001 only creates Connect's own tables (shares, share_visits) plus the SDK's; an earlier release reads none of them"
+ }
+ },
+ "contracts": {
+ "range": ">=0.279.0 <1.0.0"
+ },
+ "leases": {
+ "none": "one process holds every tunnel; a second process would need the share-to-tunnel routing CON-3 adds"
+ }
+ },
+ "notes": "Port 5240, unit openvibe-connect.service, PostgreSQL database ov_connect. Website Connect v0 (plan 2026-10-28 §5.9b; ADR-057 as reviewed 2026-11-02): a share is one port on a person's computer at https://<label>--<user>.openvibe.website behind OpenVibe sign-in, carried by a one-file client (Node 22) over one WebSocket on 443 (ovconnect-ws/1). Space site names never contain \"--\", so share hosts and Space sites never collide; nginx sends *--*.openvibe.website here and every other *.openvibe.website to Space. Modes owner, named and signin (staff-first); no public shares until CON-9 and CON-21. The share API is a person's (session or Network user token): GET/POST /api/v1/shares, GET/PATCH/DELETE /api/v1/shares/:id, POST /api/v1/shares/:id/key, GET /api/v1/shares/:id/visits. The website.*/connect.*/events.hook* capabilities stay planned until the QUIC edge, the Node plugin and the hooks land (CON-3/4/5/15). OAuth client `connect`; answers account export and deletion (ADR-033) and subscribes to those Events topics.",
+ "health": "/api/health",
+ "ready": "/api/ready",
+ "internalOrigin": "http://127.0.0.1:5240",
+ "site": {
+ "name": "Connect",
+ "icon": "connect",
+ "tagline": "Your computer, on the web, privately",
+ "what": "Share one port on your computer at its own HTTPS address on openvibe.website. Only the people you pick can open it, signed in with OpenVibe. One file and Node: no router settings, no open ports.",
+ "legalProfile": "ugc",
+ "position": 51,
+ "neighbourhood": "make"
+ }
+}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.268.0",
+ "version": "0.279.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.268.0",
+ "version": "0.279.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.278.0",
+ "version": "0.279.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",
@@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
'use strict';
'use strict';
// ADR-005 amendment 1: people's presence is ephemeral, in Chat's delivery plane.
// ADR-005 amendment 1: people's presence is ephemeral, in Chat's delivery plane.
-// ADR-043 records robot connectivity in Events; it is not a person's presence.
+// ADR-043 records robot connectivity in Events; ADR-057 records Connect device connectivity;
+// neither is a person's presence.
// The machines hub (plan L34) records the same for a person's machines: a machine coming online is the
// The machines hub (plan L34) records the same for a person's machines: a machine coming online is the
// device changing state, never the person being present. Any other `.online`, `.offline` or presence event
// device changing state, never the person being present. Any other `.online`, `.offline` or presence event
// type is still refused.
// type is still refused.