openvibers/openvibe.contracts Commits
Capability schemas (WS-C task 4) for Chat, Community and Billing: chat.send-request/send-result (the /ws/chat chat frame and its broadcast, the Live bridge batch); community paste, paste create/write/moderate, forum post writes, discussion moderation and Pulse write schemas; billing transaction, balance, cashout, subscription, entitlement and intent records with each capability's request and result. 18 capabilities complete; gaps 162 -> 144 (unreleased)
Changes
@@@ -28,62 +28,6 @@"outputSchema"]},-- {-- "id": "billing.balance.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.cashout.manage",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.cashout.request",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.entitlement.check",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.intent.create",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.ledger.admin",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.subscription.manage",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "billing.transfer.create",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },{"id": "blog.blog.configure","missing": [@@@ -168,13 +112,6 @@"outputSchema"]},-- {-- "id": "chat.message.send",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },{"id": "codes.release.manage","missing": [@@@ -187,63 +124,6 @@"inputSchema"]},-- {-- "id": "community.comment.moderate",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "community.paste.create",-- "missing": [-- "inputSchema"-- ]-- },-- {-- "id": "community.paste.moderate",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "community.paste.write",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "community.post.create",-- "missing": [-- "inputSchema"-- ]-- },-- {-- "id": "community.pulse.read",-- "missing": [-- "inputSchema"-- ]-- },-- {-- "id": "community.pulse.write",-- "missing": [-- "outputSchema"-- ]-- },-- {-- "id": "community.space.read",-- "missing": [-- "inputSchema"-- ]-- },-- {-- "id": "community.thread.read",-- "missing": [-- "inputSchema"-- ]-- },{"id": "coupons.coupon.lookup","missing": [nnew file mode 100644@@@ -0,0 +1,180 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/admin-request.v1.json",++ "title": "BillingAdminRequest",++ "description": "billing.admin-request@1: bodies of billing.ledger.admin on OpenVibe.Billing (operators). POST /api/v1/admin/freeze: { on, reason? } (unfreezing processes the provider deliveries stored meanwhile; allowed while frozen). POST /api/v1/admin/adjustments: { from, to, amount, reason, relates_to? } moves value between two accounts of one currency. POST /api/v1/purchases/settle: { provider, provider_ref, subject?, amount_cents, bits, intent_id?, test? } books a provider receipt. POST /api/v1/admin/provider-events/:id/reprocess, POST /api/v1/admin/sweep and the GETs (freeze, reconcile, provider-events, import-holds) take no body. Adjustments and settlements need an Idempotency-Key and are refused while frozen.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "on"++ ],++ "properties": {++ "on": {++ "type": "boolean"++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "from",++ "to",++ "amount",++ "reason"++ ],++ "properties": {++ "from": {++ "type": "object",++ "required": [++ "kind",++ "currency"++ ],++ "properties": {++ "kind": {++ "enum": [++ "user_credit",++ "creator_payable",++ "provider_clearing",++ "platform_revenue",++ "payouts_pending",++ "refunds",++ "import_adjustment",++ "chargeback_loss",++ "fx_conversion"++ ]++ },++ "owner": {++ "type": [++ "string",++ "null"++ ]++ },++ "currency": {++ "enum": [++ "vibes-bits",++ "usd-cents"++ ]++ }++ }++ },++ "to": {++ "type": "object",++ "required": [++ "kind",++ "currency"++ ],++ "properties": {++ "kind": {++ "enum": [++ "user_credit",++ "creator_payable",++ "provider_clearing",++ "platform_revenue",++ "payouts_pending",++ "refunds",++ "import_adjustment",++ "chargeback_loss",++ "fx_conversion"++ ]++ },++ "owner": {++ "type": [++ "string",++ "null"++ ]++ },++ "currency": {++ "enum": [++ "vibes-bits",++ "usd-cents"++ ]++ }++ }++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "In the accounts' currency (at most 1,000,000,000)."++ },++ "reason": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "relates_to": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "provider",++ "provider_ref"++ ],++ "properties": {++ "provider": {++ "type": "string"++ },++ "provider_ref": {++ "type": "string",++ "minLength": 1,++ "description": "The provider's payment id."++ },++ "subject": {++ "description": "Who the Vibes are for.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount_cents": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Cents paid."++ },++ "bits": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Vibes to credit."++ },++ "intent_id": {++ "type": "string"++ },++ "test": {++ "type": "boolean"++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,215 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/admin-result.v1.json",++ "title": "BillingAdminResult",++ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "frozen",++ "reason",++ "frozen_at",++ "frozen_by"++ ],++ "properties": {++ "frozen": {++ "type": "boolean"++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "frozen_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "frozen_by": {},++ "processed_after_unfreeze": {}++ }++ },++ {++ "type": "object",++ "required": [++ "id",++ "ok",++ "checks"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "ok": {++ "type": "boolean"++ },++ "trigger": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": "string"++ },++ "finished_at": {++ "type": "string"++ },++ "checks": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "ok"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "ok": {++ "type": "boolean"++ }++ }++ }++ },++ "warnings": {++ "type": "array"++ },++ "totals": {}++ }++ },++ {++ "type": "object",++ "required": [++ "transaction"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "duplicate_receipt": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "events"++ ],++ "properties": {++ "events": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "provider"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "provider": {++ "type": "string"++ },++ "provider_event_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "type": {++ "type": [++ "string",++ "null"++ ]++ },++ "processed_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "result": {},++ "attempts": {++ "type": "integer"++ },++ "last_error": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "event"++ ],++ "properties": {++ "event": {++ "type": "object",++ "required": [++ "id",++ "provider"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "renewed",++ "expired",++ "canceled",++ "skipped"++ ],++ "properties": {++ "renewed": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "expired": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "canceled": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "skipped": {++ "type": "integer"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "holds"++ ],++ "properties": {++ "holds": {++ "type": "array",++ "items": {++ "type": "object"++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,51 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/balance-read-result.v1.json",++ "title": "BillingBalanceReadResult",++ "description": "billing.balance-read-result@1: answers of billing.balance.read on OpenVibe.Billing. GET /api/v1/balances/:subject → billing.balance@1; GET /api/v1/transactions?subject=&limit=&cursor= → { transactions, next_cursor } (newest first, at most 200 a page; subject is required); GET /api/v1/transactions/:id → { transaction, reversed_by } (ids of the transactions that reverse it).",++ "anyOf": [++ {++ "$ref": "balance.v1.json"++ },++ {++ "type": "object",++ "required": [++ "transactions",++ "next_cursor"++ ],++ "properties": {++ "transactions": {++ "type": "array",++ "maxItems": 200,++ "items": {++ "$ref": "transaction.v1.json"++ }++ },++ "next_cursor": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "transaction",++ "reversed_by"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "reversed_by": {++ "type": "array",++ "items": {++ "type": "string"++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/balance.v1.json",++ "title": "BillingBalance",++ "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.",++ "type": "object",++ "required": ["subject", "currency", "credit", "payable", "pending_payouts", "payable_value_cents", "bits_per_usd"],++ "properties": {++ "subject": { "$ref": "../identity/subject-ref.v1.json" },++ "currency": { "const": "vibes-bits" },++ "credit": { "type": "integer" },++ "payable": { "type": "integer" },++ "pending_payouts": { "type": "integer" },++ "payable_value_cents": { "type": "integer", "description": "What payable is worth in US cents at today's rate." },++ "bits_per_usd": { "type": "number" }++ }++}nnew file mode 100644@@@ -0,0 +1,43 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout-decision.v1.json",++ "title": "BillingCashoutDecision",++ "description": "billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "payout_reference"++ ],++ "properties": {++ "payout_reference": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The provider's payout id."++ },++ "payout_provider": {++ "type": "string"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "reason"++ ],++ "properties": {++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,91 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout-request.v1.json",++ "title": "BillingCashoutRequest",++ "description": "billing.cashout-request@1: bodies of billing.cashout.request on OpenVibe.Billing. POST /api/v1/cashouts: { subject, amount, payout_method: { type, address } } moves amount bits (at least the minimum cashout) from the person's payable into payouts pending, in escrow until staff pay it (a PayPal address must be an email). POST /api/v1/recycle: { subject, amount } turns payable back into the person's own spendable credit. GET /api/v1/cashouts/:id takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "subject",++ "amount",++ "payout_method"++ ],++ "properties": {++ "subject": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits."++ },++ "payout_method": {++ "type": "object",++ "required": [++ "type",++ "address"++ ],++ "properties": {++ "type": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",++ "description": "paypal, …"++ },++ "address": {++ "type": "string",++ "minLength": 1,++ "maxLength": 254++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "subject",++ "amount"++ ],++ "properties": {++ "subject": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits."++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,48 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout-result.v1.json",++ "title": "BillingCashoutResult",++ "description": "billing.cashout-result@1: answers about cashouts on OpenVibe.Billing. POST /api/v1/cashouts (201), GET /api/v1/cashouts/:id, POST /api/v1/cashouts/:id/approve and /deny → { cashout }; GET /api/v1/cashouts?status=&subject=&limit= → { cashouts }; POST /api/v1/recycle (201) → { transaction, balance }. A cashout the Events stream carries names the payout method's type only; this API answers the caller who may see the address.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "cashout"++ ],++ "properties": {++ "cashout": {++ "$ref": "cashout.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "cashouts"++ ],++ "properties": {++ "cashouts": {++ "type": "array",++ "items": {++ "$ref": "cashout.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "transaction",++ "balance"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "balance": {++ "$ref": "balance.v1.json"++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,30 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout.v1.json",++ "title": "BillingCashout",++ "description": "billing.cashout@1: one cashout as OpenVibe.Billing presents it (server/ops/cashouts.js present). Requested cashouts wait out an escrow; staff approve (with the provider's payout reference) or deny them.",++ "type": "object",++ "required": ["id", "subject", "amount_bits", "value_cents", "status", "payout_method", "escrow_until", "request_txn", "created_at"],++ "properties": {++ "id": { "type": "string" },++ "subject": { "$ref": "../identity/subject-ref.v1.json" },++ "amount_bits": { "type": "integer", "minimum": 1 },++ "value_cents": { "type": "integer", "minimum": 0 },++ "status": { "enum": ["requested", "paid", "denied"] },++ "payout_method": {++ "type": ["object", "null"],++ "properties": {++ "type": { "type": "string" },++ "address": { "type": "string" }++ }++ },++ "escrow_until": { "type": "string" },++ "request_txn": { "type": "string" },++ "settle_txn": { "type": ["string", "null"] },++ "payout_provider": { "type": ["string", "null"] },++ "payout_reference": { "type": ["string", "null"] },++ "reason": { "type": ["string", "null"] },++ "created_at": { "type": "string" },++ "updated_at": { "type": ["string", "null"] }++ }++}nnew file mode 100644@@@ -0,0 +1,58 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/entitlement-result.v1.json",++ "title": "BillingEntitlementResult",++ "description": "billing.entitlement-result@1: answers of billing.entitlement.check on OpenVibe.Billing. GET /api/v1/entitlements/:subject?streamer= → billing.entitlement@1; without streamer → { subject, entitlements } (every active one); GET /api/v1/subscriptions → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",++ "anyOf": [++ {++ "$ref": "entitlement.v1.json"++ },++ {++ "type": "object",++ "required": [++ "subject",++ "entitlements"++ ],++ "properties": {++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "entitlements": {++ "type": "array",++ "items": {++ "$ref": "entitlement.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "subscriptions"++ ],++ "properties": {++ "subscriptions": {++ "type": "array",++ "items": {++ "$ref": "subscription.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "subscription",++ "entitlement"++ ],++ "properties": {++ "subscription": {++ "$ref": "subscription.v1.json"++ },++ "entitlement": {++ "$ref": "entitlement.v1.json"++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/entitlement.v1.json",++ "title": "BillingEntitlement",++ "description": "billing.entitlement@1: whether a person is entitled to a streamer's subscriber perks now (OpenVibe.Billing server/ops/subscriptions.js entitlement): GET /api/v1/entitlements/:subject?streamer=, and inside subscription answers. Chained, already-paid future periods extend expires_at.",++ "type": "object",++ "required": ["subject", "streamer", "kind", "active", "expires_at", "subscription"],++ "properties": {++ "subject": { "$ref": "../identity/subject-ref.v1.json" },++ "streamer": { "$ref": "../identity/subject-ref.v1.json" },++ "kind": { "type": "string" },++ "active": { "type": "boolean" },++ "expires_at": { "type": ["string", "null"] },++ "subscription": {++ "type": ["object", "null"],++ "properties": {++ "id": { "type": "string" },++ "status": { "type": "string" },++ "auto_renew": { "type": ["boolean", "integer"] },++ "cancel_at_period_end": { "type": ["boolean", "integer"] },++ "provider": { "type": "string" }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,85 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/intent-request.v1.json",++ "title": "BillingIntentRequest",++ "description": "billing.intent-request@1: the body of POST /api/v1/intents on OpenVibe.Billing (billing.intent.create): a checkout with an enabled provider for a Vibes purchase ({ kind: purchase, bits } at least the minimum purchase) or a channel subscription ({ kind: subscription, streamer }; route direct pays the streamer's own account, and a direct PowerChat subscription names receiving_account). Billing prices it; the answer is { intent, checkout_url } (201). GET /api/v1/intents/:id and POST /api/v1/intents/:id/capture take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "provider",++ "kind",++ "subject"++ ],++ "properties": {++ "provider": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",++ "description": "stripe, paypal, nowpayments, powerchat, ccbill (enabled ones only)."++ },++ "kind": {++ "enum": [++ "purchase",++ "subscription"++ ]++ },++ "subject": {++ "description": "Who pays.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "bits": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Purchase: the Vibes to buy."++ },++ "streamer": {++ "description": "Subscription: the channel's owner (never the payer).",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "route": {++ "type": "string",++ "description": "Subscription: direct, or the site route."++ },++ "receiving_account": {++ "type": "string",++ "description": "Direct PowerChat subscription: the streamer's PowerChat username."++ },++ "auto_renew": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "success_url": {++ "type": "string"++ },++ "cancel_url": {++ "type": "string"++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/intent-result.v1.json",++ "title": "BillingIntentResult",++ "description": "billing.intent-result@1: answers of billing.intent.create on OpenVibe.Billing. POST /api/v1/intents → { intent, checkout_url } (null when the provider has no hosted checkout); GET /api/v1/intents/:id → { intent }; POST /api/v1/intents/:id/capture → { result, intent } (the settlement the provider's capture produced).",++ "type": "object",++ "required": [++ "intent"++ ],++ "properties": {++ "intent": {++ "$ref": "intent.v1.json"++ },++ "checkout_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "result": {++ "description": "Capture: what applying the provider's plan did."++ }++ }++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/intent.v1.json",++ "title": "BillingIntent",++ "description": "billing.intent@1: a checkout intent as OpenVibe.Billing presents it (server/ops/intents.js present): what a person is about to pay a provider for (a Vibes purchase or a channel subscription), priced by Billing.",++ "type": "object",++ "required": ["id", "provider", "provider_ref", "kind", "subject", "streamer", "amount_cents", "fee_cents", "bits", "route", "status", "settled_txn", "created_at"],++ "properties": {++ "id": { "type": "string", "pattern": "^pi_" },++ "provider": { "type": "string" },++ "provider_ref": { "type": ["string", "null"] },++ "kind": { "enum": ["purchase", "subscription"] },++ "subject": { "$ref": "../identity/subject-ref.v1.json" },++ "streamer": { "anyOf": [{ "$ref": "../identity/subject-ref.v1.json" }, { "type": "null" }] },++ "amount_cents": { "type": "integer", "minimum": 0 },++ "fee_cents": { "type": "integer", "minimum": 0 },++ "bits": { "type": "integer", "minimum": 0 },++ "route": { "type": ["string", "null"] },++ "auto_renew": { "type": ["integer", "boolean"] },++ "status": { "type": "string", "description": "created, settled, failed, …" },++ "settled_txn": { "type": ["string", "null"] },++ "created_at": { "type": "string" },++ "checkout_ref": { "type": "string", "description": "PowerChat only: pcorder:<id> or pcsub:<id>, echoed back by its webhook." }++ }++}nnew file mode 100644@@@ -0,0 +1,104 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/subscription-request.v1.json",++ "title": "BillingSubscriptionRequest",++ "description": "billing.subscription-request@1: the body of POST /api/v1/subscriptions on OpenVibe.Billing (billing.subscription.manage): pay one period of subscriber → streamer, from the subscriber's credit (source credit, the default) or from a provider receipt (source receipt, which also needs billing.ledger.admin: claiming money was received is operator grade). POST /api/v1/subscriptions/:id/cancel (cancels at the period end, with the provider too) and the GETs take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "subscriber",++ "streamer"++ ],++ "properties": {++ "subscriber": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "streamer": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "source": {++ "enum": [++ "credit",++ "receipt"++ ]++ },++ "price_cents": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Default: Billing's subscription price."++ },++ "auto_renew": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "receipt": {++ "type": "object",++ "required": [++ "provider",++ "provider_ref"++ ],++ "properties": {++ "provider": {++ "type": "string"++ },++ "provider_ref": {++ "type": "string"++ },++ "amount_cents": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Cents paid."++ },++ "fee_cents": {++ "type": [++ "integer",++ "string"++ ]++ },++ "route": {++ "type": "string"++ },++ "subscription_ref": {++ "type": "string"++ },++ "test": {++ "type": "boolean"++ }++ }++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,43 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/subscription-result.v1.json",++ "title": "BillingSubscriptionResult",++ "description": "billing.subscription-result@1: answers about subscriptions on OpenVibe.Billing. POST /api/v1/subscriptions (201) → { subscription, entitlement, transaction }; POST /api/v1/subscriptions/:id/cancel → { subscription, provider_sync }; GET /api/v1/subscriptions?subscriber=&streamer=&status= → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "subscription"++ ],++ "properties": {++ "subscription": {++ "$ref": "subscription.v1.json"++ },++ "entitlement": {++ "description": "billing.entitlement@1 (the answer to POST /subscriptions carries the entitlement as booked)."++ },++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "provider_sync": {++ "type": "string",++ "description": "not_needed, or how the provider was told."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "subscriptions"++ ],++ "properties": {++ "subscriptions": {++ "type": "array",++ "items": {++ "$ref": "subscription.v1.json"++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/subscription.v1.json",++ "title": "BillingSubscription",++ "description": "billing.subscription@1: one channel subscription as OpenVibe.Billing presents it (server/ops/subscriptions.js present).",++ "type": "object",++ "required": ["id", "subscriber", "streamer", "provider", "status", "auto_renew", "cancel_at_period_end", "current_period_end"],++ "properties": {++ "id": { "type": "string" },++ "subscriber": { "$ref": "../identity/subject-ref.v1.json" },++ "streamer": { "$ref": "../identity/subject-ref.v1.json" },++ "tier": {},++ "provider": { "type": "string", "description": "credit, or the provider that takes the payments." },++ "provider_ref": { "type": ["string", "null"] },++ "route": { "type": ["string", "null"], "description": "site or direct (paid to the streamer's own provider account)." },++ "status": { "type": "string", "description": "active, canceled, expired, …" },++ "auto_renew": { "type": "boolean" },++ "cancel_at_period_end": { "type": "boolean" },++ "price_cents": { "type": ["integer", "null"] },++ "current_period_end": { "type": ["string", "null"] },++ "created_at": { "type": "string" },++ "updated_at": { "type": ["string", "null"] }++ }++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/transaction.v1.json",++ "title": "BillingTransaction",++ "description": "billing.transaction@1: one journal transaction as OpenVibe.Billing's /api/v1 presents it (server/ledger.js present). Amounts are integers: bits for vibes-bits, cents for usd-cents; each transaction's entries balance per currency. Subjects are bare Network subject ids.",++ "type": "object",++ "required": ["id", "type", "status", "test", "reverses_txn", "from_subject", "to_subject", "provider", "receipt_ref", "created_at", "entries"],++ "properties": {++ "id": { "type": "string", "pattern": "^txn_" },++ "type": { "enum": ["purchase", "donation", "subscription", "subscription_share", "cashout_request", "cashout_paid", "cashout_denied", "recycle", "refund", "chargeback", "adjustment", "import"] },++ "status": { "type": "string" },++ "test": { "type": "boolean", "description": "Test money: never counted." },++ "reverses_txn": { "type": ["string", "null"] },++ "from_subject": { "type": ["string", "null"] },++ "to_subject": { "type": ["string", "null"] },++ "provider": { "type": ["string", "null"] },++ "receipt_ref": { "type": ["string", "null"], "description": "<provider>:<provider payment id> for money that arrived from a provider." },++ "actor": { "description": "Who posted it (as recorded)." },++ "metadata": { "description": "Per type: amount_bits, paid_cents, target EntityRef, message, rates…" },++ "created_at": { "type": "string" },++ "entries": {++ "type": "array",++ "items": {++ "type": "object",++ "required": ["account", "amount"],++ "properties": {++ "account": {++ "type": "object",++ "required": ["kind", "currency"],++ "properties": {++ "kind": { "enum": ["user_credit", "creator_payable", "provider_clearing", "platform_revenue", "payouts_pending", "refunds", "import_adjustment", "chargeback_loss", "fx_conversion"] },++ "owner": { "type": ["string", "null"], "description": "A subject id, a provider slug, or null for a platform account." },++ "currency": { "enum": ["vibes-bits", "usd-cents"] }++ }++ },++ "amount": { "type": "integer", "description": "Signed: negative leaves the account." }++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,91 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/transfer-request.v1.json",++ "title": "BillingTransferRequest",++ "description": "billing.transfer-request@1: bodies of billing.transfer.create on OpenVibe.Billing. POST /api/v1/transfers: { from, to, amount, kind?, target?, message? } moves amount bits from the sender's credit to the recipient's payable (a tip, donation or paid interaction; never to oneself). POST /api/v1/transfers/:id/refund: { amount?, reason? } gives a credit-funded donation back (all that remains refundable when amount is absent). Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "from",++ "to",++ "amount"++ ],++ "properties": {++ "from": {++ "description": "The sender (their credit pays).",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "to": {++ "description": "The recipient (their payable receives).",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits."++ },++ "kind": {++ "enum": [++ "tip",++ "donation",++ "paid_interaction"++ ],++ "description": "Default donation."++ },++ "target": {++ "$ref": "../common/entity-ref.v1.json",++ "description": "What the money is for (a stream, a paid message, …)."++ },++ "message": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500++ }++ }++ },++ {++ "type": "object",++ "properties": {++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits to give back; default all that remains refundable."++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,26 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/transfer-result.v1.json",++ "title": "BillingTransferResult",++ "description": "billing.transfer-result@1: answers of billing.transfer.create on OpenVibe.Billing (201). POST /api/v1/transfers → { transaction, balance: { credit } } (the sender's credit after it); POST /api/v1/transfers/:id/refund → { transaction } (type refund, reverses_txn the donation).",++ "type": "object",++ "required": [++ "transaction"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "balance": {++ "type": "object",++ "required": [++ "credit"++ ],++ "properties": {++ "credit": {++ "type": "integer"++ }++ }++ }++ }++}@@@ -817,6 +817,177 @@"status": "active","adr": "ADR-012"},++ {++ "id": "billing.admin-request",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/admin-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.admin-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/admin-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.balance-read-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/balance-read-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.balance",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/balance.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.cashout-decision",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/cashout-decision.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.cashout-request",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/cashout-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.cashout-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/cashout-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.cashout",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/cashout.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.entitlement-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/entitlement-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.entitlement",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/entitlement.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.intent-request",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/intent-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.intent-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/intent-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.intent",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/intent.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.subscription-request",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/subscription-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.subscription-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/subscription-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.subscription",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/subscription.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.transaction",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/transaction.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.transfer-request",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/transfer-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "billing.transfer-result",++ "version": "1.0.0",++ "owner": "billing",++ "schema": "billing/transfer-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "deals.watch.matched","version": "1.0.0",@@@ -1609,6 +1780,24 @@"status": "active","adr": "ADR-022"},++ {++ "id": "chat.send-request",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/send-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.send-result",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/send-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "live.moderation.action","version": "1.0.0",@@@ -1628,6 +1817,96 @@"compatibility": "backward","status": "active","adr": "ADR-022"++ },++ {++ "id": "community.paste-create-request",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/paste-create-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.paste-moderate-request",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/paste-moderate-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.paste-moderate-result",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/paste-moderate-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.paste-write-request",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/paste-write-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.paste-write-result",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/paste-write-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.paste",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/paste.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.moderation-request",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/moderation-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.moderation-result",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/moderation-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.post-write-request",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/post-write-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "community.pulse-write-result",++ "version": "1.0.0",++ "owner": "community",++ "schema": "community/pulse-write-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"}]}nnew file mode 100644@@@ -0,0 +1,89 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/send-request.v1.json",++ "title": "ChatSendRequest",++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "type",++ "message"++ ],++ "properties": {++ "type": {++ "const": "chat"++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 6000,++ "description": "The text (trimmed by Chat)."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."++ },++ "auto_delete_minutes": {++ "type": [++ "integer",++ "string"++ ],++ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."++ },++ "voiceChannelId": {++ "type": "string",++ "description": "The voice channel the sender is in, echoed on the broadcast."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ops"++ ],++ "properties": {++ "boot": {++ "type": "string",++ "maxLength": 64,++ "description": "Live's boot id; placeholder message ids are mapped per boot."++ },++ "ops": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "op"++ ],++ "properties": {++ "seq": {++ "type": "integer",++ "description": "Echoed on the result."++ },++ "op": {++ "type": "string",++ "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."++ },++ "args": {++ "type": "array"++ },++ "ref": {++ "type": "number",++ "description": "Live's placeholder id for the row this op inserts."++ },++ "key": {++ "type": "string",++ "maxLength": 80,++ "description": "Idempotency key: applied once."++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,200 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/send-result.v1.json",++ "title": "ChatSendResult",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "type",++ "username",++ "role",++ "message",++ "stream_id",++ "channel_user_id",++ "is_global",++ "timestamp"++ ],++ "properties": {++ "type": {++ "const": "chat"++ },++ "id": {++ "type": "integer",++ "description": "The stored message id."++ },++ "username": {++ "type": "string",++ "description": "Display name, or the anonymous id."++ },++ "core_username": {++ "type": [++ "string",++ "null"++ ],++ "description": "The immutable login handle; null for an anonymous sender."++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "role": {++ "type": "string",++ "description": "The sender's role, anon for an anonymous sender."++ },++ "message": {++ "type": "string"++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "is_global": {++ "type": "boolean",++ "description": "true for homepage global chat (no stream, no channel)."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": "string"++ },++ "filtered": {++ "type": "boolean"++ },++ "timestamp": {++ "type": "string",++ "format": "date-time"++ },++ "auto_delete_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "voiceChannelId": {++ "type": "string"++ },++ "reply_to": {++ "type": "object",++ "required": [++ "id",++ "username",++ "message"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "username": {++ "type": "string"++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "message": {++ "type": "string",++ "description": "The parent's text, cut to 100 characters."++ }++ }++ },++ "nameFX": {++ "description": "Cosmetic name effect."++ },++ "particleFX": {++ "description": "Cosmetic particle effect."++ },++ "hatFX": {++ "description": "Cosmetic hat."++ },++ "voiceFX": {++ "description": "Cosmetic TTS voice effect."++ },++ "tag": {++ "description": "The sender's equipped tag."++ },++ "vip_badge": {++ "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "message"++ ],++ "properties": {++ "type": {++ "enum": [++ "system",++ "error"++ ]++ },++ "message": {++ "type": "string"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok",++ "results"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "results": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "ok"++ ],++ "properties": {++ "seq": {++ "type": "integer"++ },++ "ok": {++ "type": "boolean"++ },++ "result": {},++ "code": {++ "type": "string"++ },++ "error": {++ "type": "string"++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,50 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",++ "title": "CommunityModerationRequest",++ "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",++ "anyOf": [++ {++ "type": "object",++ "required": ["visibility"],++ "properties": { "visibility": { "enum": ["public", "hidden", "locked"] } }++ },++ {++ "type": "object",++ "anyOf": [++ { "required": ["pinned"] },++ { "required": ["locked"] }++ ],++ "properties": {++ "pinned": { "description": "Truthy pins the thread." },++ "locked": { "description": "Truthy locks it (announced as thread.locked / thread.unlocked)." }++ }++ },++ {++ "type": "object",++ "anyOf": [++ { "required": ["body"] },++ { "required": ["body_markdown"] }++ ],++ "properties": {++ "body": { "type": "string", "minLength": 1, "maxLength": 40000 },++ "body_markdown": { "type": "string", "minLength": 1, "maxLength": 40000 }++ }++ },++ {++ "type": "object",++ "required": ["space", "webhook_url_ref"],++ "properties": {++ "space": { "type": "string", "description": "The space's slug." },++ "webhook_url_ref": { "type": "string", "pattern": "^[A-Z0-9_]+$" },++ "enabled": { "type": "boolean", "description": "Default true." }++ }++ },++ {++ "type": "object",++ "required": ["enabled"],++ "properties": { "enabled": { "type": "boolean" } }++ },++ { "$ref": "../common/no-body.v1.json" }++ ]++}nnew file mode 100644@@@ -0,0 +1,97 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",++ "title": "CommunityModerationResult",++ "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",++ "anyOf": [++ {++ "type": "object",++ "required": ["thread"],++ "properties": {++ "thread": {++ "type": "object",++ "required": ["id"],++ "properties": {++ "id": { "type": ["integer", "string"] },++ "access_id": { "type": "string", "description": "A comment thread's browser id (cth_…)." },++ "ref": { "$ref": "../common/entity-ref.v1.json" },++ "visibility": { "type": "string" },++ "comment_count": { "type": ["integer", "null"] },++ "space": { "type": "string" },++ "slug": { "type": "string" },++ "title": { "type": "string" },++ "pinned": { "type": "boolean" },++ "locked": { "type": "boolean" }++ }++ }++ }++ },++ {++ "type": "object",++ "required": ["post"],++ "properties": {++ "post": {++ "type": "object",++ "required": ["id", "thread_id", "revision", "deleted"],++ "properties": {++ "id": { "type": "integer" },++ "thread_id": { "type": "integer" },++ "is_opening": { "type": "boolean" },++ "origin": { "type": "string" },++ "author": { "type": ["object", "null"] },++ "body_markdown": { "type": ["string", "null"] },++ "body_html": { "type": ["string", "null"] },++ "revision": { "type": "integer" },++ "deleted": { "type": "boolean" },++ "created_at": { "type": ["string", "null"] },++ "updated_at": { "type": ["string", "null"] },++ "can_edit": { "type": "boolean" },++ "attachments": { "type": "array" },++ "pastes": { "type": "array" }++ }++ }++ }++ },++ {++ "type": "object",++ "required": ["ok"],++ "properties": {++ "ok": { "const": true },++ "id": { "type": "integer" },++ "deleted": { "const": "thread" }++ }++ },++ {++ "type": "object",++ "required": ["enabled"],++ "anyOf": [++ { "required": ["deliveries"] },++ { "required": ["mappings"] }++ ],++ "properties": {++ "enabled": { "type": "boolean" },++ "deliveries": { "type": "array", "items": { "type": "object" } },++ "mappings": { "type": "array", "items": { "type": "object" } }++ }++ },++ {++ "type": "object",++ "required": ["mapping"],++ "properties": {++ "mapping": {++ "type": "object",++ "required": ["id", "space", "webhook_url_ref", "webhook_configured", "enabled"],++ "properties": {++ "id": { "type": "integer" },++ "space": { "type": "string" },++ "direction": { "type": "string" },++ "webhook_url_ref": { "type": "string" },++ "webhook_configured": { "type": "boolean" },++ "enabled": { "type": "boolean" },++ "created_at": { "type": ["string", "null"] }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,28 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",++ "title": "CommunityPasteCreateRequest",++ "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.",++ "type": "object",++ "anyOf": [++ { "required": ["content"] },++ { "required": ["screenshot"] }++ ],++ "properties": {++ "content": { "type": "string", "minLength": 1, "description": "The text (trimmed)." },++ "screenshot": { "type": "string", "contentEncoding": "binary", "description": "Multipart only: the image part." },++ "title": { "type": ["string", "null"], "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank." },++ "language": { "type": "string", "description": "A language hint; auto or absent sniffs it from the content." },++ "visibility": { "type": "string", "description": "public (default), unlisted or private." },++ "burn_after_read": { "type": ["boolean", "integer", "string"], "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else." },++ "is_nsfw": { "type": ["boolean", "integer", "string"] },++ "stream_id": { "type": ["integer", "string"], "description": "The Live stream it was made during (digits)." },++ "description": { "type": "string", "description": "Multipart screenshot: the text shown under the image." },++ "page_url": { "type": "string", "description": "Multipart screenshot: the page it shows." },++ "user_agent": { "type": "string", "description": "Multipart screenshot: the capturing browser." },++ "slug": { "type": "string", "pattern": "^[A-Za-z0-9_-]{3,80}$", "description": "Service callers, public pastes only: the slug to use (409 if taken)." },++ "metadata": { "type": ["object", "string"], "description": "Service callers only: a JSON object (at most 16 KB)." },++ "ai_summary": { "type": "string", "description": "Service callers only (cut to 2000 characters)." },++ "ai_tags": { "description": "Service callers only, with ai_summary: JSON text or a value to serialise." }++ }++}nnew file mode 100644@@@ -0,0 +1,48 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",++ "title": "CommunityPasteModerateRequest",++ "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "slugs",++ "action"++ ],++ "properties": {++ "slugs": {++ "type": "array",++ "minItems": 1,++ "items": {++ "type": "string"++ }++ },++ "action": {++ "enum": [++ "delete",++ "public",++ "unlisted",++ "private"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "screenshot"++ ],++ "properties": {++ "screenshot": {++ "type": "string",++ "contentEncoding": "binary",++ "description": "The multipart part named screenshot: the censored image."++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",++ "title": "CommunityPasteModerateResult",++ "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",++ "anyOf": [++ { "type": "object", "required": ["done", "skipped"], "properties": { "done": { "type": "integer", "minimum": 0 }, "skipped": { "type": "integer", "minimum": 0 } } },++ { "type": "object", "required": ["paste"], "properties": { "paste": { "$ref": "paste.v1.json" } } },++ { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }++ ]++}nnew file mode 100644@@@ -0,0 +1,72 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",++ "title": "CommunityPasteWriteRequest",++ "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",++ "anyOf": [++ {++ "type": "object",++ "minProperties": 1,++ "properties": {++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "content": {++ "type": "string"++ },++ "language": {++ "type": "string"++ },++ "visibility": {++ "type": "string",++ "description": "public, unlisted or private (anything else is public)."++ },++ "is_nsfw": {++ "type": [++ "boolean",++ "integer",++ "string"++ ]++ },++ "pinned": {++ "type": [++ "boolean",++ "integer",++ "string"++ ],++ "description": "Staff only; ignored otherwise."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2000++ },++ "parent_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "The top-level comment this replies to."++ },++ "anon_name": {++ "type": "string",++ "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,48 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",++ "title": "CommunityPasteWriteResult",++ "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",++ "anyOf": [++ {++ "type": "object",++ "required": ["paste"],++ "properties": {++ "id": { "type": "integer" },++ "slug": { "type": "string" },++ "url": { "type": "string" },++ "paste": { "$ref": "paste.v1.json" }++ }++ },++ { "type": "object", "required": ["success"], "properties": { "success": { "const": true } } },++ { "type": "object", "required": ["liked", "likes"], "properties": { "liked": { "type": "boolean" }, "likes": { "type": "integer", "minimum": 0 } } },++ { "type": "object", "required": ["copies"], "properties": { "copies": { "type": "integer", "minimum": 0 } } },++ {++ "type": "object",++ "required": ["comment"],++ "properties": {++ "comment": {++ "type": "object",++ "required": ["id", "paste_id", "message", "created_at"],++ "properties": {++ "id": { "type": "integer" },++ "paste_id": { "type": "integer" },++ "user_id": { "type": ["string", "null"], "description": "The author's subject; null for an anonymous comment." },++ "author_subject": { "type": ["string", "null"] },++ "parent_id": { "type": ["integer", "null"] },++ "anon_name": { "type": ["string", "null"] },++ "message": { "type": "string" },++ "is_deleted": { "type": ["integer", "boolean"] },++ "created_at": { "type": "string" },++ "updated_at": { "type": ["string", "null"] },++ "username": { "type": ["string", "null"] },++ "display_name": { "type": ["string", "null"] },++ "avatar_url": { "type": ["string", "null"] },++ "profile_color": { "type": ["string", "null"] }++ }++ }++ }++ },++ { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }++ ]++}nnew file mode 100644@@@ -0,0 +1,46 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste.v1.json",++ "title": "CommunityPaste",++ "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).",++ "type": "object",++ "required": ["id", "app_id", "slug", "user_id", "owner_subject", "origin", "type", "title", "visibility", "url", "raw_url", "created_at", "is_owner"],++ "properties": {++ "id": { "type": "integer" },++ "app_id": { "const": "community" },++ "slug": { "type": "string" },++ "user_id": { "type": ["string", "null"], "description": "The owner's subject (usr_…), or null for an anonymous paste." },++ "owner_subject": { "type": ["string", "null"] },++ "origin": { "type": "string", "description": "user, or ai for a paste an AI service wrote." },++ "type": { "enum": ["paste", "screenshot"] },++ "title": { "type": "string" },++ "content": { "type": ["string", "null"] },++ "language": { "type": ["string", "null"] },++ "visibility": { "enum": ["public", "unlisted", "private"] },++ "stream_id": { "type": ["integer", "null"], "description": "The Live stream it was made during, when its stream_ref is one." },++ "stream_ref": { "type": ["object", "null"], "description": "An EntityRef of where it was made (a Live stream, or a service's own source)." },++ "screenshot_url": { "type": ["string", "null"] },++ "metadata": { "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details." },++ "burn_after_read": { "type": "boolean" },++ "forked_from": { "type": ["integer", "null"] },++ "pinned": { "type": "boolean" },++ "views": { "type": "integer", "minimum": 0 },++ "unique_views": { "type": "integer", "minimum": 0 },++ "copies": { "type": "integer", "minimum": 0 },++ "likes": { "type": "integer", "minimum": 0 },++ "is_nsfw": { "type": "boolean" },++ "ai_summary": { "type": ["string", "null"] },++ "ai_tags": { "type": ["string", "null"], "description": "JSON text as the AI pass wrote it." },++ "ai_analyzed_at": { "type": ["string", "null"] },++ "revision": { "type": "integer" },++ "url": { "type": "string", "description": "/p/<slug>" },++ "raw_url": { "type": "string", "description": "/p/<slug>/raw" },++ "created_at": { "type": "string" },++ "updated_at": { "type": ["string", "null"] },++ "is_owner": { "type": "boolean", "description": "Whether the caller owns it." },++ "username": { "type": ["string", "null"] },++ "display_name": { "type": ["string", "null"], "description": "OpenVibe AI for an AI paste." },++ "avatar_url": { "type": ["string", "null"] },++ "profile_color": { "type": ["string", "null"] }++ }++}nnew file mode 100644@@@ -0,0 +1,49 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",++ "title": "CommunityPostWriteRequest",++ "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",++ "anyOf": [++ {++ "type": "object",++ "anyOf": [++ { "required": ["body"] },++ { "required": ["body_markdown"] }++ ],++ "properties": {++ "title": { "type": "string", "description": "A new thread's title." },++ "body": { "type": "string", "minLength": 1, "maxLength": 40000, "description": "Markdown." },++ "body_markdown": { "type": "string", "minLength": 1, "maxLength": 40000, "description": "Read when body is absent." },++ "members_only": {++ "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",++ "anyOf": [++ { "type": ["boolean", "null"] },++ { "type": "string", "pattern": "^usr_" },++ { "type": "object", "required": ["owner"], "properties": { "owner": { "type": "string", "pattern": "^usr_" } } }++ ]++ },++ "category": { "type": "string", "description": "A new thread only: the slug of one of the space's categories." },++ "attachments": {++ "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",++ "anyOf": [++ { "type": "string", "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$" },++ { "type": "array", "maxItems": 4, "items": { "type": "string", "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$" } }++ ]++ },++ "pastes": {++ "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",++ "type": ["array", "string"],++ "items": { "type": "string" }++ }++ }++ },++ {++ "type": "object",++ "required": ["value"],++ "properties": {++ "value": { "enum": [1, -1, 0, "1", "-1", "0"], "description": "A thread vote; 0 takes the vote back." }++ }++ },++ { "$ref": "../common/no-body.v1.json" }++ ]++}nnew file mode 100644@@@ -0,0 +1,51 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",++ "title": "CommunityPulseWriteResult",++ "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",++ "anyOf": [++ {++ "type": "object",++ "required": ["item", "created"],++ "properties": {++ "created": { "type": "boolean" },++ "item": {++ "type": "object",++ "required": ["id", "source", "title", "url", "origin", "label", "actor", "occurred_at"],++ "properties": {++ "id": { "type": "integer" },++ "source": {++ "type": "object",++ "required": ["service", "type", "id"],++ "properties": {++ "service": { "type": "string" },++ "type": { "type": "string" },++ "id": { "type": "string" }++ }++ },++ "title": { "type": "string", "maxLength": 300 },++ "url": { "type": "string" },++ "origin": { "enum": ["user", "ai", "system"] },++ "label": { "enum": ["AI", "System", null] },++ "actor": {++ "type": ["object", "null"],++ "properties": {++ "subject": { "type": ["string", "null"] },++ "username": { "type": ["string", "null"] },++ "display_name": { "type": ["string", "null"] },++ "avatar_url": { "type": ["string", "null"] },++ "profile_color": { "type": ["string", "null"] }++ }++ },++ "occurred_at": { "type": ["string", "null"] }++ }++ }++ }++ },++ {++ "type": "object",++ "required": ["removed"],++ "properties": { "removed": { "type": "integer", "minimum": 0, "maximum": 1 } }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1 @@++{ "on": "yes" }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": { "kind": "wallet", "currency": "vibes-bits" }, "to": { "kind": "user_credit", "currency": "vibes-bits" }, "amount": 1, "reason": "x" }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": { "kind": "platform_revenue", "owner": null, "currency": "vibes-bits" }, "to": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": 250, "reason": "goodwill after the outage" }nnew file mode 100644@@@ -0,0 +1 @@++{ "on": true, "reason": "provider incident" }nnew file mode 100644@@@ -0,0 +1 @@++{ "renewed": "all" }nnew file mode 100644@@@ -0,0 +1 @@++{ "renewed": ["sub_01JAB2C3D4E5F6G7H8J9K0MNPW"], "expired": [], "canceled": [], "skipped": 0 }nnew file mode 100644@@@ -0,0 +1 @@++{ "frozen": false, "reason": null, "frozen_at": null, "frozen_by": null, "processed_after_unfreeze": { "processed": 2 } }nnew file mode 100644@@@ -0,0 +1 @@++{ "transactions": [ { "id": "txn_1" } ], "next_cursor": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "vibes-bits", "credit": 1200, "payable": 5500, "pending_payouts": 0, "payable_value_cents": 5500, "bits_per_usd": 100 }nnew file mode 100644@@@ -0,0 +1 @@++{ "transactions": [ { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] } ], "next_cursor": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "usd-cents", "credit": 1, "payable": 0, "pending_payouts": 0, "payable_value_cents": 0, "bits_per_usd": 100 }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "vibes-bits", "credit": 1200, "payable": 5500, "pending_payouts": 0, "payable_value_cents": 5500, "bits_per_usd": 100 }nnew file mode 100644@@@ -0,0 +1 @@++{ "payout_reference": "" }nnew file mode 100644@@@ -0,0 +1 @@++{ "payout_reference": "PAYID-M5A7", "payout_provider": "paypal" }nnew file mode 100644@@@ -0,0 +1 @@++{ "reason": "address bounced" }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": 0 }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": 5000, "payout_method": { "type": "paypal", "address": "[email protected]" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": "1500" }nnew file mode 100644@@@ -0,0 +1 @@++{ "cashouts": [ { "id": "co_1" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "cashout": { "id": "co_01JAB2C3D4E5F6G7H8J9K0MNPT", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "amount_bits": 5000, "value_cents": 5000, "status": "requested", "payout_method": { "type": "paypal", "address": "[email protected]" }, "escrow_until": "2026-10-02T18:00:00.000Z", "request_txn": "txn_01JAB2C3D4E5F6G7H8J9K0MNPV", "settle_txn": null, "payout_provider": null, "payout_reference": null, "reason": null, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "transaction": { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] }, "balance": { "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "vibes-bits", "credit": 1200, "payable": 5500, "pending_payouts": 0, "payable_value_cents": 5500, "bits_per_usd": 100 } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "co_1", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "amount_bits": 0, "value_cents": 0, "status": "requested", "payout_method": null, "escrow_until": "x", "request_txn": "t", "created_at": "x" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "co_01JAB2C3D4E5F6G7H8J9K0MNPT", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "amount_bits": 5000, "value_cents": 5000, "status": "requested", "payout_method": { "type": "paypal", "address": "[email protected]" }, "escrow_until": "2026-10-02T18:00:00.000Z", "request_txn": "txn_01JAB2C3D4E5F6G7H8J9K0MNPV", "settle_txn": null, "payout_provider": null, "payout_reference": null, "reason": null, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "entitlements": [] }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "entitlements": [ { "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": "yes", "expires_at": null, "subscription": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "provider": "stripe", "kind": "gift", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }nnew file mode 100644@@@ -0,0 +1 @@++{ "provider": "powerchat", "kind": "subscription", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "route": "direct", "receiving_account": "alice" }nnew file mode 100644@@@ -0,0 +1 @@++{ "provider": "stripe", "kind": "purchase", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "bits": 1000, "success_url": "https://openvibe.live/wallet?ok=1" }nnew file mode 100644@@@ -0,0 +1 @@++{ "checkout_url": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "intent": { "id": "pi_01JAB2C3D4E5F6G7H8J9K0MNPX", "provider": "stripe", "provider_ref": "cs_test_a1", "kind": "purchase", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": null, "amount_cents": 999, "fee_cents": 0, "bits": 1000, "route": null, "auto_renew": 0, "status": "created", "settled_txn": null, "created_at": "2026-09-25T18:00:00.000Z" }, "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_a1" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "pi_01JAB2C3D4E5F6G7H8J9K0MNPX", "provider": "stripe", "provider_ref": "cs_test_a1", "kind": "donation", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": null, "amount_cents": 999, "fee_cents": 0, "bits": 1000, "route": null, "auto_renew": 0, "status": "created", "settled_txn": null, "created_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "pi_01JAB2C3D4E5F6G7H8J9K0MNPX", "provider": "stripe", "provider_ref": "cs_test_a1", "kind": "purchase", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": null, "amount_cents": 999, "fee_cents": 0, "bits": 1000, "route": null, "auto_renew": 0, "status": "created", "settled_txn": null, "created_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "subscriber": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": "gift" }nnew file mode 100644@@@ -0,0 +1 @@++{ "subscriber": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": "credit", "auto_renew": true }nnew file mode 100644@@@ -0,0 +1 @@++{ "subscription": { "id": "sub_1" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "subscriber": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "tier": 1, "provider": "credit", "provider_ref": null, "route": null, "status": "active", "auto_renew": true, "cancel_at_period_end": false, "price_cents": 499, "current_period_end": "2026-10-25T18:00:00.000Z", "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }, "provider_sync": "not_needed" }nnew file mode 100644@@@ -0,0 +1 @@++{ "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "subscriber": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "tier": 1, "provider": "credit", "provider_ref": null, "route": null, "status": "active", "auto_renew": true, "cancel_at_period_end": false, "price_cents": 499, "current_period_end": "2026-10-25T18:00:00.000Z", "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }, "entitlement": { "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } }, "transaction": { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "sub_1", "subscriber": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "provider": "credit", "status": "active", "auto_renew": true, "cancel_at_period_end": false, "current_period_end": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "subscriber": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "tier": 1, "provider": "credit", "provider_ref": null, "route": null, "status": "active", "auto_renew": true, "cancel_at_period_end": false, "price_cents": 499, "current_period_end": "2026-10-25T18:00:00.000Z", "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "txn_1", "type": "gift", "status": "posted", "test": false, "reverses_txn": null, "from_subject": null, "to_subject": null, "provider": null, "receipt_ref": null, "created_at": "x", "entries": [] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": "gst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": "0", "kind": "bribe" }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": -5 }nnew file mode 100644@@@ -0,0 +1 @@++{ "amount": 200, "reason": "sent twice" }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "to": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": 500, "kind": "tip", "target": { "service": "live", "type": "stream", "id": "812" }, "message": "gg" }nnew file mode 100644@@@ -0,0 +1 @@++{ "balance": { "credit": 700 } }nnew file mode 100644@@@ -0,0 +1 @@++{ "transaction": { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] }, "balance": { "credit": 700 } }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "chat", "message": "" }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "chat", "text": "hello" }nnew file mode 100644@@@ -0,0 +1 @@++{ "boot": "b-1727300000000", "ops": [{ "seq": 1, "op": "broadcastToStream", "args": [42, { "type": "chat", "username": "AI Viewer", "message": "hello chat" }] }] }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "chat", "message": "gg that was clean", "reply_to_id": 81234, "auto_delete_minutes": 30 }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "chat", "username": "Alice", "message": "hi" }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "notice", "message": "x" }nnew file mode 100644@@@ -0,0 +1 @@++{ "ok": true, "results": [{ "seq": 1, "ok": true, "result": null }, { "seq": 2, "ok": false, "code": "capability.denied", "error": "chat.message.send not granted" }] }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "chat", "id": 81240, "username": "Alice", "core_username": "alice", "user_id": 17, "anon_id": null, "role": "user", "message": "gg that was clean", "stream_id": 42, "channel_user_id": 5, "is_global": false, "avatar_url": "https://openvibe.media/avatar/alice", "profile_color": "#3b82f6", "filtered": false, "timestamp": "2026-09-25T20:14:03.120Z", "auto_delete_at": null, "reply_to": { "id": 81234, "username": "bob", "user_id": 9, "message": "did you see that" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "type": "system", "message": "Slow down! You are sending messages too fast." }nnew file mode 100644@@@ -0,0 +1 @@++{ "visibility": "deleted" }nnew file mode 100644@@@ -0,0 +1 @@++{ "space": "announcements", "webhook_url_ref": "https://discord.com/api/webhooks/1/abc" }nnew file mode 100644@@@ -0,0 +1 @@++{ "visibility": "locked" }nnew file mode 100644@@@ -0,0 +1 @@++{ "pinned": true }nnew file mode 100644@@@ -0,0 +1 @@++{ "space": "announcements", "webhook_url_ref": "DISCORD_WEBHOOK_ANNOUNCEMENTS" }nnew file mode 100644@@@ -0,0 +1 @@++{ "ok": false }nnew file mode 100644@@@ -0,0 +1 @@++{ "thread": { "id": 311, "access_id": "cth_01JAB2C3D4E5F6G7H8J9K0MNPQ", "ref": { "service": "live", "type": "vod", "id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "visibility": "locked", "comment_count": 14, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "mapping": { "id": 3, "space": "announcements", "direction": "out", "webhook_url_ref": "DISCORD_WEBHOOK_ANNOUNCEMENTS", "webhook_configured": true, "enabled": true, "created_at": "2026-09-25T18:00:00.000Z" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "ok": true, "id": 77, "deleted": "thread" }nnew file mode 100644@@@ -0,0 +1 @@++{ "content": "", "title": "blank" }nnew file mode 100644@@@ -0,0 +1 @@++{ "title": "no content", "visibility": "public" }nnew file mode 100644@@@ -0,0 +1 @@++{ "screenshot": "<bytes>", "title": "Bug on the dashboard", "description": "the chart overflows", "page_url": "https://openvibe.live/dashboard" }nnew file mode 100644@@@ -0,0 +1 @@++{ "title": "ffmpeg restream flags", "content": "ffmpeg -re -i in.flv -c copy -f flv rtmp://example/live", "language": "auto", "visibility": "unlisted", "stream_id": "812" }nnew file mode 100644@@@ -0,0 +1 @@++{ "slugs": ["k3JfQ2"], "action": "archive" }nnew file mode 100644@@@ -0,0 +1 @@++{ "slugs": ["k3JfQ2", "a8bC9d"], "action": "unlisted" }nnew file mode 100644@@@ -0,0 +1 @@++{ "screenshot": "<bytes>" }nnew file mode 100644@@@ -0,0 +1 @@++{ "done": -1, "skipped": 0 }nnew file mode 100644@@@ -0,0 +1 @@++{ "done": 2, "skipped": 0 }nnew file mode 100644@@@ -0,0 +1 @@++{ "message": "Comment deleted" }nnew file mode 100644@@@ -0,0 +1 @@++{ "title": 42 }nnew file mode 100644@@@ -0,0 +1 @@++{ "message": "this saved me an hour", "parent_id": 91 }nnew file mode 100644@@@ -0,0 +1 @@++{}nnew file mode 100644@@@ -0,0 +1 @@++{ "title": "ffmpeg restream flags (fixed)", "visibility": "private" }nnew file mode 100644@@@ -0,0 +1 @@++{ "liked": "yes" }nnew file mode 100644@@@ -0,0 +1 @@++{ "comment": { "id": 93, "paste_id": 4412, "user_id": null, "author_subject": null, "parent_id": null, "anon_name": "Anonymous", "message": "nice", "is_deleted": 0, "created_at": "2026-09-25 18:05:00", "updated_at": null, "username": null, "display_name": null, "avatar_url": null, "profile_color": null } }nnew file mode 100644@@@ -0,0 +1 @@++{ "liked": true, "likes": 6 }nnew file mode 100644@@@ -0,0 +1 @@++{ "paste": { "unique_views": 12, "id": 4412, "app_id": "community", "slug": "k3JfQ2", "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "owner_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "origin": "user", "type": "paste", "title": "ffmpeg restream flags", "content": "ffmpeg -re -i in.flv -c copy -f flv rtmp://example/live", "language": "text", "visibility": "public", "stream_id": 812, "stream_ref": { "service": "live", "type": "stream", "id": "812" }, "screenshot_url": null, "metadata": null, "burn_after_read": false, "forked_from": null, "pinned": false, "views": 30, "copies": 2, "likes": 5, "is_nsfw": false, "ai_summary": null, "ai_tags": null, "ai_analyzed_at": null, "revision": 1, "url": "/p/k3JfQ2", "raw_url": "/p/k3JfQ2/raw", "created_at": "2026-09-25 18:02:11", "updated_at": "2026-09-25 18:02:11", "is_owner": true, "username": "alice", "display_name": "Alice", "avatar_url": "https://openvibe.media/avatar/alice", "profile_color": "#3b82f6" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": 4412, "app_id": "media", "slug": "k3JfQ2", "user_id": null, "owner_subject": null, "origin": "user", "type": "paste", "title": "x", "visibility": "public", "url": "/p/k3JfQ2", "raw_url": "/p/k3JfQ2/raw", "created_at": "2026-09-25 18:02:11", "is_owner": false }nnew file mode 100644@@@ -0,0 +1 @@++{ "unique_views": 12, "id": 4412, "app_id": "community", "slug": "k3JfQ2", "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "owner_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "origin": "user", "type": "paste", "title": "ffmpeg restream flags", "content": "ffmpeg -re -i in.flv -c copy -f flv rtmp://example/live", "language": "text", "visibility": "public", "stream_id": 812, "stream_ref": { "service": "live", "type": "stream", "id": "812" }, "screenshot_url": null, "metadata": null, "burn_after_read": false, "forked_from": null, "pinned": false, "views": 30, "copies": 2, "likes": 5, "is_nsfw": false, "ai_summary": null, "ai_tags": null, "ai_analyzed_at": null, "revision": 1, "url": "/p/k3JfQ2", "raw_url": "/p/k3JfQ2/raw", "created_at": "2026-09-25 18:02:11", "updated_at": "2026-09-25 18:02:11", "is_owner": true, "username": "alice", "display_name": "Alice", "avatar_url": "https://openvibe.media/avatar/alice", "profile_color": "#3b82f6" }nnew file mode 100644@@@ -0,0 +1 @@++{ "body": "x", "attachments": ["med_1", "med_2"] }nnew file mode 100644@@@ -0,0 +1 @@++{ "value": 2 }nnew file mode 100644@@@ -0,0 +1 @@++{ "title": "Restreaming to three platforms at once", "body": "What bitrates do you use?", "category": "help", "pastes": ["k3JfQ2"] }nnew file mode 100644@@@ -0,0 +1 @@++{ "body_markdown": "6000 kbps works for me", "attachments": ["med_01JAB2C3D4E5F6G7H8J9K0MNPQ"] }nnew file mode 100644@@@ -0,0 +1 @@++{ "value": -1 }nnew file mode 100644@@@ -0,0 +1 @@++{ "created": true, "item": { "id": 1, "title": "x", "url": "https://x", "origin": "robot" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "created": true, "item": { "id": 9001, "source": { "service": "live", "type": "clip", "id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "title": "Alice clipped a clean 1v3", "url": "https://openvibe.live/clip/med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "origin": "user", "label": null, "actor": { "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "username": "alice", "display_name": "Alice", "avatar_url": null, "profile_color": null }, "occurred_at": "2026-09-25T18:00:00.000Z" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "removed": 1 }@@@ -8338,1907 +8338,3573 @@}}},-- "deals.watch.matched": {++ "billing.admin-request": {"version": "1.0.0",-- "owner": "deals",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.watch.matched.v1.json",-- "title": "DealsWatchMatchedPayload",-- "description": "deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.",-- "type": "object",-- "required": [-- "watch_id",-- "recipient",-- "kind",-- "query",-- "product_id",-- "max_price",-- "currency",-- "offer_id",-- "offer_url",-- "title",-- "observation"-- ],-- "additionalProperties": false,-- "properties": {-- "watch_id": {-- "type": "string",-- "pattern": "^dwt_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "recipient": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The watch owner's user subject id: who is notified."-- },-- "kind": {-- "enum": [-- "keyword",-- "product",-- "price_below"-- ]-- },-- "query": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "Keywords of a keyword watch, or of a price-below watch without a product."-- },-- "product_id": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 64,-- "description": "Deals product id of a product or price-below watch."-- },-- "max_price": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 40,-- "description": "Price-below threshold as entered (decimal text, e.g. 19.99)."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217 currency of max_price."-- },-- "offer_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64,-- "description": "The canonical (root) offer id (dof_…)."-- },-- "offer_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical offer page URL on Deals."-- },-- "title": {-- "type": "string",-- "minLength": 1,-- "maxLength": 500-- },-- "observation": {++ "$id": "https://openvibe.network/contracts/billing/admin-request.v1.json",++ "title": "BillingAdminRequest",++ "description": "billing.admin-request@1: bodies of billing.ledger.admin on OpenVibe.Billing (operators). POST /api/v1/admin/freeze: { on, reason? } (unfreezing processes the provider deliveries stored meanwhile; allowed while frozen). POST /api/v1/admin/adjustments: { from, to, amount, reason, relates_to? } moves value between two accounts of one currency. POST /api/v1/purchases/settle: { provider, provider_ref, subject?, amount_cents, bits, intent_id?, test? } books a provider receipt. POST /api/v1/admin/provider-events/:id/reprocess, POST /api/v1/admin/sweep and the GETs (freeze, reconcile, provider-events, import-holds) take no body. Adjustments and settlements need an Idempotency-Key and are refused while frozen.",++ "anyOf": [++ {"type": "object","required": [-- "id",-- "observed_at",-- "price",-- "currency",-- "availability"++ "on"],-- "additionalProperties": false,"properties": {-- "id": {-- "type": "string",-- "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "observed_at": {-- "type": "string",-- "format": "date-time"-- },-- "price": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 40,-- "description": "Decimal text as stated; null when the observation stated no price."++ "on": {++ "type": "boolean"},-- "currency": {++ "reason": {"type": ["string","null"-- ],-- "pattern": "^[A-Z]{3}$"-- },-- "availability": {-- "enum": [-- "in_stock",-- "out_of_stock",-- "preorder",-- "discontinued",-- "limited",-- "sold_out",-- "online_only",-- "in_store_only",-- null]}}-- }-- }-- }-- },-- "trade.alert.triggered": {-- "version": "1.0.0",-- "owner": "trade",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.alert.triggered.v1.json",-- "title": "TradeAlertTriggeredPayload",-- "description": "trade.alert.triggered v1 (OpenVibe.Trade server/domain/alerts.js deliver). One delivery of one private alert rule: a threshold crossed by a new observation (trigger kind observation), or a newly seen document of the instrument (filing_type / new_document rules, trigger kind document). Written in the transaction that recorded the observation or document; at most one per (rule, trigger). Envelope: subject { type: user, id: <rule owner usr_…> }, visibility subject, priority important, actor service:trade. Data only, never rendered notification text (ADR-020): OpenVibe.Network renders a TRADE_ALERT notification (server/notifications/events-consumer.js). Information only, not investment advice.",-- "type": "object",-- "required": [-- "delivery_id",-- "rule",-- "instrument",-- "trigger",-- "disclaimer"-- ],-- "additionalProperties": false,-- "properties": {-- "delivery_id": {-- "type": "string",-- "pattern": "^ald_[0-9A-HJKMNP-TV-Z]{26}$"},-- "rule": {++ {"type": "object",-- "additionalProperties": false,"required": [-- "id",-- "kind",-- "status",-- "instrument",-- "metric",-- "operator",-- "threshold",-- "unit",-- "currency",-- "form_types",-- "armed",-- "created_at",-- "updated_at"++ "from",++ "to",++ "amount",++ "reason"],"properties": {-- "id": {-- "type": "string",-- "pattern": "^alr_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "kind": {-- "enum": [-- "threshold",-- "filing_type",-- "new_document"-- ]-- },-- "status": {-- "enum": [-- "active",-- "deleted"-- ]-- },-- "instrument": {++ "from": {"type": "object","required": [-- "id"++ "kind",++ "currency"],-- "additionalProperties": false,"properties": {-- "id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ "kind": {++ "enum": [++ "user_credit",++ "creator_payable",++ "provider_clearing",++ "platform_revenue",++ "payouts_pending",++ "refunds",++ "import_adjustment",++ "chargeback_loss",++ "fx_conversion"++ ]},-- "symbol": {-- "type": "string"++ "owner": {++ "type": [++ "string",++ "null"++ ]},-- "name": {-- "type": "string"++ "currency": {++ "enum": [++ "vibes-bits",++ "usd-cents"++ ]}}},-- "metric": {-- "type": [-- "string",-- "null"++ "to": {++ "type": "object",++ "required": [++ "kind",++ "currency"],-- "maxLength": 80,-- "description": "Threshold rules: e.g. price.close, volume, us-gaap:Revenues."-- },-- "operator": {-- "enum": [-- "above",-- "below",-- null-- ]++ "properties": {++ "kind": {++ "enum": [++ "user_credit",++ "creator_payable",++ "provider_clearing",++ "platform_revenue",++ "payouts_pending",++ "refunds",++ "import_adjustment",++ "chargeback_loss",++ "fx_conversion"++ ]++ },++ "owner": {++ "type": [++ "string",++ "null"++ ]++ },++ "currency": {++ "enum": [++ "vibes-bits",++ "usd-cents"++ ]++ }++ }},-- "threshold": {++ "amount": {"type": [-- "string",-- "null"++ "integer",++ "string"],-- "maxLength": 64,-- "description": "Decimal text as entered."++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "In the accounts' currency (at most 1,000,000,000)."},-- "unit": {++ "reason": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "relates_to": {"type": ["string","null"-- ],-- "maxLength": 20++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "provider",++ "provider_ref"++ ],++ "properties": {++ "provider": {++ "type": "string"},-- "currency": {++ "provider_ref": {++ "type": "string",++ "minLength": 1,++ "description": "The provider's payment id."++ },++ "subject": {++ "description": "Who the Vibes are for.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount_cents": {"type": [-- "string",-- "null"++ "integer",++ "string"],-- "pattern": "^[A-Z]{3}$"++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Cents paid."},-- "form_types": {++ "bits": {"type": [-- "array",-- "null"++ "integer",++ "string"],-- "items": {-- "type": "string",-- "maxLength": 20-- },-- "description": "filing_type rules: the 1–10 form types that trigger it (10-K, 8-K…)."++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Vibes to credit."},-- "armed": {++ "intent_id": {++ "type": "string"++ },++ "test": {++ "type": "boolean"++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "billing.admin-result": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/admin-result.v1.json",++ "title": "BillingAdminResult",++ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "frozen",++ "reason",++ "frozen_at",++ "frozen_by"++ ],++ "properties": {++ "frozen": {++ "type": "boolean"++ },++ "reason": {"type": [-- "boolean",++ "string","null"-- ],-- "description": "Threshold rules: armed before this delivery (always true when it fired); null for document rules."++ ]},-- "created_at": {-- "type": "string",-- "format": "date-time"++ "frozen_at": {++ "type": [++ "string",++ "null"++ ]},-- "updated_at": {-- "type": "string",-- "format": "date-time"-- }++ "frozen_by": {},++ "processed_after_unfreeze": {}}},-- "instrument": {++ {"type": "object","required": ["id",-- "symbol",-- "name",-- "url"++ "ok",++ "checks"],-- "additionalProperties": false,"properties": {"id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ "type": "string"},-- "symbol": {-- "type": "string",-- "minLength": 1,-- "maxLength": 32++ "ok": {++ "type": "boolean"},-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 300++ "trigger": {++ "type": [++ "string",++ "null"++ ]},-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Instrument page on Trade."++ "started_at": {++ "type": "string"++ },++ "finished_at": {++ "type": "string"++ },++ "checks": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "ok"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "ok": {++ "type": "boolean"++ }++ }++ }++ },++ "warnings": {++ "type": "array"++ },++ "totals": {}++ }++ },++ {++ "type": "object",++ "required": [++ "transaction"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "duplicate_receipt": {++ "type": "boolean"}}},-- "trigger": {-- "oneOf": [-- {-- "type": "object",-- "additionalProperties": false,-- "required": [-- "kind",-- "id",-- "metric",-- "value",-- "unit",-- "currency",-- "observed_at",-- "retrieved_at",-- "source_key",-- "source_url"-- ],-- "properties": {-- "kind": {-- "const": "observation"-- },-- "id": {-- "type": "string",-- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "metric": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80-- },-- "value": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64,-- "description": "The decimal exactly as the source stated it."-- },-- "unit": {-- "type": "string",-- "minLength": 1,-- "maxLength": 20-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$"-- },-- "observed_at": {-- "type": "string",-- "format": "date-time"-- },-- "retrieved_at": {-- "type": "string",-- "format": "date-time"-- },-- "source_key": {-- "type": "string",-- "minLength": 1,-- "maxLength": 128,-- "description": "OpenVibe.Sources source key."-- },-- "source_url": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 2048++ {++ "type": "object",++ "required": [++ "events"++ ],++ "properties": {++ "events": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "provider"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "provider": {++ "type": "string"++ },++ "provider_event_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "type": {++ "type": [++ "string",++ "null"++ ]++ },++ "processed_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "result": {},++ "attempts": {++ "type": "integer"++ },++ "last_error": {++ "type": [++ "string",++ "null"++ ]++ }}}-- },-- {++ }++ }++ },++ {++ "type": "object",++ "required": [++ "event"++ ],++ "properties": {++ "event": {"type": "object",-- "additionalProperties": false,"required": [-- "kind","id",-- "form_type",-- "title",-- "url",-- "published_at",-- "retrieved_at",-- "source_key"-- ],-- "properties": {-- "kind": {-- "const": "document"-- },-- "id": {-- "type": "string",-- "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "form_type": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 20-- },-- "title": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 1000-- },-- "url": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 2048-- },-- "published_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "The source's own date; null if it gave none."-- },-- "retrieved_at": {-- "type": "string",-- "format": "date-time"-- },-- "source_key": {-- "type": "string",-- "minLength": 1,-- "maxLength": 128-- }++ "provider"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "renewed",++ "expired",++ "canceled",++ "skipped"++ ],++ "properties": {++ "renewed": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "expired": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "canceled": {++ "type": "array",++ "items": {++ "type": "string"}++ },++ "skipped": {++ "type": "integer"}-- ]++ }},-- "disclaimer": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "Currently \"Information only — not investment advice; no trading here.\""++ {++ "type": "object",++ "required": [++ "holds"++ ],++ "properties": {++ "holds": {++ "type": "array",++ "items": {++ "type": "object"++ }++ }++ }}-- }++ ]}},-- "tools.job.created": {++ "billing.balance-read-result": {"version": "1.0.0",-- "owner": "tools",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.created.v1.json",-- "title": "ToolsJobCreatedPayload",-- "description": "tools.job.created v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A job was accepted and queued (POST /api/v1/jobs, or a retry of a failed job, which carries retry_of). An Idempotency-Key replay creates no job and no event. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor the owner when it is a subject (user, service, app or mod), else service:tools.",-- "type": "object",-- "required": [-- "job_id",-- "service",-- "type",-- "type_version",-- "state",-- "owner",-- "attempts",-- "max_attempts",-- "created_at",-- "retry_of"-- ],-- "additionalProperties": false,-- "properties": {-- "job_id": {-- "type": "string",-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "service": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$",-- "description": "The Tools satellite that runs the job (img, audio, docs…)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type, e.g. img.process."-- },-- "type_version": {-- "type": "integer",-- "minimum": 1-- },-- "state": {-- "const": "queued"++ "$id": "https://openvibe.network/contracts/billing/balance-read-result.v1.json",++ "title": "BillingBalanceReadResult",++ "description": "billing.balance-read-result@1: answers of billing.balance.read on OpenVibe.Billing. GET /api/v1/balances/:subject → billing.balance@1; GET /api/v1/transactions?subject=&limit=&cursor= → { transactions, next_cursor } (newest first, at most 200 a page; subject is required); GET /api/v1/transactions/:id → { transaction, reversed_by } (ids of the transactions that reverse it).",++ "anyOf": [++ {++ "$ref": "balance.v1.json"},-- "owner": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"++ {++ "type": "object",++ "required": [++ "transactions",++ "next_cursor"++ ],++ "properties": {++ "transactions": {++ "type": "array",++ "maxItems": 200,++ "items": {++ "$ref": "transaction.v1.json"++ }},-- {-- "type": "null"++ "next_cursor": {++ "type": [++ "string",++ "null"++ ]}-- ],-- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."-- },-- "attempts": {-- "type": "integer",-- "minimum": 0-- },-- "max_attempts": {-- "type": "integer",-- "minimum": 1-- },-- "created_at": {-- "type": "string",-- "format": "date-time"++ }},-- "retry_of": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "transaction",++ "reversed_by"],-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "reversed_by": {++ "type": "array",++ "items": {++ "type": "string"++ }++ }++ }}-- }++ ]}},-- "tools.job.started": {++ "billing.balance": {"version": "1.0.0",-- "owner": "tools",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.started.v1.json",-- "title": "ToolsJobStartedPayload",-- "description": "tools.job.started v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A worker claimed the job and it is running (attempts counts this attempt). A job requeued after a restart starts again and is announced again. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:tools.",++ "$id": "https://openvibe.network/contracts/billing/balance.v1.json",++ "title": "BillingBalance",++ "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.","type": "object","required": [-- "job_id",-- "service",-- "type",-- "type_version",-- "state",-- "owner",-- "attempts",-- "max_attempts",-- "created_at",-- "retry_of",-- "started_at"++ "subject",++ "currency",++ "credit",++ "payable",++ "pending_payouts",++ "payable_value_cents",++ "bits_per_usd"],-- "additionalProperties": false,"properties": {-- "job_id": {-- "type": "string",-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "service": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$",-- "description": "The Tools satellite that runs the job (img, audio, docs…)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type, e.g. img.process."++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"},-- "type_version": {-- "type": "integer",-- "minimum": 1++ "currency": {++ "const": "vibes-bits"},-- "state": {-- "const": "running"++ "credit": {++ "type": "integer"},-- "owner": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }-- ],-- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."++ "payable": {++ "type": "integer"},-- "attempts": {-- "type": "integer",-- "minimum": 0++ "pending_payouts": {++ "type": "integer"},-- "max_attempts": {++ "payable_value_cents": {"type": "integer",-- "minimum": 1-- },-- "created_at": {-- "type": "string",-- "format": "date-time"++ "description": "What payable is worth in US cents at today's rate."},-- "retry_of": {-- "type": [-- "string",-- "null"++ "bits_per_usd": {++ "type": "number"++ }++ }++ }++ },++ "billing.cashout-decision": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout-decision.v1.json",++ "title": "BillingCashoutDecision",++ "description": "billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "payout_reference"],-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ "properties": {++ "payout_reference": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The provider's payout id."++ },++ "payout_provider": {++ "type": "string"++ }++ }},-- "started_at": {-- "type": "string",-- "format": "date-time"++ {++ "type": "object",++ "required": [++ "reason"++ ],++ "properties": {++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "tools.job.succeeded": {++ "billing.cashout-request": {"version": "1.0.0",-- "owner": "tools",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.succeeded.v1.json",-- "title": "ToolsJobSucceededPayload",-- "description": "tools.job.succeeded v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job finished and its result files are stored (in Media when TOOLS_JOB_RESULTS=media). Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",++ "$id": "https://openvibe.network/contracts/billing/cashout-request.v1.json",++ "title": "BillingCashoutRequest",++ "description": "billing.cashout-request@1: bodies of billing.cashout.request on OpenVibe.Billing. POST /api/v1/cashouts: { subject, amount, payout_method: { type, address } } moves amount bits (at least the minimum cashout) from the person's payable into payouts pending, in escrow until staff pay it (a PayPal address must be an email). POST /api/v1/recycle: { subject, amount } turns payable back into the person's own spendable credit. GET /api/v1/cashouts/:id takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "subject",++ "amount",++ "payout_method"++ ],++ "properties": {++ "subject": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits."++ },++ "payout_method": {++ "type": "object",++ "required": [++ "type",++ "address"++ ],++ "properties": {++ "type": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",++ "description": "paypal, …"++ },++ "address": {++ "type": "string",++ "minLength": 1,++ "maxLength": 254++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "subject",++ "amount"++ ],++ "properties": {++ "subject": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits."++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "billing.cashout-result": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout-result.v1.json",++ "title": "BillingCashoutResult",++ "description": "billing.cashout-result@1: answers about cashouts on OpenVibe.Billing. POST /api/v1/cashouts (201), GET /api/v1/cashouts/:id, POST /api/v1/cashouts/:id/approve and /deny → { cashout }; GET /api/v1/cashouts?status=&subject=&limit= → { cashouts }; POST /api/v1/recycle (201) → { transaction, balance }. A cashout the Events stream carries names the payout method's type only; this API answers the caller who may see the address.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "cashout"++ ],++ "properties": {++ "cashout": {++ "$ref": "cashout.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "cashouts"++ ],++ "properties": {++ "cashouts": {++ "type": "array",++ "items": {++ "$ref": "cashout.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "transaction",++ "balance"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "balance": {++ "$ref": "balance.v1.json"++ }++ }++ }++ ]++ }++ },++ "billing.cashout": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/cashout.v1.json",++ "title": "BillingCashout",++ "description": "billing.cashout@1: one cashout as OpenVibe.Billing presents it (server/ops/cashouts.js present). Requested cashouts wait out an escrow; staff approve (with the provider's payout reference) or deny them.","type": "object","required": [-- "job_id",-- "service",-- "type",-- "type_version",-- "state",-- "owner",-- "attempts",-- "max_attempts",-- "created_at",-- "retry_of",-- "started_at",-- "finished_at",-- "expires_at",-- "result"++ "id",++ "subject",++ "amount_bits",++ "value_cents",++ "status",++ "payout_method",++ "escrow_until",++ "request_txn",++ "created_at"],-- "additionalProperties": false,"properties": {-- "job_id": {-- "type": "string",-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "service": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$",-- "description": "The Tools satellite that runs the job (img, audio, docs…)."++ "id": {++ "type": "string"},-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type, e.g. img.process."++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"},-- "type_version": {++ "amount_bits": {"type": "integer","minimum": 1},-- "state": {-- "const": "succeeded"++ "value_cents": {++ "type": "integer",++ "minimum": 0},-- "owner": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"++ "status": {++ "enum": [++ "requested",++ "paid",++ "denied"++ ]++ },++ "payout_method": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "type": {++ "type": "string"},-- {-- "type": "null"++ "address": {++ "type": "string"}-- ],-- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."-- },-- "attempts": {-- "type": "integer",-- "minimum": 0++ }},-- "max_attempts": {-- "type": "integer",-- "minimum": 1++ "escrow_until": {++ "type": "string"},-- "created_at": {-- "type": "string",-- "format": "date-time"++ "request_txn": {++ "type": "string"},-- "retry_of": {++ "settle_txn": {"type": ["string","null"-- ],-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."-- },-- "started_at": {-- "type": "string",-- "format": "date-time"++ ]},-- "finished_at": {-- "type": "string",-- "format": "date-time"++ "payout_provider": {++ "type": [++ "string",++ "null"++ ]},-- "expires_at": {++ "payout_reference": {"type": ["string","null"-- ],-- "format": "date-time",-- "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."++ ]},-- "result": {++ "reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ "billing.entitlement-result": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/entitlement-result.v1.json",++ "title": "BillingEntitlementResult",++ "description": "billing.entitlement-result@1: answers of billing.entitlement.check on OpenVibe.Billing. GET /api/v1/entitlements/:subject?streamer= → billing.entitlement@1; without streamer → { subject, entitlements } (every active one); GET /api/v1/subscriptions → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",++ "anyOf": [++ {++ "$ref": "entitlement.v1.json"++ },++ {"type": "object","required": [-- "files"++ "subject",++ "entitlements"],-- "additionalProperties": false,-- "description": "Where the result files are, never their content, names or the tool's output data (GET /api/v1/jobs/:id has those, for the owner).","properties": {-- "files": {++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "entitlements": {"type": "array",-- "maxItems": 100,"items": {-- "type": "object",-- "required": [-- "index",-- "mime",-- "size",-- "sha256",-- "storage",-- "media"-- ],-- "additionalProperties": false,-- "properties": {-- "index": {-- "type": "integer",-- "minimum": 0,-- "description": "n in /api/v1/jobs/:id/files/:n."-- },-- "mime": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200-- },-- "size": {-- "type": "integer",-- "minimum": 0-- },-- "sha256": {-- "type": "string",-- "pattern": "^[0-9a-f]{64}$"-- },-- "storage": {-- "enum": [-- "local",-- "media"-- ],-- "description": "media: stored as an OpenVibe.Media object (TOOLS_JOB_RESULTS=media); local: kept on the satellite until the job expires."-- },-- "media": {-- "oneOf": [-- {-- "$ref": "../../media/media-ref.v1.json"-- },-- {-- "type": "null"-- }-- ],-- "description": "{ media_id, role: output } when storage is media."-- }-- }++ "$ref": "entitlement.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "subscriptions"++ ],++ "properties": {++ "subscriptions": {++ "type": "array",++ "items": {++ "$ref": "subscription.v1.json"}}}++ },++ {++ "type": "object",++ "required": [++ "subscription",++ "entitlement"++ ],++ "properties": {++ "subscription": {++ "$ref": "subscription.v1.json"++ },++ "entitlement": {++ "$ref": "entitlement.v1.json"++ }++ }}-- }++ ]}},-- "tools.job.failed": {++ "billing.entitlement": {"version": "1.0.0",-- "owner": "tools",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.failed.v1.json",-- "title": "ToolsJobFailedPayload",-- "description": "tools.job.failed v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job failed: the tool refused the input, it timed out, or it was running when the satellite restarted and its type does not requeue. A cancelled job is not a failure and is not announced as one. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",++ "$id": "https://openvibe.network/contracts/billing/entitlement.v1.json",++ "title": "BillingEntitlement",++ "description": "billing.entitlement@1: whether a person is entitled to a streamer's subscriber perks now (OpenVibe.Billing server/ops/subscriptions.js entitlement): GET /api/v1/entitlements/:subject?streamer=, and inside subscription answers. Chained, already-paid future periods extend expires_at.","type": "object","required": [-- "job_id",-- "service",-- "type",-- "type_version",-- "state",-- "owner",-- "attempts",-- "max_attempts",-- "created_at",-- "retry_of",-- "started_at",-- "finished_at",++ "subject",++ "streamer",++ "kind",++ "active","expires_at",-- "error",-- "retryable"++ "subscription"],-- "additionalProperties": false,"properties": {-- "job_id": {-- "type": "string",-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "service": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$",-- "description": "The Tools satellite that runs the job (img, audio, docs…)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type, e.g. img.process."-- },-- "type_version": {-- "type": "integer",-- "minimum": 1-- },-- "state": {-- "const": "failed"-- },-- "owner": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }-- ],-- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."-- },-- "attempts": {-- "type": "integer",-- "minimum": 0++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"},-- "max_attempts": {-- "type": "integer",-- "minimum": 1++ "streamer": {++ "$ref": "../identity/subject-ref.v1.json"},-- "created_at": {-- "type": "string",-- "format": "date-time"++ "kind": {++ "type": "string"},-- "retry_of": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ "active": {++ "type": "boolean"},-- "started_at": {++ "expires_at": {"type": ["string","null"-- ],-- "format": "date-time",-- "description": "null when the job failed before it ever ran."-- },-- "finished_at": {-- "type": "string",-- "format": "date-time"++ ]},-- "expires_at": {++ "subscription": {"type": [-- "string",++ "object","null"],-- "format": "date-time",-- "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."-- },-- "error": {-- "type": "object",-- "required": [-- "status",-- "code",-- "detail"-- ],-- "additionalProperties": false,-- "description": "The job's problem (errors.problem@1) without request or trace ids.","properties": {++ "id": {++ "type": "string"++ },"status": {-- "type": "integer",-- "minimum": 400,-- "maximum": 599++ "type": "string"},-- "code": {-- "type": "string",-- "pattern": "^tools\\.[a-z0-9_.]+$",-- "description": "tools.job.failed, tools.job.timeout, or a code the tool chose (tools.…: tools.pdf.too_many_pages, tools.pdf.wrong_password…)."++ "auto_renew": {++ "type": [++ "boolean",++ "integer"++ ]},-- "detail": {-- "type": "string",-- "maxLength": 500,-- "description": "Human-readable reason; server paths are scrubbed."++ "cancel_at_period_end": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "provider": {++ "type": "string"}}-- },-- "retryable": {-- "type": "boolean"}}}},-- "live.stream.started": {++ "billing.intent-request": {"version": "1.0.0",-- "owner": "live",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/live.stream.started.v1.json",-- "title": "LiveStreamStartedPayload",-- "description": "live.stream.started v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js createStream). A streams row went live: WebRTC/JSMPEG from the dashboard, RTMP ingest, WHIP, or an OpenRe.Stream session mirrored into Live. Written to Live's event_outbox in the transaction that inserts the row, so the event exists if and only if the stream did. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 1 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",-- "type": "object",-- "required": [-- "stream_id",-- "channel",-- "title",-- "category",-- "protocol",-- "is_nsfw",-- "started_at"-- ],-- "additionalProperties": false,-- "properties": {-- "stream_id": {-- "type": "integer",-- "minimum": 1,-- "description": "Live's streams row id (the envelope subject id)."-- },-- "channel": {++ "$id": "https://openvibe.network/contracts/billing/intent-request.v1.json",++ "title": "BillingIntentRequest",++ "description": "billing.intent-request@1: the body of POST /api/v1/intents on OpenVibe.Billing (billing.intent.create): a checkout with an enabled provider for a Vibes purchase ({ kind: purchase, bits } at least the minimum purchase) or a channel subscription ({ kind: subscription, streamer }; route direct pays the streamer's own account, and a direct PowerChat subscription names receiving_account). Billing prices it; the answer is { intent, checkout_url } (201). GET /api/v1/intents/:id and POST /api/v1/intents/:id/capture take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {"type": "object","required": [-- "username",-- "display_name",-- "url"++ "provider",++ "kind",++ "subject"],-- "additionalProperties": false,"properties": {-- "username": {-- "type": "string",-- "minLength": 1,-- "description": "The streamer's Live username (the channel slug)."-- },-- "display_name": {++ "provider": {"type": "string",-- "minLength": 1,-- "description": "The display name, or the username when there is none."++ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",++ "description": "stripe, paypal, nowpayments, powerchat, ccbill (enabled ones only)."},-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https://openvibe\\.live/",-- "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."++ "kind": {++ "enum": [++ "purchase",++ "subscription"++ ]},"subject": {-- "type": "object",-- "required": [-- "type",-- "id"++ "description": "Who pays.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "bits": {++ "type": [++ "integer",++ "string"],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Purchase: the Vibes to buy."++ },++ "streamer": {++ "description": "Subscription: the channel's owner (never the payer).",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"},-- "id": {++ {"type": "string","pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"}-- },-- "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."++ ]++ },++ "route": {++ "type": "string",++ "description": "Subscription: direct, or the site route."++ },++ "receiving_account": {++ "type": "string",++ "description": "Direct PowerChat subscription: the streamer's PowerChat username."++ },++ "auto_renew": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "success_url": {++ "type": "string"++ },++ "cancel_url": {++ "type": "string"}}},-- "title": {-- "type": "string"-- },-- "category": {-- "type": [-- "string",-- "null"-- ],-- "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."-- },-- "protocol": {-- "enum": [-- "jsmpeg",-- "webrtc",-- "rtmp"-- ],-- "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."-- },-- "is_nsfw": {-- "type": "boolean"-- },-- "started_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "UTC. null only for a row without a start time, which createStream never writes."++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "live.stream.ended": {++ "billing.intent-result": {"version": "1.0.0",-- "owner": "live",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json",-- "title": "LiveStreamEndedPayload",-- "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",++ "$id": "https://openvibe.network/contracts/billing/intent-result.v1.json",++ "title": "BillingIntentResult",++ "description": "billing.intent-result@1: answers of billing.intent.create on OpenVibe.Billing. POST /api/v1/intents → { intent, checkout_url } (null when the provider has no hosted checkout); GET /api/v1/intents/:id → { intent }; POST /api/v1/intents/:id/capture → { result, intent } (the settlement the provider's capture produced).","type": "object","required": [-- "stream_id",-- "channel",-- "title",-- "category",-- "protocol",-- "is_nsfw",-- "started_at",-- "ended_at",-- "duration_seconds"++ "intent"],-- "additionalProperties": false,"properties": {-- "stream_id": {-- "type": "integer",-- "minimum": 1,-- "description": "Live's streams row id (the envelope subject id)."-- },-- "channel": {-- "type": "object",-- "required": [-- "username",-- "display_name",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "username": {-- "type": "string",-- "minLength": 1,-- "description": "The streamer's Live username (the channel slug)."-- },-- "display_name": {-- "type": "string",-- "minLength": 1,-- "description": "The display name, or the username when there is none."-- },-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https://openvibe\\.live/",-- "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."-- },-- "subject": {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- },-- "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."-- }-- }-- },-- "title": {-- "type": "string"-- },-- "category": {-- "type": [-- "string",-- "null"-- ],-- "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."-- },-- "protocol": {-- "enum": [-- "jsmpeg",-- "webrtc",-- "rtmp"-- ],-- "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."-- },-- "is_nsfw": {-- "type": "boolean"++ "intent": {++ "$ref": "intent.v1.json"},-- "started_at": {++ "checkout_url": {"type": ["string","null"-- ],-- "format": "date-time",-- "description": "UTC. null only for a row without a start time, which createStream never writes."-- },-- "ended_at": {-- "type": "string",-- "format": "date-time",-- "description": "UTC."++ ]},-- "duration_seconds": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 0,-- "description": "Whole seconds from started_at to ended_at; null when the row has no start time."++ "result": {++ "description": "Capture: what applying the provider's plan did."}}}},-- "live.release.deployed": {++ "billing.intent": {"version": "1.0.0",-- "owner": "live",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/live.release.deployed.v1.json",-- "title": "LiveReleaseDeployedPayload",-- "description": "live.release.deployed v1 (OpenVibe.Live server/events/release-events.js envelopeFor, called by server/chat/deploy-notice.js announce). The first boot that runs commits never announced before: queued in the transaction that records them as announced (site setting deploy_last_announced), so a restart without new code emits nothing and a crash before that commit announces again on the next boot with the same subject. A consumer keys on subject.id (the head commit) to fold repeats. The commit subjects are public already (Live chat and /updates); the consumer decides what is shown. Envelope: subject { type: release, id: <head commit, 40 hex> }, visibility internal, priority low, actor service:live.",++ "$id": "https://openvibe.network/contracts/billing/intent.v1.json",++ "title": "BillingIntent",++ "description": "billing.intent@1: a checkout intent as OpenVibe.Billing presents it (server/ops/intents.js present): what a person is about to pay a provider for (a Vibes purchase or a channel subscription), priced by Billing.","type": "object","required": [-- "service",-- "release",-- "commit",-- "previous",-- "commit_count",-- "commits",-- "deployed_at",-- "notes_url"++ "id",++ "provider",++ "provider_ref",++ "kind",++ "subject",++ "streamer",++ "amount_cents",++ "fee_cents",++ "bits",++ "route",++ "status",++ "settled_txn",++ "created_at"],-- "additionalProperties": false,"properties": {-- "service": {-- "const": "live"-- },-- "release": {++ "id": {"type": "string",-- "pattern": "^[0-9a-f]{7,40}$",-- "description": "The head commit's short sha as git abbreviated it (its first 12 characters when the head is not in the list)."++ "pattern": "^pi_"},-- "commit": {-- "type": "string",-- "pattern": "^[0-9a-f]{40}$",-- "description": "The head commit (the envelope subject id)."++ "provider": {++ "type": "string"},-- "previous": {++ "provider_ref": {"type": ["string","null"-- ],-- "pattern": "^[0-9a-f]{40}$",-- "description": "The head announced before this one; null on the first announcement."++ ]},-- "commit_count": {++ "kind": {++ "enum": [++ "purchase",++ "subscription"++ ]++ },++ "subject": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "streamer": {++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "amount_cents": {"type": "integer",-- "minimum": 1,-- "maximum": 40,-- "description": "How many new commits were found (git log previous..head, at most 40; only the head on a first run or after history was rewritten)."++ "minimum": 0},-- "commits": {-- "type": "array",-- "minItems": 1,-- "maxItems": 40,-- "description": "The new commits, newest first.",-- "items": {-- "type": "object",-- "required": [-- "hash",-- "short",-- "subject",-- "date"-- ],-- "additionalProperties": false,-- "properties": {-- "hash": {-- "type": "string",-- "pattern": "^[0-9a-f]{40}$"-- },-- "short": {-- "type": "string",-- "pattern": "^[0-9a-f]{4,40}$"-- },-- "subject": {-- "type": "string",-- "maxLength": 200,-- "description": "The commit subject, cut to 200 characters."-- },-- "date": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "Author date (git %aI, with the author's UTC offset)."++ "fee_cents": {++ "type": "integer",++ "minimum": 0++ },++ "bits": {++ "type": "integer",++ "minimum": 0++ },++ "route": {++ "type": [++ "string",++ "null"++ ]++ },++ "auto_renew": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "status": {++ "type": "string",++ "description": "created, settled, failed, …"++ },++ "settled_txn": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_at": {++ "type": "string"++ },++ "checkout_ref": {++ "type": "string",++ "description": "PowerChat only: pcorder:<id> or pcsub:<id>, echoed back by its webhook."++ }++ }++ }++ },++ "billing.subscription-request": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/subscription-request.v1.json",++ "title": "BillingSubscriptionRequest",++ "description": "billing.subscription-request@1: the body of POST /api/v1/subscriptions on OpenVibe.Billing (billing.subscription.manage): pay one period of subscriber → streamer, from the subscriber's credit (source credit, the default) or from a provider receipt (source receipt, which also needs billing.ledger.admin: claiming money was received is operator grade). POST /api/v1/subscriptions/:id/cancel (cancels at the period end, with the provider too) and the GETs take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "subscriber",++ "streamer"++ ],++ "properties": {++ "subscriber": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "streamer": {++ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "source": {++ "enum": [++ "credit",++ "receipt"++ ]++ },++ "price_cents": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Default: Billing's subscription price."++ },++ "auto_renew": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "receipt": {++ "type": "object",++ "required": [++ "provider",++ "provider_ref"++ ],++ "properties": {++ "provider": {++ "type": "string"++ },++ "provider_ref": {++ "type": "string"++ },++ "amount_cents": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Cents paid."++ },++ "fee_cents": {++ "type": [++ "integer",++ "string"++ ]++ },++ "route": {++ "type": "string"++ },++ "subscription_ref": {++ "type": "string"++ },++ "test": {++ "type": "boolean"++ }}}}},-- "deployed_at": {-- "type": "string",-- "format": "date-time",-- "description": "When this boot announced it (UTC)."++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "billing.subscription-result": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/subscription-result.v1.json",++ "title": "BillingSubscriptionResult",++ "description": "billing.subscription-result@1: answers about subscriptions on OpenVibe.Billing. POST /api/v1/subscriptions (201) → { subscription, entitlement, transaction }; POST /api/v1/subscriptions/:id/cancel → { subscription, provider_sync }; GET /api/v1/subscriptions?subscriber=&streamer=&status= → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "subscription"++ ],++ "properties": {++ "subscription": {++ "$ref": "subscription.v1.json"++ },++ "entitlement": {++ "description": "billing.entitlement@1 (the answer to POST /subscriptions carries the entitlement as booked)."++ },++ "transaction": {++ "$ref": "transaction.v1.json"++ },++ "provider_sync": {++ "type": "string",++ "description": "not_needed, or how the provider was told."++ }++ }},-- "notes_url": {-- "const": "https://openvibe.live/updates"++ {++ "type": "object",++ "required": [++ "subscriptions"++ ],++ "properties": {++ "subscriptions": {++ "type": "array",++ "items": {++ "$ref": "subscription.v1.json"++ }++ }++ }}-- }++ ]}},-- "media.job.proposed": {++ "billing.subscription": {"version": "1.0.0",-- "owner": "media",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",-- "title": "MediaJobProposedPayload",-- "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "$id": "https://openvibe.network/contracts/billing/subscription.v1.json",++ "title": "BillingSubscription",++ "description": "billing.subscription@1: one channel subscription as OpenVibe.Billing presents it (server/ops/subscriptions.js present).","type": "object","required": ["id",-- "app_id",-- "object_id",-- "type",++ "subscriber",++ "streamer",++ "provider","status",-- "attempts",-- "max_attempts",-- "error_code",-- "cancel_requested",-- "has_result",-- "run_after",-- "decided_at",-- "created_at",-- "updated_at",-- "started_at",-- "finished_at"++ "auto_renew",++ "cancel_at_period_end",++ "current_period_end"],-- "additionalProperties": false,"properties": {"id": {-- "type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ "type": "string"},-- "app_id": {++ "subscriber": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "streamer": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "tier": {},++ "provider": {"type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ "description": "credit, or the provider that takes the payments."},-- "object_id": {++ "provider_ref": {++ "type": [++ "string",++ "null"++ ]++ },++ "route": {"type": ["string","null"],-- "maxLength": 200,-- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ "description": "site or direct (paid to the streamer's own provider account)."},-- "type": {++ "status": {"type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ "description": "active, canceled, expired, …"},-- "status": {-- "const": "proposed"++ "auto_renew": {++ "type": "boolean"},-- "attempts": {-- "type": "integer",-- "minimum": 0,-- "description": "Attempts started so far."++ "cancel_at_period_end": {++ "type": "boolean"},-- "max_attempts": {-- "type": "integer",-- "minimum": 1,-- "maximum": 10-- },-- "error_code": {-- "type": "null"-- },-- "cancel_requested": {-- "type": "boolean",-- "description": "Its owner asked a running job to stop."-- },-- "has_result": {-- "const": false,-- "description": "A proposal has no result."++ "price_cents": {++ "type": [++ "integer",++ "null"++ ]},-- "run_after": {++ "current_period_end": {"type": ["string","null"-- ],-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "A queued job waits until then (retry backoff); null otherwise."-- },-- "decided_at": {-- "type": "null",-- "description": "Nobody has decided yet."++ ]},"created_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ "type": "string"},"updated_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"-- },-- "started_at": {-- "type": "null"-- },-- "finished_at": {-- "type": "null"++ "type": [++ "string",++ "null"++ ]}}}},-- "media.job.queued": {++ "billing.transaction": {"version": "1.0.0",-- "owner": "media",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",-- "title": "MediaJobQueuedPayload",-- "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "$id": "https://openvibe.network/contracts/billing/transaction.v1.json",++ "title": "BillingTransaction",++ "description": "billing.transaction@1: one journal transaction as OpenVibe.Billing's /api/v1 presents it (server/ledger.js present). Amounts are integers: bits for vibes-bits, cents for usd-cents; each transaction's entries balance per currency. Subjects are bare Network subject ids.","type": "object","required": ["id",-- "app_id",-- "object_id","type","status",-- "attempts",-- "max_attempts",-- "error_code",-- "cancel_requested",-- "has_result",-- "run_after",-- "decided_at",++ "test",++ "reverses_txn",++ "from_subject",++ "to_subject",++ "provider",++ "receipt_ref","created_at",-- "updated_at",-- "started_at",-- "finished_at"++ "entries"],-- "additionalProperties": false,"properties": {"id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."-- },-- "app_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."-- },-- "object_id": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ "pattern": "^txn_"},"type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ "enum": [++ "purchase",++ "donation",++ "subscription",++ "subscription_share",++ "cashout_request",++ "cashout_paid",++ "cashout_denied",++ "recycle",++ "refund",++ "chargeback",++ "adjustment",++ "import"++ ]},"status": {-- "const": "queued"-- },-- "attempts": {-- "type": "integer",-- "minimum": 0,-- "description": "Attempts started so far."++ "type": "string"},-- "max_attempts": {-- "type": "integer",-- "minimum": 1,-- "maximum": 10++ "test": {++ "type": "boolean",++ "description": "Test money: never counted."},-- "error_code": {-- "type": "null"++ "reverses_txn": {++ "type": [++ "string",++ "null"++ ]},-- "cancel_requested": {-- "type": "boolean",-- "description": "Its owner asked a running job to stop."++ "from_subject": {++ "type": [++ "string",++ "null"++ ]},-- "has_result": {-- "const": false,-- "description": "A job that never ran has no result."++ "to_subject": {++ "type": [++ "string",++ "null"++ ]},-- "run_after": {++ "provider": {"type": ["string","null"-- ],-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "A queued job waits until then (retry backoff); null otherwise."++ ]},-- "decided_at": {++ "receipt_ref": {"type": ["string","null"],-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."++ "description": "<provider>:<provider payment id> for money that arrived from a provider."},-- "created_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ "actor": {++ "description": "Who posted it (as recorded)."},-- "updated_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ "metadata": {++ "description": "Per type: amount_bits, paid_cents, target EntityRef, message, rates…"},-- "started_at": {-- "type": "null"++ "created_at": {++ "type": "string"},-- "finished_at": {-- "type": "null"++ "entries": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "account",++ "amount"++ ],++ "properties": {++ "account": {++ "type": "object",++ "required": [++ "kind",++ "currency"++ ],++ "properties": {++ "kind": {++ "enum": [++ "user_credit",++ "creator_payable",++ "provider_clearing",++ "platform_revenue",++ "payouts_pending",++ "refunds",++ "import_adjustment",++ "chargeback_loss",++ "fx_conversion"++ ]++ },++ "owner": {++ "type": [++ "string",++ "null"++ ],++ "description": "A subject id, a provider slug, or null for a platform account."++ },++ "currency": {++ "enum": [++ "vibes-bits",++ "usd-cents"++ ]++ }++ }++ },++ "amount": {++ "type": "integer",++ "description": "Signed: negative leaves the account."++ }++ }++ }}}}},-- "media.job.started": {++ "billing.transfer-request": {"version": "1.0.0",-- "owner": "media",++ "owner": "billing","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",-- "title": "MediaJobStartedPayload",-- "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",-- "type": "object",-- "required": [-- "id",-- "app_id",-- "object_id",-- "type",-- "status",-- "attempts",-- "max_attempts",-- "error_code",-- "cancel_requested",-- "has_result",-- "run_after",-- "decided_at",-- "created_at",-- "updated_at",-- "started_at",-- "finished_at"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."-- },-- "app_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."-- },-- "object_id": {-- "type": [-- "string",-- "null"++ "$id": "https://openvibe.network/contracts/billing/transfer-request.v1.json",++ "title": "BillingTransferRequest",++ "description": "billing.transfer-request@1: bodies of billing.transfer.create on OpenVibe.Billing. POST /api/v1/transfers: { from, to, amount, kind?, target?, message? } moves amount bits from the sender's credit to the recipient's payable (a tip, donation or paid interaction; never to oneself). POST /api/v1/transfers/:id/refund: { amount?, reason? } gives a credit-funded donation back (all that remains refundable when amount is absent). Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "from",++ "to",++ "amount"],-- "maxLength": 200,-- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."-- },-- "status": {-- "const": "running",-- "description": "started carries the job's state, running."-- },-- "attempts": {-- "type": "integer",-- "minimum": 1,-- "description": "Attempts started so far, this one included."-- },-- "max_attempts": {-- "type": "integer",-- "minimum": 1,-- "maximum": 10-- },-- "error_code": {-- "type": [-- "string",-- "null"-- ],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."-- },-- "cancel_requested": {-- "type": "boolean",-- "description": "Its owner asked a running job to stop."-- },-- "has_result": {-- "const": false,-- "description": "A running job has no result yet."++ "properties": {++ "from": {++ "description": "The sender (their credit pays).",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "to": {++ "description": "The recipient (their payable receives).",++ "anyOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ ]++ },++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits."++ },++ "kind": {++ "enum": [++ "tip",++ "donation",++ "paid_interaction"++ ],++ "description": "Default donation."++ },++ "target": {++ "$ref": "../common/entity-ref.v1.json",++ "description": "What the money is for (a stream, a paid message, …)."++ },++ "message": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500++ }++ }},-- "run_after": {-- "type": "null",-- "description": "Cleared when the job is claimed."++ {++ "type": "object",++ "properties": {++ "amount": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 1,++ "pattern": "^[1-9][0-9]*$",++ "description": "Bits to give back; default all that remains refundable."++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ }++ }++ }++ ]++ }++ },++ "billing.transfer-result": {++ "version": "1.0.0",++ "owner": "billing",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/billing/transfer-result.v1.json",++ "title": "BillingTransferResult",++ "description": "billing.transfer-result@1: answers of billing.transfer.create on OpenVibe.Billing (201). POST /api/v1/transfers → { transaction, balance: { credit } } (the sender's credit after it); POST /api/v1/transfers/:id/refund → { transaction } (type refund, reverses_txn the donation).",++ "type": "object",++ "required": [++ "transaction"++ ],++ "properties": {++ "transaction": {++ "$ref": "transaction.v1.json"},-- "decided_at": {-- "type": [-- "string",-- "null"++ "balance": {++ "type": "object",++ "required": [++ "credit"],-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."-- },-- "created_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"-- },-- "updated_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"-- },-- "started_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "The first attempt's start."-- },-- "finished_at": {-- "type": "null"++ "properties": {++ "credit": {++ "type": "integer"++ }++ }}}}},-- "media.job.retrying": {++ "deals.watch.matched": {"version": "1.0.0",-- "owner": "media",++ "owner": "deals","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",-- "title": "MediaJobRetryingPayload",-- "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.watch.matched.v1.json",++ "title": "DealsWatchMatchedPayload",++ "description": "deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.","type": "object","required": [-- "id",-- "app_id",-- "object_id",-- "type",-- "status",-- "attempts",-- "max_attempts",-- "error_code",-- "cancel_requested",-- "has_result",-- "run_after",-- "decided_at",-- "created_at",-- "updated_at",-- "started_at",-- "finished_at"++ "watch_id",++ "recipient",++ "kind",++ "query",++ "product_id",++ "max_price",++ "currency",++ "offer_id",++ "offer_url",++ "title",++ "observation"],"additionalProperties": false,"properties": {-- "id": {++ "watch_id": {"type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ "pattern": "^dwt_[0-9A-HJKMNP-TV-Z]{26}$"},-- "app_id": {++ "recipient": {"type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The watch owner's user subject id: who is notified."},-- "object_id": {++ "kind": {++ "enum": [++ "keyword",++ "product",++ "price_below"++ ]++ },++ "query": {"type": ["string","null"],"maxLength": 200,-- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."-- },-- "status": {-- "const": "queued",-- "description": "retrying carries the job's state, queued."-- },-- "attempts": {-- "type": "integer",-- "minimum": 1,-- "description": "Attempts started so far, the failed one included."-- },-- "max_attempts": {-- "type": "integer",-- "minimum": 1,-- "maximum": 10++ "description": "Keywords of a keyword watch, or of a price-below watch without a product."},-- "error_code": {++ "product_id": {"type": ["string","null"],-- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."-- },-- "cancel_requested": {-- "type": "boolean",-- "description": "Its owner asked a running job to stop."-- },-- "has_result": {-- "const": false,-- "description": "A job waiting for its retry has no result."++ "maxLength": 64,++ "description": "Deals product id of a product or price-below watch."},-- "run_after": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "Not before this time (the retry backoff)."++ "max_price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Price-below threshold as entered (decimal text, e.g. 19.99)."},-- "decided_at": {++ "currency": {"type": ["string","null"],-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217 currency of max_price."},-- "created_at": {++ "offer_id": {"type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ "minLength": 1,++ "maxLength": 64,++ "description": "The canonical (root) offer id (dof_…)."},-- "updated_at": {++ "offer_url": {"type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical offer page URL on Deals."},-- "started_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "The first attempt's start."-- },-- "finished_at": {-- "type": "null"-- }-- }-- }-- },-- "media.job.succeeded": {-- "version": "1.0.0",-- "owner": "media",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",-- "title": "MediaJobSucceededPayload",-- "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",-- "type": "object",-- "required": [-- "id",-- "app_id",-- "object_id",-- "type",-- "status",-- "attempts",-- "max_attempts",-- "error_code",-- "cancel_requested",-- "has_result",-- "run_after",-- "decided_at",-- "created_at",-- "updated_at",-- "started_at",-- "finished_at"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."-- },-- "app_id": {++ "title": {"type": "string","minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."-- },-- "object_id": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 200,-- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."-- },-- "status": {-- "const": "succeeded"-- },-- "attempts": {-- "type": "integer",-- "minimum": 1,-- "description": "Attempts started, the successful one included."-- },-- "max_attempts": {-- "type": "integer",-- "minimum": 1,-- "maximum": 10-- },-- "error_code": {-- "type": "null",-- "description": "A success clears the last failure."-- },-- "cancel_requested": {-- "type": "boolean",-- "description": "Its owner asked a running job to stop."-- },-- "has_result": {-- "type": "boolean",-- "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it."-- },-- "run_after": {-- "type": "null"++ "maxLength": 500},-- "decided_at": {-- "type": [-- "string",-- "null"++ "observation": {++ "type": "object",++ "required": [++ "id",++ "observed_at",++ "price",++ "currency",++ "availability"],-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."-- },-- "created_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"-- },-- "updated_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"-- },-- "started_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",-- "description": "The first attempt's start."-- },-- "finished_at": {-- "type": "string",-- "format": "date-time",-- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "observed_at": {++ "type": "string",++ "format": "date-time"++ },++ "price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Decimal text as stated; null when the observation stated no price."++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$"++ },++ "availability": {++ "enum": [++ "in_stock",++ "out_of_stock",++ "preorder",++ "discontinued",++ "limited",++ "sold_out",++ "online_only",++ "in_store_only",++ null++ ]++ }++ }}}}},-- "media.job.failed": {++ "trade.alert.triggered": {"version": "1.0.0",-- "owner": "media",++ "owner": "trade","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",-- "title": "MediaJobFailedPayload",-- "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.alert.triggered.v1.json",++ "title": "TradeAlertTriggeredPayload",++ "description": "trade.alert.triggered v1 (OpenVibe.Trade server/domain/alerts.js deliver). One delivery of one private alert rule: a threshold crossed by a new observation (trigger kind observation), or a newly seen document of the instrument (filing_type / new_document rules, trigger kind document). Written in the transaction that recorded the observation or document; at most one per (rule, trigger). Envelope: subject { type: user, id: <rule owner usr_…> }, visibility subject, priority important, actor service:trade. Data only, never rendered notification text (ADR-020): OpenVibe.Network renders a TRADE_ALERT notification (server/notifications/events-consumer.js). Information only, not investment advice.","type": "object","required": [-- "id",-- "app_id",-- "object_id",-- "type",-- "status",-- "attempts",-- "max_attempts",-- "error_code",-- "cancel_requested",-- "has_result",-- "run_after",-- "decided_at",-- "created_at",-- "updated_at",-- "started_at",-- "finished_at"++ "delivery_id",++ "rule",++ "instrument",++ "trigger",++ "disclaimer"],"additionalProperties": false,"properties": {-- "id": {-- "type": "string",-- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."-- },-- "app_id": {++ "delivery_id": {"type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ "pattern": "^ald_[0-9A-HJKMNP-TV-Z]{26}$"},-- "object_id": {-- "type": [-- "string",-- "null"++ "rule": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "id",++ "kind",++ "status",++ "instrument",++ "metric",++ "operator",++ "threshold",++ "unit",++ "currency",++ "form_types",++ "armed",++ "created_at",++ "updated_at"],-- "maxLength": 200,-- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",-- "maxLength": 80,-- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."-- },-- "status": {-- "const": "failed"-- },-- "attempts": {-- "type": "integer",-- "minimum": 1,-- "description": "Attempts started, the last failed one included."-- },-- "max_attempts": {-- "type": "integer",-- "minimum": 1,-- "maximum": 10-- },-- "error_code": {-- "type": [-- "string",-- "null"++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^alr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "kind": {++ "enum": [++ "threshold",++ "filing_type",++ "new_document"++ ]++ },++ "status": {++ "enum": [++ "active",++ "deleted"++ ]++ },++ "instrument": {++ "type": "object",++ "required": [++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "symbol": {++ "type": "string"++ },++ "name": {++ "type": "string"++ }++ }++ },++ "metric": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 80,++ "description": "Threshold rules: e.g. price.close, volume, us-gaap:Revenues."++ },++ "operator": {++ "enum": [++ "above",++ "below",++ null++ ]++ },++ "threshold": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 64,++ "description": "Decimal text as entered."++ },++ "unit": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 20++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$"++ },++ "form_types": {++ "type": [++ "array",++ "null"++ ],++ "items": {++ "type": "string",++ "maxLength": 20++ },++ "description": "filing_type rules: the 1–10 form types that trigger it (10-K, 8-K…)."++ },++ "armed": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Threshold rules: armed before this delivery (always true when it fired); null for document rules."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ },++ "instrument": {++ "type": "object",++ "required": [++ "id",++ "symbol",++ "name",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "symbol": {++ "type": "string",++ "minLength": 1,++ "maxLength": 32++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Instrument page on Trade."++ }++ }++ },++ "trigger": {++ "oneOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "id",++ "metric",++ "value",++ "unit",++ "currency",++ "observed_at",++ "retrieved_at",++ "source_key",++ "source_url"++ ],++ "properties": {++ "kind": {++ "const": "observation"++ },++ "id": {++ "type": "string",++ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "metric": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "value": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64,++ "description": "The decimal exactly as the source stated it."++ },++ "unit": {++ "type": "string",++ "minLength": 1,++ "maxLength": 20++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$"++ },++ "observed_at": {++ "type": "string",++ "format": "date-time"++ },++ "retrieved_at": {++ "type": "string",++ "format": "date-time"++ },++ "source_key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 128,++ "description": "OpenVibe.Sources source key."++ },++ "source_url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2048++ }++ }++ },++ {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "kind",++ "id",++ "form_type",++ "title",++ "url",++ "published_at",++ "retrieved_at",++ "source_key"++ ],++ "properties": {++ "kind": {++ "const": "document"++ },++ "id": {++ "type": "string",++ "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "form_type": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 20++ },++ "title": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 1000++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2048++ },++ "published_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "The source's own date; null if it gave none."++ },++ "retrieved_at": {++ "type": "string",++ "format": "date-time"++ },++ "source_key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 128++ }++ }++ }++ ]++ },++ "disclaimer": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "Currently \"Information only — not investment advice; no trading here.\""++ }++ }++ }++ },++ "tools.job.created": {++ "version": "1.0.0",++ "owner": "tools",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.created.v1.json",++ "title": "ToolsJobCreatedPayload",++ "description": "tools.job.created v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A job was accepted and queued (POST /api/v1/jobs, or a retry of a failed job, which carries retry_of). An Idempotency-Key replay creates no job and no event. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor the owner when it is a subject (user, service, app or mod), else service:tools.",++ "type": "object",++ "required": [++ "job_id",++ "service",++ "type",++ "type_version",++ "state",++ "owner",++ "attempts",++ "max_attempts",++ "created_at",++ "retry_of"++ ],++ "additionalProperties": false,++ "properties": {++ "job_id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$",++ "description": "The Tools satellite that runs the job (img, audio, docs…)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type, e.g. img.process."++ },++ "type_version": {++ "type": "integer",++ "minimum": 1++ },++ "state": {++ "const": "queued"++ },++ "owner": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."++ },++ "attempts": {++ "type": "integer",++ "minimum": 0++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "retry_of": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ }++ }++ }++ },++ "tools.job.started": {++ "version": "1.0.0",++ "owner": "tools",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.started.v1.json",++ "title": "ToolsJobStartedPayload",++ "description": "tools.job.started v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A worker claimed the job and it is running (attempts counts this attempt). A job requeued after a restart starts again and is announced again. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:tools.",++ "type": "object",++ "required": [++ "job_id",++ "service",++ "type",++ "type_version",++ "state",++ "owner",++ "attempts",++ "max_attempts",++ "created_at",++ "retry_of",++ "started_at"++ ],++ "additionalProperties": false,++ "properties": {++ "job_id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$",++ "description": "The Tools satellite that runs the job (img, audio, docs…)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type, e.g. img.process."++ },++ "type_version": {++ "type": "integer",++ "minimum": 1++ },++ "state": {++ "const": "running"++ },++ "owner": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."++ },++ "attempts": {++ "type": "integer",++ "minimum": 0++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "retry_of": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ },++ "started_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "tools.job.succeeded": {++ "version": "1.0.0",++ "owner": "tools",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.succeeded.v1.json",++ "title": "ToolsJobSucceededPayload",++ "description": "tools.job.succeeded v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job finished and its result files are stored (in Media when TOOLS_JOB_RESULTS=media). Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",++ "type": "object",++ "required": [++ "job_id",++ "service",++ "type",++ "type_version",++ "state",++ "owner",++ "attempts",++ "max_attempts",++ "created_at",++ "retry_of",++ "started_at",++ "finished_at",++ "expires_at",++ "result"++ ],++ "additionalProperties": false,++ "properties": {++ "job_id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$",++ "description": "The Tools satellite that runs the job (img, audio, docs…)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type, e.g. img.process."++ },++ "type_version": {++ "type": "integer",++ "minimum": 1++ },++ "state": {++ "const": "succeeded"++ },++ "owner": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."++ },++ "attempts": {++ "type": "integer",++ "minimum": 0++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "retry_of": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ },++ "started_at": {++ "type": "string",++ "format": "date-time"++ },++ "finished_at": {++ "type": "string",++ "format": "date-time"++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."++ },++ "result": {++ "type": "object",++ "required": [++ "files"++ ],++ "additionalProperties": false,++ "description": "Where the result files are, never their content, names or the tool's output data (GET /api/v1/jobs/:id has those, for the owner).",++ "properties": {++ "files": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "object",++ "required": [++ "index",++ "mime",++ "size",++ "sha256",++ "storage",++ "media"++ ],++ "additionalProperties": false,++ "properties": {++ "index": {++ "type": "integer",++ "minimum": 0,++ "description": "n in /api/v1/jobs/:id/files/:n."++ },++ "mime": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "size": {++ "type": "integer",++ "minimum": 0++ },++ "sha256": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$"++ },++ "storage": {++ "enum": [++ "local",++ "media"++ ],++ "description": "media: stored as an OpenVibe.Media object (TOOLS_JOB_RESULTS=media); local: kept on the satellite until the job expires."++ },++ "media": {++ "oneOf": [++ {++ "$ref": "../../media/media-ref.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "{ media_id, role: output } when storage is media."++ }++ }++ }++ }++ }++ }++ }++ }++ },++ "tools.job.failed": {++ "version": "1.0.0",++ "owner": "tools",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.failed.v1.json",++ "title": "ToolsJobFailedPayload",++ "description": "tools.job.failed v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job failed: the tool refused the input, it timed out, or it was running when the satellite restarted and its type does not requeue. A cancelled job is not a failure and is not announced as one. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",++ "type": "object",++ "required": [++ "job_id",++ "service",++ "type",++ "type_version",++ "state",++ "owner",++ "attempts",++ "max_attempts",++ "created_at",++ "retry_of",++ "started_at",++ "finished_at",++ "expires_at",++ "error",++ "retryable"++ ],++ "additionalProperties": false,++ "properties": {++ "job_id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$",++ "description": "The Tools satellite that runs the job (img, audio, docs…)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type, e.g. img.process."++ },++ "type_version": {++ "type": "integer",++ "minimum": 1++ },++ "state": {++ "const": "failed"++ },++ "owner": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."++ },++ "attempts": {++ "type": "integer",++ "minimum": 0++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "retry_of": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "null when the job failed before it ever ran."++ },++ "finished_at": {++ "type": "string",++ "format": "date-time"++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."++ },++ "error": {++ "type": "object",++ "required": [++ "status",++ "code",++ "detail"++ ],++ "additionalProperties": false,++ "description": "The job's problem (errors.problem@1) without request or trace ids.",++ "properties": {++ "status": {++ "type": "integer",++ "minimum": 400,++ "maximum": 599++ },++ "code": {++ "type": "string",++ "pattern": "^tools\\.[a-z0-9_.]+$",++ "description": "tools.job.failed, tools.job.timeout, or a code the tool chose (tools.…: tools.pdf.too_many_pages, tools.pdf.wrong_password…)."++ },++ "detail": {++ "type": "string",++ "maxLength": 500,++ "description": "Human-readable reason; server paths are scrubbed."++ }++ }++ },++ "retryable": {++ "type": "boolean"++ }++ }++ }++ },++ "live.stream.started": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.stream.started.v1.json",++ "title": "LiveStreamStartedPayload",++ "description": "live.stream.started v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js createStream). A streams row went live: WebRTC/JSMPEG from the dashboard, RTMP ingest, WHIP, or an OpenRe.Stream session mirrored into Live. Written to Live's event_outbox in the transaction that inserts the row, so the event exists if and only if the stream did. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 1 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",++ "type": "object",++ "required": [++ "stream_id",++ "channel",++ "title",++ "category",++ "protocol",++ "is_nsfw",++ "started_at"++ ],++ "additionalProperties": false,++ "properties": {++ "stream_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live's streams row id (the envelope subject id)."++ },++ "channel": {++ "type": "object",++ "required": [++ "username",++ "display_name",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "description": "The streamer's Live username (the channel slug)."++ },++ "display_name": {++ "type": "string",++ "minLength": 1,++ "description": "The display name, or the username when there is none."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://openvibe\\.live/",++ "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."++ },++ "subject": {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."++ }++ }++ },++ "title": {++ "type": "string"++ },++ "category": {++ "type": [++ "string",++ "null"++ ],++ "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."++ },++ "protocol": {++ "enum": [++ "jsmpeg",++ "webrtc",++ "rtmp"++ ],++ "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."++ },++ "is_nsfw": {++ "type": "boolean"++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "UTC. null only for a row without a start time, which createStream never writes."++ }++ }++ }++ },++ "live.stream.ended": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json",++ "title": "LiveStreamEndedPayload",++ "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",++ "type": "object",++ "required": [++ "stream_id",++ "channel",++ "title",++ "category",++ "protocol",++ "is_nsfw",++ "started_at",++ "ended_at",++ "duration_seconds"++ ],++ "additionalProperties": false,++ "properties": {++ "stream_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live's streams row id (the envelope subject id)."++ },++ "channel": {++ "type": "object",++ "required": [++ "username",++ "display_name",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "description": "The streamer's Live username (the channel slug)."++ },++ "display_name": {++ "type": "string",++ "minLength": 1,++ "description": "The display name, or the username when there is none."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://openvibe\\.live/",++ "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."++ },++ "subject": {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },++ "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."++ }++ }++ },++ "title": {++ "type": "string"++ },++ "category": {++ "type": [++ "string",++ "null"++ ],++ "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."++ },++ "protocol": {++ "enum": [++ "jsmpeg",++ "webrtc",++ "rtmp"++ ],++ "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."++ },++ "is_nsfw": {++ "type": "boolean"++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "UTC. null only for a row without a start time, which createStream never writes."++ },++ "ended_at": {++ "type": "string",++ "format": "date-time",++ "description": "UTC."++ },++ "duration_seconds": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "Whole seconds from started_at to ended_at; null when the row has no start time."++ }++ }++ }++ },++ "live.release.deployed": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/live.release.deployed.v1.json",++ "title": "LiveReleaseDeployedPayload",++ "description": "live.release.deployed v1 (OpenVibe.Live server/events/release-events.js envelopeFor, called by server/chat/deploy-notice.js announce). The first boot that runs commits never announced before: queued in the transaction that records them as announced (site setting deploy_last_announced), so a restart without new code emits nothing and a crash before that commit announces again on the next boot with the same subject. A consumer keys on subject.id (the head commit) to fold repeats. The commit subjects are public already (Live chat and /updates); the consumer decides what is shown. Envelope: subject { type: release, id: <head commit, 40 hex> }, visibility internal, priority low, actor service:live.",++ "type": "object",++ "required": [++ "service",++ "release",++ "commit",++ "previous",++ "commit_count",++ "commits",++ "deployed_at",++ "notes_url"++ ],++ "additionalProperties": false,++ "properties": {++ "service": {++ "const": "live"++ },++ "release": {++ "type": "string",++ "pattern": "^[0-9a-f]{7,40}$",++ "description": "The head commit's short sha as git abbreviated it (its first 12 characters when the head is not in the list)."++ },++ "commit": {++ "type": "string",++ "pattern": "^[0-9a-f]{40}$",++ "description": "The head commit (the envelope subject id)."++ },++ "previous": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[0-9a-f]{40}$",++ "description": "The head announced before this one; null on the first announcement."++ },++ "commit_count": {++ "type": "integer",++ "minimum": 1,++ "maximum": 40,++ "description": "How many new commits were found (git log previous..head, at most 40; only the head on a first run or after history was rewritten)."++ },++ "commits": {++ "type": "array",++ "minItems": 1,++ "maxItems": 40,++ "description": "The new commits, newest first.",++ "items": {++ "type": "object",++ "required": [++ "hash",++ "short",++ "subject",++ "date"++ ],++ "additionalProperties": false,++ "properties": {++ "hash": {++ "type": "string",++ "pattern": "^[0-9a-f]{40}$"++ },++ "short": {++ "type": "string",++ "pattern": "^[0-9a-f]{4,40}$"++ },++ "subject": {++ "type": "string",++ "maxLength": 200,++ "description": "The commit subject, cut to 200 characters."++ },++ "date": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "Author date (git %aI, with the author's UTC offset)."++ }++ }++ }++ },++ "deployed_at": {++ "type": "string",++ "format": "date-time",++ "description": "When this boot announced it (UTC)."++ },++ "notes_url": {++ "const": "https://openvibe.live/updates"++ }++ }++ }++ },++ "media.job.proposed": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",++ "title": "MediaJobProposedPayload",++ "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "object_id",++ "type",++ "status",++ "attempts",++ "max_attempts",++ "error_code",++ "cancel_requested",++ "has_result",++ "run_after",++ "decided_at",++ "created_at",++ "updated_at",++ "started_at",++ "finished_at"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "object_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ },++ "status": {++ "const": "proposed"++ },++ "attempts": {++ "type": "integer",++ "minimum": 0,++ "description": "Attempts started so far."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10++ },++ "error_code": {++ "type": "null"++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "Its owner asked a running job to stop."++ },++ "has_result": {++ "const": false,++ "description": "A proposal has no result."++ },++ "run_after": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "A queued job waits until then (retry backoff); null otherwise."++ },++ "decided_at": {++ "type": "null",++ "description": "Nobody has decided yet."++ },++ "created_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "started_at": {++ "type": "null"++ },++ "finished_at": {++ "type": "null"++ }++ }++ }++ },++ "media.job.queued": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",++ "title": "MediaJobQueuedPayload",++ "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "object_id",++ "type",++ "status",++ "attempts",++ "max_attempts",++ "error_code",++ "cancel_requested",++ "has_result",++ "run_after",++ "decided_at",++ "created_at",++ "updated_at",++ "started_at",++ "finished_at"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "object_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ },++ "status": {++ "const": "queued"++ },++ "attempts": {++ "type": "integer",++ "minimum": 0,++ "description": "Attempts started so far."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10++ },++ "error_code": {++ "type": "null"++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "Its owner asked a running job to stop."++ },++ "has_result": {++ "const": false,++ "description": "A job that never ran has no result."++ },++ "run_after": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "A queued job waits until then (retry backoff); null otherwise."++ },++ "decided_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."++ },++ "created_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "started_at": {++ "type": "null"++ },++ "finished_at": {++ "type": "null"++ }++ }++ }++ },++ "media.job.started": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",++ "title": "MediaJobStartedPayload",++ "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "object_id",++ "type",++ "status",++ "attempts",++ "max_attempts",++ "error_code",++ "cancel_requested",++ "has_result",++ "run_after",++ "decided_at",++ "created_at",++ "updated_at",++ "started_at",++ "finished_at"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "object_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ },++ "status": {++ "const": "running",++ "description": "started carries the job's state, running."++ },++ "attempts": {++ "type": "integer",++ "minimum": 1,++ "description": "Attempts started so far, this one included."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10++ },++ "error_code": {++ "type": [++ "string",++ "null"++ ],++ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "Its owner asked a running job to stop."++ },++ "has_result": {++ "const": false,++ "description": "A running job has no result yet."++ },++ "run_after": {++ "type": "null",++ "description": "Cleared when the job is claimed."++ },++ "decided_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."++ },++ "created_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "started_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."++ },++ "finished_at": {++ "type": "null"++ }++ }++ }++ },++ "media.job.retrying": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",++ "title": "MediaJobRetryingPayload",++ "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "object_id",++ "type",++ "status",++ "attempts",++ "max_attempts",++ "error_code",++ "cancel_requested",++ "has_result",++ "run_after",++ "decided_at",++ "created_at",++ "updated_at",++ "started_at",++ "finished_at"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "object_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ },++ "status": {++ "const": "queued",++ "description": "retrying carries the job's state, queued."++ },++ "attempts": {++ "type": "integer",++ "minimum": 1,++ "description": "Attempts started so far, the failed one included."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10++ },++ "error_code": {++ "type": [++ "string",++ "null"++ ],++ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "Its owner asked a running job to stop."++ },++ "has_result": {++ "const": false,++ "description": "A job waiting for its retry has no result."++ },++ "run_after": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "Not before this time (the retry backoff)."++ },++ "decided_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."++ },++ "created_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "started_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."++ },++ "finished_at": {++ "type": "null"++ }++ }++ }++ },++ "media.job.succeeded": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",++ "title": "MediaJobSucceededPayload",++ "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "object_id",++ "type",++ "status",++ "attempts",++ "max_attempts",++ "error_code",++ "cancel_requested",++ "has_result",++ "run_after",++ "decided_at",++ "created_at",++ "updated_at",++ "started_at",++ "finished_at"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "object_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ },++ "status": {++ "const": "succeeded"++ },++ "attempts": {++ "type": "integer",++ "minimum": 1,++ "description": "Attempts started, the successful one included."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10++ },++ "error_code": {++ "type": "null",++ "description": "A success clears the last failure."++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "Its owner asked a running job to stop."++ },++ "has_result": {++ "type": "boolean",++ "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it."++ },++ "run_after": {++ "type": "null"++ },++ "decided_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."++ },++ "created_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ },++ "started_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",++ "description": "The first attempt's start."++ },++ "finished_at": {++ "type": "string",++ "format": "date-time",++ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"++ }++ }++ }++ },++ "media.job.failed": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",++ "title": "MediaJobFailedPayload",++ "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "object_id",++ "type",++ "status",++ "attempts",++ "max_attempts",++ "error_code",++ "cancel_requested",++ "has_result",++ "run_after",++ "decided_at",++ "created_at",++ "updated_at",++ "started_at",++ "finished_at"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "object_id": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 200,++ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",++ "maxLength": 80,++ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."++ },++ "status": {++ "const": "failed"++ },++ "attempts": {++ "type": "integer",++ "minimum": 1,++ "description": "Attempts started, the last failed one included."++ },++ "max_attempts": {++ "type": "integer",++ "minimum": 1,++ "maximum": 10++ },++ "error_code": {++ "type": [++ "string",++ "null"],"description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."},@@@ -14248,337 +15914,726 @@"maxLength": 2048,"description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."},-- "previous_status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed"++ "previous_status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed"++ ],++ "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."++ },++ "reason": {++ "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",++ "anyOf": [++ {++ "enum": [++ "expiry",++ "evidence",++ "report",++ "decay",++ "staff"++ ]++ },++ {++ "type": "string",++ "pattern": "^service:[\\s\\S]+$",++ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ }++ ]++ }++ }++ }++ },++ "coupons.coupon.disabled": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",++ "title": "CouponsCouponDisabledPayload",++ "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "status",++ "confidence",++ "review_state",++ "expires_at",++ "canonical_url",++ "previous_status",++ "reason"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "status": {++ "const": "disabled"++ },++ "confidence": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."++ },++ "review_state": {++ "enum": [++ "pending",++ "published"++ ]++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "Known expiry (UTC); null = expiry unknown."++ },++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."++ },++ "previous_status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed",++ "expired"++ ],++ "description": "Status before it was disabled."++ },++ "reason": {++ "description": "source_removed, staff, or service:<client>[:<note>].",++ "anyOf": [++ {++ "enum": [++ "source_removed",++ "staff"++ ]++ },++ {++ "type": "string",++ "pattern": "^service:[\\s\\S]+$",++ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ }++ ]++ }++ }++ }++ },++ "coupons.report.created": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",++ "title": "CouponsReportCreatedPayload",++ "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "outcome",++ "channel",++ "day"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant of the reported code."++ },++ "outcome": {++ "enum": [++ "worked",++ "failed"++ ]++ },++ "reason": {++ "enum": [++ "invalid",++ "expired",++ "min_spend_not_met",++ "not_eligible",++ "other"++ ],++ "description": "Why a failed code failed, when the reporter said. Never on a worked report."++ },++ "channel": {++ "enum": [++ "site",++ "extension",++ "api"++ ],++ "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."++ },++ "day": {++ "type": "string",++ "format": "date",++ "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."++ }++ },++ "if": {++ "properties": {++ "outcome": {++ "const": "worked"++ }++ },++ "required": [++ "outcome"++ ]++ },++ "then": {++ "not": {++ "required": [++ "reason"++ ]++ }++ }++ }++ },++ "coupons.confidence.changed": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",++ "title": "CouponsConfidenceChangedPayload",++ "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "from",++ "to",++ "status"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "from": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Confidence before; null = no counted report."++ },++ "to": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Confidence after; null = no counted report left in the window."++ },++ "status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed",++ "expired",++ "disabled"++ ],++ "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."++ }++ }++ }++ },++ "coupons.index_document.upserted": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",++ "title": "CouponsIndexDocumentUpsertedPayload",++ "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object",++ "required": [++ "owner",++ "type",++ "deleted"],-- "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."-- },-- "reason": {-- "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",-- "anyOf": [-- {++ "properties": {++ "owner": {++ "const": "coupons"++ },++ "type": {"enum": [-- "expiry",-- "evidence",-- "report",-- "decay",-- "staff"++ "coupon",++ "merchant"]},-- {++ "id": {"type": "string",-- "pattern": "^service:[\\s\\S]+$",-- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "deleted": {++ "const": false}++ }++ }++ ]++ }++ },++ "coupons.index_document.deleted": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",++ "title": "CouponsIndexDocumentDeletedPayload",++ "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "enum": [++ "coupon",++ "merchant"]++ },++ "id": {++ "type": "string",++ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."}}}},-- "coupons.coupon.disabled": {++ "deals.offer.created": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "deals","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",-- "title": "CouponsCouponDisabledPayload",-- "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",++ "title": "DealsOfferCreatedPayload",++ "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.","type": "object","required": [-- "merchant_id",++ "offer_id",++ "slug",++ "url",++ "title","status",-- "confidence",-- "review_state",-- "expires_at",-- "canonical_url",-- "previous_status",-- "reason"++ "store",++ "product_id",++ "latest_observation",++ "freshness",++ "indexability"],"additionalProperties": false,"properties": {-- "merchant_id": {++ "offer_id": {"type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer id."++ },++ "slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 80,++ "description": "Page slug: the slugified title plus the last six characters of the id."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical offer page URL on Deals (/d/<slug>)."++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200},"status": {-- "const": "disabled"++ "const": "active",++ "description": "A new offer is always active."},-- "confidence": {++ "store": {"type": [-- "number",++ "string","null"],-- "minimum": 0,-- "maximum": 1,-- "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."-- },-- "review_state": {-- "enum": [-- "pending",-- "published"-- ]++ "minLength": 1,++ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."},-- "expires_at": {++ "product_id": {"type": ["string","null"],-- "format": "date-time",-- "description": "Known expiry (UTC); null = expiry unknown."++ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Deals product the offer is attached to, if any."},-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."++ "latest_observation": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "price",++ "currency",++ "availability",++ "observed_at"++ ],++ "additionalProperties": false,++ "description": "The newest observation across the offer's merge group; null only when the group has none.",++ "properties": {++ "price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Decimal text as stated; null when no price was stated (never 0)."++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217; never assumed."++ },++ "availability": {++ "enum": [++ "in_stock",++ "out_of_stock",++ "preorder",++ "discontinued",++ "limited",++ "sold_out",++ "online_only",++ "in_store_only",++ null++ ]++ },++ "observed_at": {++ "type": "string",++ "format": "date-time"++ }++ }},-- "previous_status": {++ "freshness": {"enum": [-- "unknown",-- "reported_working",-- "reported_failed",-- "expired"++ "fresh",++ "stale",++ "unobserved"],-- "description": "Status before it was disabled."++ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."},-- "reason": {-- "description": "source_removed, staff, or service:<client>[:<note>].",-- "anyOf": [-- {++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",++ "properties": {++ "decision": {"enum": [-- "source_removed",-- "staff"++ "index",++ "noindex"]},-- {++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ },++ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ }++ }++ },++ "imported_from": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "additionalProperties": false,++ "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",++ "properties": {++ "service": {++ "const": "sources"++ },++ "type": {++ "const": "item"++ },++ "id": {"type": "string",-- "pattern": "^service:[\\s\\S]+$",-- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"}-- ]++ }}}}},-- "coupons.report.created": {++ "deals.offer.updated": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "deals","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",-- "title": "CouponsReportCreatedPayload",-- "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",++ "title": "DealsOfferUpdatedPayload",++ "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.","type": "object","required": [-- "merchant_id",-- "outcome",-- "channel",-- "day"++ "offer_id",++ "slug",++ "url",++ "title",++ "status",++ "store",++ "product_id",++ "latest_observation",++ "freshness",++ "indexability",++ "changed"],"additionalProperties": false,"properties": {-- "merchant_id": {++ "offer_id": {"type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant of the reported code."-- },-- "outcome": {-- "enum": [-- "worked",-- "failed"-- ]++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer id."},-- "reason": {-- "enum": [-- "invalid",-- "expired",-- "min_spend_not_met",-- "not_eligible",-- "other"-- ],-- "description": "Why a failed code failed, when the reporter said. Never on a worked report."++ "slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 80,++ "description": "Page slug: the slugified title plus the last six characters of the id."},-- "channel": {-- "enum": [-- "site",-- "extension",-- "api"-- ],-- "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical offer page URL on Deals (/d/<slug>)."},-- "day": {++ "title": {"type": "string",-- "format": "date",-- "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."-- }-- },-- "if": {-- "properties": {-- "outcome": {-- "const": "worked"-- }++ "minLength": 1,++ "maxLength": 200},-- "required": [-- "outcome"-- ]-- },-- "then": {-- "not": {-- "required": [-- "reason"++ "status": {++ "enum": [++ "active",++ "expired",++ "disabled"]-- }-- }-- }-- },-- "coupons.confidence.changed": {-- "version": "1.0.0",-- "owner": "coupons",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",-- "title": "CouponsConfidenceChangedPayload",-- "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",-- "type": "object",-- "required": [-- "merchant_id",-- "from",-- "to",-- "status"-- ],-- "additionalProperties": false,-- "properties": {-- "merchant_id": {-- "type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."},-- "from": {++ "store": {"type": [-- "number",++ "string","null"],-- "minimum": 0,-- "maximum": 1,-- "description": "Confidence before; null = no counted report."++ "minLength": 1,++ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."},-- "to": {++ "product_id": {"type": [-- "number",++ "string","null"],-- "minimum": 0,-- "maximum": 1,-- "description": "Confidence after; null = no counted report left in the window."++ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Deals product the offer is attached to, if any."},-- "status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed",-- "expired",-- "disabled"++ "latest_observation": {++ "type": [++ "object",++ "null"],-- "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."-- }-- }-- }-- },-- "coupons.index_document.upserted": {-- "version": "1.0.0",-- "owner": "coupons",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",-- "title": "CouponsIndexDocumentUpsertedPayload",-- "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"-- },-- {-- "type": "object","required": [-- "owner",-- "type",-- "deleted"++ "price",++ "currency",++ "availability",++ "observed_at"],++ "additionalProperties": false,++ "description": "The newest observation across the offer's merge group; null only when the group has none.","properties": {-- "owner": {-- "const": "coupons"++ "price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Decimal text as stated; null when no price was stated (never 0)."},-- "type": {++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217; never assumed."++ },++ "availability": {"enum": [-- "coupon",-- "merchant"++ "in_stock",++ "out_of_stock",++ "preorder",++ "discontinued",++ "limited",++ "sold_out",++ "online_only",++ "in_store_only",++ null]},-- "id": {++ "observed_at": {"type": "string",-- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ "format": "date-time"++ }++ }++ },++ "freshness": {++ "enum": [++ "fresh",++ "stale",++ "unobserved"++ ],++ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]},-- "deleted": {-- "const": false++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ },++ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."}}-- }-- ]-- }-- },-- "coupons.index_document.deleted": {-- "version": "1.0.0",-- "owner": "coupons",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",-- "title": "CouponsIndexDocumentDeletedPayload",-- "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",-- "type": "object",-- "required": [-- "type",-- "id",-- "revision"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {++ },++ "changed": {++ "type": "array",++ "minItems": 1,++ "uniqueItems": true,++ "items": {++ "enum": [++ "title",++ "description",++ "category",++ "expires_at",++ "product_id",++ "ai_summary",++ "review_state",++ "text_origin",++ "observation",++ "sources",++ "status",++ "merged",++ "unmerged"++ ]++ },++ "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."++ },++ "observation_id": {++ "type": "string",++ "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."++ },++ "source_removed": {++ "type": "string",++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."++ },++ "moderation": {"enum": [-- "coupon",-- "merchant"-- ]++ "disabled",++ "enabled"++ ],++ "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."},-- "id": {++ "merged_offer_id": {"type": "string",-- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [merged]: the duplicate now merged into this offer."},-- "revision": {-- "type": "integer",-- "minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ "unmerged_offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [unmerged], on the former target: the offer split back out."++ },++ "unmerged_from": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [unmerged], on the restored offer: the offer it had been merged into."}}}},-- "deals.offer.created": {++ "deals.offer.expired": {"version": "1.0.0","owner": "deals","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",-- "title": "DealsOfferCreatedPayload",-- "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",++ "title": "DealsOfferExpiredPayload",++ "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.","type": "object","required": ["offer_id",@@@ -14590,7 +16645,8 @@"product_id","latest_observation","freshness",-- "indexability"++ "indexability",++ "reason"],"additionalProperties": false,"properties": {@@@ -14618,8 +16674,7 @@"maxLength": 200},"status": {-- "const": "active",-- "description": "A new offer is always active."++ "const": "expired"},"store": {"type": [@@@ -14721,2642 +16776,3835 @@}}},-- "imported_from": {++ "reason": {++ "type": "string",++ "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",++ "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."++ }++ }++ }++ },++ "deals.vote.changed": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",++ "title": "DealsVoteChangedPayload",++ "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",++ "type": "object",++ "required": [++ "offer_id",++ "listing_id",++ "value",++ "previous",++ "weight",++ "votes",++ "hotness"++ ],++ "additionalProperties": false,++ "properties": {++ "offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer of the merge group; the vote row is stored on it."++ },++ "listing_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The offer the request addressed: the root, or an offer merged into it."++ },++ "value": {++ "enum": [++ 1,++ -1,++ 0++ ],++ "description": "The new effective vote; 0 = removed."++ },++ "previous": {++ "enum": [++ 1,++ -1,++ 0++ ],++ "description": "The effective vote before; always different from value."++ },++ "weight": {++ "type": "number",++ "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."++ },++ "votes": {"type": "object","required": [-- "service",-- "type",-- "id"++ "up",++ "down",++ "up_weight",++ "down_weight"],"additionalProperties": false,-- "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",++ "description": "Tally of effective votes across the merge group after the change (a voter counts once).","properties": {-- "service": {-- "const": "sources"++ "up": {++ "type": "integer",++ "minimum": 0++ },++ "down": {++ "type": "integer",++ "minimum": 0++ },++ "up_weight": {++ "type": "number",++ "description": "Sum of the up voters' weights, 4 decimals."++ },++ "down_weight": {++ "type": "number",++ "description": "Sum of the down voters' weights, 4 decimals."++ }++ }++ },++ "hotness": {++ "type": "object",++ "required": [++ "formula",++ "hot",++ "score"++ ],++ "additionalProperties": false,++ "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",++ "properties": {++ "formula": {++ "type": "string",++ "pattern": "^hot@[0-9]+$",++ "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."++ },++ "hot": {++ "type": "number"++ },++ "score": {++ "type": "number",++ "description": "up_weight - down_weight, 4 decimals."++ }++ }++ }++ }++ }++ },++ "deals.index_document.upserted": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",++ "title": "DealsIndexDocumentUpsertedPayload",++ "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object",++ "required": [++ "owner",++ "type",++ "deleted"++ ],++ "properties": {++ "owner": {++ "const": "deals"},"type": {-- "const": "item"++ "enum": [++ "offer",++ "product"++ ]},"id": {"type": "string",-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "deleted": {++ "const": false}}}++ ]++ }++ },++ "deals.index_document.deleted": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",++ "title": "DealsIndexDocumentDeletedPayload",++ "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "offer"++ },++ "id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ }}}},-- "deals.offer.updated": {++ "trade.observation.created": {"version": "1.0.0",-- "owner": "deals",++ "owner": "trade","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",-- "title": "DealsOfferUpdatedPayload",-- "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",++ "title": "TradeObservationCreatedPayload",++ "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.","type": "object","required": [-- "offer_id",-- "slug",-- "url",-- "title",-- "status",-- "store",-- "product_id",-- "latest_observation",-- "freshness",-- "indexability",-- "changed"++ "instrument",++ "id",++ "metric",++ "value",++ "unit",++ "currency",++ "period",++ "observed_at",++ "retrieved_at",++ "recorded_at",++ "max_age_sec",++ "source"],"additionalProperties": false,"properties": {-- "offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer id."++ "instrument": {++ "type": "object",++ "required": [++ "id",++ "symbol"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "symbol": {++ "type": "string",++ "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"++ }++ }},-- "slug": {++ "id": {"type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 80,-- "description": "Page slug: the slugified title plus the last six characters of the id."++ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"},-- "url": {++ "metric": {"type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical offer page URL on Deals (/d/<slug>)."++ "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",++ "description": "e.g. price.close, volume, us-gaap:Revenues."},-- "title": {++ "value": {"type": "string",-- "minLength": 1,-- "maxLength": 200++ "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",++ "maxLength": 64,++ "description": "The decimal exactly as the source stated it (text, never rounded)."},-- "status": {-- "enum": [-- "active",-- "expired",-- "disabled"-- ]++ "unit": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",++ "description": "e.g. USD, shares, USD/shares."},-- "store": {++ "currency": {"type": ["string","null"],-- "minLength": 1,-- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217 when the value is monetary; null otherwise."},-- "product_id": {++ "period": {"type": ["string","null"],-- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Deals product the offer is attached to, if any."++ "minLength": 1,++ "maxLength": 40,++ "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."},-- "latest_observation": {++ "observed_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the value was true, per the source."++ },++ "retrieved_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the source was fetched."++ },++ "recorded_at": {++ "type": "string",++ "format": "date-time",++ "description": "When Trade recorded the row."++ },++ "max_age_sec": {"type": [-- "object",++ "integer","null"],++ "minimum": 60,++ "maximum": 315360000,++ "description": "The value's own maximum age, when the recorder gave one."++ },++ "source": {++ "type": "object","required": [-- "price",-- "currency",-- "availability",-- "observed_at"++ "key",++ "item_id",++ "url",++ "ref"],"additionalProperties": false,-- "description": "The newest observation across the offer's merge group; null only when the group has none.","properties": {-- "price": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",++ "description": "OpenVibe.Sources source key."++ },++ "item_id": {"type": ["string","null"],-- "maxLength": 40,-- "description": "Decimal text as stated; null when no price was stated (never 0)."++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The Sources item the value came from, when there is one."},-- "currency": {++ "url": {"type": ["string","null"],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217; never assumed."-- },-- "availability": {-- "enum": [-- "in_stock",-- "out_of_stock",-- "preorder",-- "discontinued",-- "limited",-- "sold_out",-- "online_only",-- "in_store_only",-- null-- ]++ "pattern": "^https?://",++ "description": "The source's page for the value (absolute http(s) URL, normalised)."},-- "observed_at": {++ "ref": {"type": "string",-- "format": "date-time"-- }-- }-- },-- "freshness": {-- "enum": [-- "fresh",-- "stale",-- "unobserved"-- ],-- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."-- },-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]-- },-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- },-- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ "minLength": 1,++ "maxLength": 300,++ "description": "The source's own reference for this datum; unique per source_key."}}-- },-- "changed": {-- "type": "array",-- "minItems": 1,-- "uniqueItems": true,-- "items": {-- "enum": [-- "title",-- "description",-- "category",-- "expires_at",-- "product_id",-- "ai_summary",-- "review_state",-- "text_origin",-- "observation",-- "sources",-- "status",-- "merged",-- "unmerged"-- ]-- },-- "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."-- },-- "observation_id": {++ }++ }++ }++ },++ "trade.source.stale": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",++ "title": "TradeSourceStalePayload",++ "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",++ "type": "object",++ "required": [++ "source_key",++ "stale",++ "stale_since",++ "last_success_at",++ "stale_after_sec",++ "upstream_status",++ "evaluated_at"++ ],++ "additionalProperties": false,++ "properties": {++ "source_key": {"type": "string",-- "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",++ "description": "OpenVibe.Sources source key."},-- "source_removed": {++ "stale": {++ "const": true++ },++ "stale_since": {"type": "string",-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."++ "format": "date-time",++ "description": "Last success + window, or when Trade first saw the source if it never succeeded."},-- "moderation": {-- "enum": [-- "disabled",-- "enabled"++ "last_success_at": {++ "type": [++ "string",++ "null"],-- "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."++ "format": "date-time",++ "description": "Last known successful fetch; null when none is known."},-- "merged_offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [merged]: the duplicate now merged into this offer."++ "stale_after_sec": {++ "type": "integer",++ "description": "The staleness window applied, in seconds."},-- "unmerged_offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [unmerged], on the former target: the offer split back out."++ "upstream_status": {++ "type": [++ "string",++ "null"++ ],++ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."},-- "unmerged_from": {++ "evaluated_at": {"type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [unmerged], on the restored offer: the offer it had been merged into."++ "format": "date-time"}}}},-- "deals.offer.expired": {++ "trade.source.recovered": {"version": "1.0.0",-- "owner": "deals",++ "owner": "trade","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",-- "title": "DealsOfferExpiredPayload",-- "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",++ "title": "TradeSourceRecoveredPayload",++ "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.","type": "object","required": [-- "offer_id",-- "slug",-- "url",-- "title",-- "status",-- "store",-- "product_id",-- "latest_observation",-- "freshness",-- "indexability",-- "reason"++ "source_key",++ "stale",++ "stale_since",++ "last_success_at",++ "stale_after_sec",++ "upstream_status",++ "evaluated_at"],"additionalProperties": false,"properties": {-- "offer_id": {++ "source_key": {"type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer id."++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",++ "description": "OpenVibe.Sources source key."},-- "slug": {-- "type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 80,-- "description": "Page slug: the slugified title plus the last six characters of the id."++ "stale": {++ "const": false},-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical offer page URL on Deals (/d/<slug>)."++ "stale_since": {++ "type": "null",++ "description": "Always null: the source is fresh."},-- "title": {++ "last_success_at": {"type": "string",-- "minLength": 1,-- "maxLength": 200++ "format": "date-time",++ "description": "The successful fetch that made it fresh (or a later one)."},-- "status": {-- "const": "expired"++ "stale_after_sec": {++ "type": "integer",++ "description": "The staleness window applied, in seconds."},-- "store": {++ "upstream_status": {"type": ["string","null"],-- "minLength": 1,-- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."},-- "product_id": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Deals product the offer is attached to, if any."++ "evaluated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "trade.index_document.upserted": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",++ "title": "TradeIndexDocumentUpsertedPayload",++ "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"},-- "latest_observation": {-- "type": [-- "object",-- "null"-- ],++ {++ "type": "object","required": [-- "price",-- "currency",-- "availability",-- "observed_at"++ "owner",++ "type",++ "deleted"],-- "additionalProperties": false,-- "description": "The newest observation across the offer's merge group; null only when the group has none.","properties": {-- "price": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 40,-- "description": "Decimal text as stated; null when no price was stated (never 0)."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217; never assumed."++ "owner": {++ "const": "trade"},-- "availability": {-- "enum": [-- "in_stock",-- "out_of_stock",-- "preorder",-- "discontinued",-- "limited",-- "sold_out",-- "online_only",-- "in_store_only",-- null-- ]++ "type": {++ "const": "instrument"},-- "observed_at": {++ "id": {"type": "string",-- "format": "date-time"-- }-- }-- },-- "freshness": {-- "enum": [-- "fresh",-- "stale",-- "unobserved"-- ],-- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."-- },-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"},-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- },-- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ "deleted": {++ "const": false}}++ }++ ]++ }++ },++ "trade.index_document.deleted": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",++ "title": "TradeIndexDocumentDeletedPayload",++ "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "instrument"},-- "reason": {++ "id": {"type": "string",-- "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",-- "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."}}}},-- "deals.vote.changed": {++ "games.player.joined": {"version": "1.0.0",-- "owner": "deals",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",-- "title": "DealsVoteChangedPayload",-- "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",++ "title": "GamesPlayerJoinedPayload",++ "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.","type": "object","required": [-- "offer_id",-- "listing_id",-- "value",-- "previous",-- "weight",-- "votes",-- "hotness"++ "player",++ "world",++ "restored"],"additionalProperties": false,"properties": {-- "offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer of the merge group; the vote row is stored on it."++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }++ }++ ]},-- "listing_id": {++ "world": {"type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The offer the request addressed: the root, or an offer merged into it."-- },-- "value": {-- "enum": [-- 1,-- -1,-- 0-- ],-- "description": "The new effective vote; 0 = removed."-- },-- "previous": {-- "enum": [-- 1,-- -1,-- 0-- ],-- "description": "The effective vote before; always different from value."-- },-- "weight": {-- "type": "number",-- "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."},-- "votes": {-- "type": "object",-- "required": [-- "up",-- "down",-- "up_weight",-- "down_weight"-- ],-- "additionalProperties": false,-- "description": "Tally of effective votes across the merge group after the change (a voter counts once).",-- "properties": {-- "up": {-- "type": "integer",-- "minimum": 0-- },-- "down": {-- "type": "integer",-- "minimum": 0-- },-- "up_weight": {-- "type": "number",-- "description": "Sum of the up voters' weights, 4 decimals."++ "restored": {++ "type": "boolean",++ "description": "The character was loaded from the database (false: created by this join)."++ }++ }++ }++ },++ "games.player.left": {++ "version": "1.0.0",++ "owner": "games",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",++ "title": "GamesPlayerLeftPayload",++ "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "type": "object",++ "required": [++ "player",++ "world",++ "session_seconds"++ ],++ "additionalProperties": false,++ "properties": {++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }},-- "down_weight": {-- "type": "number",-- "description": "Sum of the down voters' weights, 4 decimals."++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }}-- }++ ]},-- "hotness": {-- "type": "object",-- "required": [-- "formula",-- "hot",-- "score"-- ],-- "additionalProperties": false,-- "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",-- "properties": {-- "formula": {-- "type": "string",-- "pattern": "^hot@[0-9]+$",-- "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."-- },-- "hot": {-- "type": "number"-- },-- "score": {-- "type": "number",-- "description": "up_weight - down_weight, 4 decimals."-- }-- }-- }-- }-- }-- },-- "deals.index_document.upserted": {-- "version": "1.0.0",-- "owner": "deals",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",-- "title": "DealsIndexDocumentUpsertedPayload",-- "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"++ "world": {++ "type": "string",++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."},-- {-- "type": "object",-- "required": [-- "owner",-- "type",-- "deleted"-- ],-- "properties": {-- "owner": {-- "const": "deals"-- },-- "type": {-- "enum": [-- "offer",-- "product"-- ]-- },-- "id": {-- "type": "string",-- "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "deleted": {-- "const": false-- }-- }++ "session_seconds": {++ "type": "integer",++ "minimum": 0}-- ]++ }}},-- "deals.index_document.deleted": {++ "games.skill.leveled": {"version": "1.0.0",-- "owner": "deals",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",-- "title": "DealsIndexDocumentDeletedPayload",-- "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",++ "title": "GamesSkillLeveledPayload",++ "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.","type": "object","required": [-- "type",-- "id",-- "revision"++ "player",++ "world",++ "skill",++ "level",++ "previous_level"],"additionalProperties": false,"properties": {-- "type": {-- "const": "offer"++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }++ }++ ]},-- "id": {++ "world": {"type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."},-- "revision": {++ "skill": {++ "type": "string",++ "pattern": "^[a-z0-9_]+$",++ "description": "Content skill id (e.g. mining, woodcutting)."++ },++ "level": {++ "type": "integer",++ "minimum": 2,++ "description": "Level now persisted; always greater than previous_level."++ },++ "previous_level": {"type": "integer","minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ "description": "Level at the previous baseline (1 when the skill had none)."}}}},-- "trade.observation.created": {++ "games.blueprint.unlocked": {"version": "1.0.0",-- "owner": "trade",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",-- "title": "TradeObservationCreatedPayload",-- "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",++ "title": "GamesBlueprintUnlockedPayload",++ "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.","type": "object","required": [-- "instrument",-- "id",-- "metric",-- "value",-- "unit",-- "currency",-- "period",-- "observed_at",-- "retrieved_at",-- "recorded_at",-- "max_age_sec",-- "source"++ "player",++ "world",++ "recipe"],"additionalProperties": false,"properties": {-- "instrument": {-- "type": "object",-- "required": [-- "id",-- "symbol"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "symbol": {-- "type": "string",-- "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"-- }-- }-- },-- "id": {-- "type": "string",-- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "metric": {-- "type": "string",-- "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",-- "description": "e.g. price.close, volume, us-gaap:Revenues."-- },-- "value": {-- "type": "string",-- "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",-- "maxLength": 64,-- "description": "The decimal exactly as the source stated it (text, never rounded)."-- },-- "unit": {-- "type": "string",-- "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",-- "description": "e.g. USD, shares, USD/shares."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217 when the value is monetary; null otherwise."-- },-- "period": {-- "type": [-- "string",-- "null"-- ],-- "minLength": 1,-- "maxLength": 40,-- "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."-- },-- "observed_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the value was true, per the source."-- },-- "retrieved_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the source was fetched."-- },-- "recorded_at": {-- "type": "string",-- "format": "date-time",-- "description": "When Trade recorded the row."-- },-- "max_age_sec": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 60,-- "maximum": 315360000,-- "description": "The value's own maximum age, when the recorder gave one."-- },-- "source": {-- "type": "object",-- "required": [-- "key",-- "item_id",-- "url",-- "ref"-- ],-- "additionalProperties": false,-- "properties": {-- "key": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",-- "description": "OpenVibe.Sources source key."-- },-- "item_id": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The Sources item the value came from, when there is one."-- },-- "url": {-- "type": [-- "string",-- "null"++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"],-- "pattern": "^https?://",-- "description": "The source's page for the value (absolute http(s) URL, normalised)."++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }},-- "ref": {-- "type": "string",-- "minLength": 1,-- "maxLength": 300,-- "description": "The source's own reference for this datum; unique per source_key."++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }}-- }++ ]++ },++ "world": {++ "type": "string",++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ },++ "recipe": {++ "type": "string",++ "pattern": "^[a-z0-9_]+$",++ "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."}}}},-- "trade.source.stale": {++ "games.world.saved": {"version": "1.0.0",-- "owner": "trade",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",-- "title": "TradeSourceStalePayload",-- "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",++ "title": "GamesWorldSavedPayload",++ "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.","type": "object","required": [-- "source_key",-- "stale",-- "stale_since",-- "last_success_at",-- "stale_after_sec",-- "upstream_status",-- "evaluated_at"++ "world",++ "reason",++ "entities_written",++ "players_written",++ "since"],"additionalProperties": false,"properties": {-- "source_key": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",-- "description": "OpenVibe.Sources source key."-- },-- "stale": {-- "const": true-- },-- "stale_since": {++ "world": {"type": "string",-- "format": "date-time",-- "description": "Last success + window, or when Trade first saw the source if it never succeeded."++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."},-- "last_success_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "Last known successful fetch; null when none is known."++ "reason": {++ "enum": [++ "checkpoint",++ "shutdown"++ ]},-- "stale_after_sec": {++ "entities_written": {"type": "integer",-- "description": "The staleness window applied, in seconds."++ "minimum": 0,++ "description": "World entity rows written since `since`."},-- "upstream_status": {-- "type": [-- "string",-- "null"-- ],-- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."++ "players_written": {++ "type": "integer",++ "minimum": 0,++ "description": "Character rows written by flushes since `since`."},-- "evaluated_at": {++ "since": {"type": "string",-- "format": "date-time"++ "format": "date-time",++ "description": "Start of the period these totals cover."}}}},-- "trade.source.recovered": {++ "games.mod.installed": {"version": "1.0.0",-- "owner": "trade",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",-- "title": "TradeSourceRecoveredPayload",-- "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",++ "title": "GamesModInstalledPayload",++ "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "source_key",-- "stale",-- "stale_since",-- "last_success_at",-- "stale_after_sec",-- "upstream_status",-- "evaluated_at"++ "mod",++ "requested",++ "granted",++ "status"],"additionalProperties": false,"properties": {-- "source_key": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",-- "description": "OpenVibe.Sources source key."-- },-- "stale": {-- "const": false-- },-- "stale_since": {-- "type": "null",-- "description": "Always null: the source is fresh."-- },-- "last_success_at": {-- "type": "string",-- "format": "date-time",-- "description": "The successful fetch that made it fresh (or a later one)."++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"++ ],++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."++ }++ }},-- "stale_after_sec": {-- "type": "integer",-- "description": "The staleness window applied, in seconds."++ "requested": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "manifest.permissions.capabilities, sorted."},-- "upstream_status": {-- "type": [-- "string",-- "null"-- ],-- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."++ "granted": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "The approved subset of requested, sorted (may be empty)."},-- "evaluated_at": {-- "type": "string",-- "format": "date-time"++ "status": {++ "enum": [++ "enabled",++ "disabled"++ ]}}}},-- "trade.index_document.upserted": {++ "games.mod.enabled": {"version": "1.0.0",-- "owner": "trade",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",-- "title": "TradeIndexDocumentUpsertedPayload",-- "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"-- },-- {++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",++ "title": "GamesModEnabledPayload",++ "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",++ "type": "object",++ "required": [++ "mod"++ ],++ "additionalProperties": false,++ "properties": {++ "mod": {"type": "object","required": [-- "owner",-- "type",-- "deleted"++ "id",++ "name",++ "version",++ "target",++ "trust_tier"],++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "owner": {-- "const": "trade"++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."},-- "type": {-- "const": "instrument"++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80},-- "id": {++ "version": {"type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."},-- "deleted": {-- "const": false++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."}}}-- ]++ }}},-- "trade.index_document.deleted": {++ "games.mod.disabled": {"version": "1.0.0",-- "owner": "trade",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",-- "title": "TradeIndexDocumentDeletedPayload",-- "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",++ "title": "GamesModDisabledPayload",++ "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "type",-- "id",-- "revision"++ "mod"],"additionalProperties": false,"properties": {-- "type": {-- "const": "instrument"-- },-- "id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "revision": {-- "type": "integer",-- "minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"++ ],++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."++ }++ }}}}},-- "games.player.joined": {++ "games.mod.grants_changed": {"version": "1.0.0","owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",-- "title": "GamesPlayerJoinedPayload",-- "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",++ "title": "GamesModGrantsChangedPayload",++ "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "player",-- "world",-- "restored"++ "mod",++ "granted",++ "revoked"],"additionalProperties": false,"properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account",-- "subject"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "network"-- },-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }-- ]-- }-- }++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"++ ],++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."},-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "guest"-- }-- }++ "description": "Install metadata set at install; never consulted by a grant check."}-- ]++ }},-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ "granted": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Capabilities newly granted by this change."},-- "restored": {-- "type": "boolean",-- "description": "The character was loaded from the database (false: created by this join)."++ "revoked": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Capabilities whose grant this change revoked."}}}},-- "games.player.left": {++ "games.mod.revoked": {"version": "1.0.0","owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",-- "title": "GamesPlayerLeftPayload",-- "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",++ "title": "GamesModRevokedPayload",++ "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "player",-- "world",-- "session_seconds"++ "mod",++ "revoked_grants"],"additionalProperties": false,"properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account",-- "subject"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "network"-- },-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }-- ]-- }-- }++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"++ ],++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."},-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "guest"-- }-- }++ "description": "Install metadata set at install; never consulted by a grant check."}++ }++ },++ "revoked_grants": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Grants active before the revoke, sorted."++ },++ "reason": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ }++ }++ }++ },++ "media.object.deleted": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",++ "title": "MediaObjectDeletedPayload",++ "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",++ "type": "object",++ "required": [++ "object_id",++ "app_id",++ "kind",++ "legacy_ref",++ "deleted_at"++ ],++ "additionalProperties": false,++ "properties": {++ "object_id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "kind": {++ "enum": [++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"]},-- "world": {++ "legacy_ref": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300,++ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."++ },++ "deleted_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the change committed, ISO 8601 UTC."++ }++ }++ }++ },++ "media.object.visibility_changed": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",++ "title": "MediaObjectVisibilityChangedPayload",++ "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",++ "type": "object",++ "required": [++ "object_id",++ "app_id",++ "kind",++ "legacy_ref",++ "visibility",++ "previous_visibility",++ "changed_at"++ ],++ "additionalProperties": false,++ "properties": {++ "object_id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "app_id": {"type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."},-- "session_seconds": {-- "type": "integer",-- "minimum": 0++ "kind": {++ "enum": [++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"++ ]++ },++ "legacy_ref": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300,++ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."++ },++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ],++ "description": "The new value."++ },++ "previous_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ],++ "description": "The value before the change; never equal to visibility."++ },++ "changed_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the change committed, ISO 8601 UTC."}++ },++ "not": {++ "anyOf": [++ {++ "required": [++ "visibility",++ "previous_visibility"++ ],++ "properties": {++ "visibility": {++ "const": "public"++ },++ "previous_visibility": {++ "const": "public"++ }++ }++ },++ {++ "required": [++ "visibility",++ "previous_visibility"++ ],++ "properties": {++ "visibility": {++ "const": "unlisted"++ },++ "previous_visibility": {++ "const": "unlisted"++ }++ }++ },++ {++ "required": [++ "visibility",++ "previous_visibility"++ ],++ "properties": {++ "visibility": {++ "const": "private"++ },++ "previous_visibility": {++ "const": "private"++ }++ }++ }++ ]}}},-- "games.skill.leveled": {++ "community.paste.created": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",-- "title": "GamesSkillLeveledPayload",-- "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",++ "title": "CommunityPasteCreatedPayload",++ "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.","type": "object","required": [-- "player",-- "world",-- "skill",-- "level",-- "previous_level"++ "paste_id",++ "type",++ "origin",++ "visibility",++ "owner",++ "url"],"additionalProperties": false,"properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account",-- "subject"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "network"-- },-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }-- ]-- }-- }-- },-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "guest"-- }-- }++ "paste_id": {++ "type": "string",++ "pattern": "^[a-z0-9-]{3,64}$",++ "description": "The paste's slug."++ },++ "type": {++ "enum": [++ "paste",++ "screenshot"++ ]++ },++ "origin": {++ "enum": [++ "user",++ "ai",++ "import"++ ],++ "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."++ },++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private",++ "hidden",++ "deleted"++ ]++ },++ "owner": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"}]},-- "world": {++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."++ }++ }++ }++ },++ "community.paste.updated": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",++ "title": "CommunityPasteUpdatedPayload",++ "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "type": "object",++ "required": [++ "paste_id",++ "changed",++ "visibility",++ "revision",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "paste_id": {"type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ "pattern": "^[a-z0-9-]{3,64}$"},-- "skill": {-- "type": "string",-- "pattern": "^[a-z0-9_]+$",-- "description": "Content skill id (e.g. mining, woodcutting)."++ "changed": {++ "type": "array",++ "minItems": 1,++ "uniqueItems": true,++ "items": {++ "enum": [++ "title",++ "content",++ "language",++ "visibility",++ "is_nsfw",++ "pinned"++ ]++ }},-- "level": {-- "type": "integer",-- "minimum": 2,-- "description": "Level now persisted; always greater than previous_level."++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private",++ "hidden",++ "deleted"++ ]},-- "previous_level": {++ "revision": {"type": "integer",-- "minimum": 1,-- "description": "Level at the previous baseline (1 when the skill had none)."++ "minimum": 1++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."}}}},-- "games.blueprint.unlocked": {++ "community.paste.deleted": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",-- "title": "GamesBlueprintUnlockedPayload",-- "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",++ "title": "CommunityPasteDeletedPayload",++ "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.","type": "object","required": [-- "player",-- "world",-- "recipe"++ "paste_id"],"additionalProperties": false,"properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "paste_id": {++ "type": "string",++ "pattern": "^[a-z0-9-]{3,64}$"++ }++ }++ }++ },++ "community.thread.created": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",++ "title": "CommunityThreadCreatedPayload",++ "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "type": "object",++ "required": [++ "thread_id",++ "space",++ "author",++ "visibility",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "thread_id": {++ "type": "integer",++ "minimum": 1++ },++ "space": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ },++ "author": {"oneOf": [{-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account",-- "subject"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "network"-- },-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }-- ]-- }-- }++ "$ref": "../../identity/subject-ref.v1.json"},{-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "guest"-- }-- }++ "type": "null"}]},-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ "visibility": {++ "enum": [++ "public",++ "members",++ "hidden"++ ]},-- "recipe": {-- "type": "string",-- "pattern": "^[a-z0-9_]+$",-- "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."}}}},-- "games.world.saved": {++ "community.post.created": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",-- "title": "GamesWorldSavedPayload",-- "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",++ "title": "CommunityPostCreatedPayload",++ "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.","type": "object","required": [-- "world",-- "reason",-- "entities_written",-- "players_written",-- "since"++ "post_id",++ "thread_id",++ "space",++ "author",++ "visibility",++ "url"],"additionalProperties": false,"properties": {-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."-- },-- "reason": {-- "enum": [-- "checkpoint",-- "shutdown"-- ]-- },-- "entities_written": {++ "post_id": {"type": "integer",-- "minimum": 0,-- "description": "World entity rows written since `since`."++ "minimum": 1},-- "players_written": {++ "thread_id": {"type": "integer",-- "minimum": 0,-- "description": "Character rows written by flushes since `since`."++ "minimum": 1},-- "since": {++ "space": {"type": "string",-- "format": "date-time",-- "description": "Start of the period these totals cover."++ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ },++ "author": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "visibility": {++ "enum": [++ "public",++ "members",++ "hidden"++ ]++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."}}}},-- "games.mod.installed": {++ "community.comment.created": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",-- "title": "GamesModInstalledPayload",-- "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",++ "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",++ "title": "CommunityCommentCreatedPayload",++ "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.","type": "object","required": [-- "mod",-- "requested",-- "granted",-- "status"++ "comment_id",++ "thread_access_id",++ "ref",++ "author",++ "visibility",++ "url"],"additionalProperties": false,"properties": {-- "mod": {++ "comment_id": {++ "type": "integer",++ "minimum": 1++ },++ "thread_access_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64++ },++ "ref": {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "service",++ "type",++ "id"],"additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },-- "name": {++ "service": {"type": "string",-- "minLength": 1,-- "maxLength": 80++ "pattern": "^[a-z][a-z0-9-]{1,39}$"},-- "version": {++ "type": {"type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."++ "pattern": "^[a-z][a-z0-9_]{1,39}$"},-- "target": {++ "id": {"type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."-- },-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."++ "minLength": 1,++ "maxLength": 200}}},-- "requested": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "manifest.permissions.capabilities, sorted."-- },-- "granted": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "The approved subset of requested, sorted (may be empty)."++ "author": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]},-- "status": {++ "visibility": {"enum": [-- "enabled",-- "disabled"++ "public",++ "hidden",++ "locked"]++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."}}}},-- "games.mod.enabled": {++ "chat.moderation.action": {"version": "1.0.0",-- "owner": "games",++ "owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",-- "title": "GamesModEnabledPayload",-- "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",++ "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",++ "title": "ChatModerationActionPayload",++ "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).","type": "object","required": [-- "mod"++ "action_id",++ "action_type",++ "scope_type"],"additionalProperties": false,"properties": {-- "mod": {-- "type": "object",-- "required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"-- ],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80-- },-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."-- },-- "target": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."-- },-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."-- }-- }++ "action_id": {++ "type": "integer",++ "minimum": 1++ },++ "action_type": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64,++ "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."++ },++ "scope_type": {++ "type": "string",++ "minLength": 1,++ "maxLength": 32,++ "description": "site, channel, stream, room, …"++ },++ "scope_id": {++ "type": [++ "string",++ "integer",++ "null"++ ]++ },++ "actor_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."++ },++ "actor_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "target_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "details": {++ "type": "object"}}}},-- "games.mod.disabled": {++ "chat.send-request": {"version": "1.0.0",-- "owner": "games",++ "owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",-- "title": "GamesModDisabledPayload",-- "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",-- "type": "object",-- "required": [-- "mod"-- ],-- "additionalProperties": false,-- "properties": {-- "mod": {++ "$id": "https://openvibe.network/contracts/chat/send-request.v1.json",++ "title": "ChatSendRequest",++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",++ "anyOf": [++ {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "type",++ "message"],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."++ "type": {++ "const": "chat"},-- "name": {++ "message": {"type": "string","minLength": 1,-- "maxLength": 80++ "maxLength": 6000,++ "description": "The text (trimmed by Chat)."},-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."++ "reply_to_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."},-- "target": {++ "auto_delete_minutes": {++ "type": [++ "integer",++ "string"++ ],++ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."++ },++ "voiceChannelId": {"type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ "description": "The voice channel the sender is in, echoed on the broadcast."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ops"++ ],++ "properties": {++ "boot": {++ "type": "string",++ "maxLength": 64,++ "description": "Live's boot id; placeholder message ids are mapped per boot."},-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."++ "ops": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "op"++ ],++ "properties": {++ "seq": {++ "type": "integer",++ "description": "Echoed on the result."++ },++ "op": {++ "type": "string",++ "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."++ },++ "args": {++ "type": "array"++ },++ "ref": {++ "type": "number",++ "description": "Live's placeholder id for the row this op inserts."++ },++ "key": {++ "type": "string",++ "maxLength": 80,++ "description": "Idempotency key: applied once."++ }++ }++ }}}}-- }++ ]}},-- "games.mod.grants_changed": {++ "chat.send-result": {"version": "1.0.0",-- "owner": "games",++ "owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",-- "title": "GamesModGrantsChangedPayload",-- "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",-- "type": "object",-- "required": [-- "mod",-- "granted",-- "revoked"-- ],-- "additionalProperties": false,-- "properties": {-- "mod": {++ "$id": "https://openvibe.network/contracts/chat/send-result.v1.json",++ "title": "ChatSendResult",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",++ "anyOf": [++ {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "type",++ "username",++ "role",++ "message",++ "stream_id",++ "channel_user_id",++ "is_global",++ "timestamp"],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {++ "type": {++ "const": "chat"++ },"id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."++ "type": "integer",++ "description": "The stored message id."},-- "name": {++ "username": {"type": "string",-- "minLength": 1,-- "maxLength": 80++ "description": "Display name, or the anonymous id."},-- "version": {++ "core_username": {++ "type": [++ "string",++ "null"++ ],++ "description": "The immutable login handle; null for an anonymous sender."++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "role": {"type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."++ "description": "The sender's role, anon for an anonymous sender."},-- "target": {++ "message": {++ "type": "string"++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "is_global": {++ "type": "boolean",++ "description": "true for homepage global chat (no stream, no channel)."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": "string"++ },++ "filtered": {++ "type": "boolean"++ },++ "timestamp": {"type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ "format": "date-time"},-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"++ "auto_delete_at": {++ "type": [++ "string",++ "null"],-- "description": "Install metadata set at install; never consulted by a grant check."++ "format": "date-time"++ },++ "voiceChannelId": {++ "type": "string"++ },++ "reply_to": {++ "type": "object",++ "required": [++ "id",++ "username",++ "message"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "username": {++ "type": "string"++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "message": {++ "type": "string",++ "description": "The parent's text, cut to 100 characters."++ }++ }++ },++ "nameFX": {++ "description": "Cosmetic name effect."++ },++ "particleFX": {++ "description": "Cosmetic particle effect."++ },++ "hatFX": {++ "description": "Cosmetic hat."++ },++ "voiceFX": {++ "description": "Cosmetic TTS voice effect."++ },++ "tag": {++ "description": "The sender's equipped tag."++ },++ "vip_badge": {++ "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "message"++ ],++ "properties": {++ "type": {++ "enum": [++ "system",++ "error"++ ]++ },++ "message": {++ "type": "string"}}},-- "granted": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "Capabilities newly granted by this change."-- },-- "revoked": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "Capabilities whose grant this change revoked."++ {++ "type": "object",++ "required": [++ "ok",++ "results"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "results": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "ok"++ ],++ "properties": {++ "seq": {++ "type": "integer"++ },++ "ok": {++ "type": "boolean"++ },++ "result": {},++ "code": {++ "type": "string"++ },++ "error": {++ "type": "string"++ }++ }++ }++ }++ }}-- }++ ]}},-- "games.mod.revoked": {++ "live.moderation.action": {"version": "1.0.0",-- "owner": "games",++ "owner": "live","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",-- "title": "GamesModRevokedPayload",-- "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",++ "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",++ "title": "LiveModerationActionPayload",++ "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).","type": "object","required": [-- "mod",-- "revoked_grants"++ "action_id",++ "action_type",++ "scope_type"],"additionalProperties": false,"properties": {-- "mod": {-- "type": "object",-- "required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"-- ],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80-- },-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."-- },-- "target": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."-- },-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."-- }-- }++ "action_id": {++ "type": "integer",++ "minimum": 1},-- "revoked_grants": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "Grants active before the revoke, sorted."++ "action_type": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64,++ "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."},-- "reason": {++ "scope_type": {"type": "string","minLength": 1,-- "maxLength": 500++ "maxLength": 32,++ "description": "site, channel, stream, room, …"++ },++ "scope_id": {++ "type": [++ "string",++ "integer",++ "null"++ ]++ },++ "actor_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."++ },++ "actor_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "target_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "details": {++ "type": "object"}}}},-- "media.object.deleted": {++ "community.moderation.action": {"version": "1.0.0",-- "owner": "media",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",-- "title": "MediaObjectDeletedPayload",-- "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",++ "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",++ "title": "CommunityModerationActionPayload",++ "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.","type": "object","required": [-- "object_id",-- "app_id",-- "kind",-- "legacy_ref",-- "deleted_at"++ "action",++ "target",++ "actor_subject"],"additionalProperties": false,"properties": {-- "object_id": {-- "type": "string",-- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "app_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."-- },-- "kind": {++ "action": {"enum": [-- "vod",-- "clip",-- "file",-- "thumbnail",-- "screenshot",-- "avatar",-- "asset"++ "paste.edited",++ "paste.deleted",++ "paste.visibility_changed",++ "paste.censored",++ "pastes.bulk",++ "forks.deleted",++ "comment.deleted",++ "thread.deleted",++ "thread.locked",++ "thread.unlocked",++ "post.deleted"]},-- "legacy_ref": {++ "target": {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "enum": [++ "paste",++ "paste_comment",++ "comment",++ "thread",++ "post",++ "pastes"++ ]++ },++ "id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "owner_subject": {++ "type": [++ "string",++ "null"++ ],++ "description": "Whose content it was, when known."++ }++ }++ },++ "actor_subject": {"type": ["string","null"],-- "maxLength": 300,-- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."++ "description": "The staff member (null only for a service acting without a person)."},-- "deleted_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the change committed, ISO 8601 UTC."++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500++ },++ "details": {++ "type": "object",++ "description": "Per action: the fields changed, a bulk action's name and count, … never the content."}}}},-- "media.object.visibility_changed": {++ "community.paste-create-request": {"version": "1.0.0",-- "owner": "media",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",-- "title": "MediaObjectVisibilityChangedPayload",-- "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",++ "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",++ "title": "CommunityPasteCreateRequest",++ "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.","type": "object",-- "required": [-- "object_id",-- "app_id",-- "kind",-- "legacy_ref",-- "visibility",-- "previous_visibility",-- "changed_at"++ "anyOf": [++ {++ "required": [++ "content"++ ]++ },++ {++ "required": [++ "screenshot"++ ]++ }],-- "additionalProperties": false,"properties": {-- "object_id": {-- "type": "string",-- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "app_id": {++ "content": {"type": "string","minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."++ "description": "The text (trimmed)."},-- "kind": {-- "enum": [-- "vod",-- "clip",-- "file",-- "thumbnail",-- "screenshot",-- "avatar",-- "asset"-- ]++ "screenshot": {++ "type": "string",++ "contentEncoding": "binary",++ "description": "Multipart only: the image part."},-- "legacy_ref": {++ "title": {"type": ["string","null"],-- "maxLength": 300,-- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."++ "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank."++ },++ "language": {++ "type": "string",++ "description": "A language hint; auto or absent sniffs it from the content."},"visibility": {-- "enum": [-- "public",-- "unlisted",-- "private"++ "type": "string",++ "description": "public (default), unlisted or private."++ },++ "burn_after_read": {++ "type": [++ "boolean",++ "integer",++ "string"],-- "description": "The new value."++ "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else."},-- "previous_visibility": {-- "enum": [-- "public",-- "unlisted",-- "private"++ "is_nsfw": {++ "type": [++ "boolean",++ "integer",++ "string"++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "string"],-- "description": "The value before the change; never equal to visibility."++ "description": "The Live stream it was made during (digits)."},-- "changed_at": {++ "description": {"type": "string",-- "format": "date-time",-- "description": "When the change committed, ISO 8601 UTC."++ "description": "Multipart screenshot: the text shown under the image."++ },++ "page_url": {++ "type": "string",++ "description": "Multipart screenshot: the page it shows."++ },++ "user_agent": {++ "type": "string",++ "description": "Multipart screenshot: the capturing browser."++ },++ "slug": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_-]{3,80}$",++ "description": "Service callers, public pastes only: the slug to use (409 if taken)."++ },++ "metadata": {++ "type": [++ "object",++ "string"++ ],++ "description": "Service callers only: a JSON object (at most 16 KB)."++ },++ "ai_summary": {++ "type": "string",++ "description": "Service callers only (cut to 2000 characters)."++ },++ "ai_tags": {++ "description": "Service callers only, with ai_summary: JSON text or a value to serialise."}-- },-- "not": {-- "anyOf": [-- {-- "required": [-- "visibility",-- "previous_visibility"-- ],-- "properties": {-- "visibility": {-- "const": "public"-- },-- "previous_visibility": {-- "const": "public"-- }-- }-- },-- {-- "required": [-- "visibility",-- "previous_visibility"-- ],-- "properties": {-- "visibility": {-- "const": "unlisted"-- },-- "previous_visibility": {-- "const": "unlisted"-- }-- }-- },-- {-- "required": [-- "visibility",-- "previous_visibility"-- ],-- "properties": {-- "visibility": {-- "const": "private"-- },-- "previous_visibility": {-- "const": "private"-- }-- }-- }-- ]}}},-- "community.paste.created": {++ "community.paste-moderate-request": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",-- "title": "CommunityPasteCreatedPayload",-- "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "paste_id",-- "type",-- "origin",-- "visibility",-- "owner",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "paste_id": {-- "type": "string",-- "pattern": "^[a-z0-9-]{3,64}$",-- "description": "The paste's slug."-- },-- "type": {-- "enum": [-- "paste",-- "screenshot"-- ]-- },-- "origin": {-- "enum": [-- "user",-- "ai",-- "import"++ "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",++ "title": "CommunityPasteModerateRequest",++ "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "slugs",++ "action"],-- "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."-- },-- "visibility": {-- "enum": [-- "public",-- "unlisted",-- "private",-- "hidden",-- "deleted"-- ]-- },-- "owner": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"++ "properties": {++ "slugs": {++ "type": "array",++ "minItems": 1,++ "items": {++ "type": "string"++ }},-- {-- "type": "null"++ "action": {++ "enum": [++ "delete",++ "public",++ "unlisted",++ "private"++ ]}-- ]++ }},-- "url": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "screenshot"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."++ "properties": {++ "screenshot": {++ "type": "string",++ "contentEncoding": "binary",++ "description": "The multipart part named screenshot: the censored image."++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "community.paste.updated": {++ "community.paste-moderate-result": {"version": "1.0.0","owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",-- "title": "CommunityPasteUpdatedPayload",-- "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "paste_id",-- "changed",-- "visibility",-- "revision",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "paste_id": {-- "type": "string",-- "pattern": "^[a-z0-9-]{3,64}$"-- },-- "changed": {-- "type": "array",-- "minItems": 1,-- "uniqueItems": true,-- "items": {-- "enum": [-- "title",-- "content",-- "language",-- "visibility",-- "is_nsfw",-- "pinned"-- ]++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",++ "title": "CommunityPasteModerateResult",++ "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "done",++ "skipped"++ ],++ "properties": {++ "done": {++ "type": "integer",++ "minimum": 0++ },++ "skipped": {++ "type": "integer",++ "minimum": 0++ }}},-- "visibility": {-- "enum": [-- "public",-- "unlisted",-- "private",-- "hidden",-- "deleted"-- ]-- },-- "revision": {-- "type": "integer",-- "minimum": 1++ {++ "type": "object",++ "required": [++ "paste"++ ],++ "properties": {++ "paste": {++ "$ref": "paste.v1.json"++ }++ }},-- "url": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "message"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."++ "properties": {++ "message": {++ "type": "string"++ }++ }}-- }++ ]}},-- "community.paste.deleted": {++ "community.paste-write-request": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",-- "title": "CommunityPasteDeletedPayload",-- "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "paste_id"-- ],-- "additionalProperties": false,-- "properties": {-- "paste_id": {-- "type": "string",-- "pattern": "^[a-z0-9-]{3,64}$"++ "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",++ "title": "CommunityPasteWriteRequest",++ "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",++ "anyOf": [++ {++ "type": "object",++ "minProperties": 1,++ "properties": {++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "content": {++ "type": "string"++ },++ "language": {++ "type": "string"++ },++ "visibility": {++ "type": "string",++ "description": "public, unlisted or private (anything else is public)."++ },++ "is_nsfw": {++ "type": [++ "boolean",++ "integer",++ "string"++ ]++ },++ "pinned": {++ "type": [++ "boolean",++ "integer",++ "string"++ ],++ "description": "Staff only; ignored otherwise."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2000++ },++ "parent_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "The top-level comment this replies to."++ },++ "anon_name": {++ "type": "string",++ "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "community.thread.created": {++ "community.paste-write-result": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",-- "title": "CommunityThreadCreatedPayload",-- "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "thread_id",-- "space",-- "author",-- "visibility",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "thread_id": {-- "type": "integer",-- "minimum": 1++ "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",++ "title": "CommunityPasteWriteResult",++ "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "paste"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "slug": {++ "type": "string"++ },++ "url": {++ "type": "string"++ },++ "paste": {++ "$ref": "paste.v1.json"++ }++ }},-- "space": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ {++ "type": "object",++ "required": [++ "success"++ ],++ "properties": {++ "success": {++ "const": true++ }++ }},-- "author": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"++ {++ "type": "object",++ "required": [++ "liked",++ "likes"++ ],++ "properties": {++ "liked": {++ "type": "boolean"},-- {-- "type": "null"++ "likes": {++ "type": "integer",++ "minimum": 0}-- ]++ }},-- "visibility": {-- "enum": [-- "public",-- "members",-- "hidden"-- ]++ {++ "type": "object",++ "required": [++ "copies"++ ],++ "properties": {++ "copies": {++ "type": "integer",++ "minimum": 0++ }++ }++ },++ {++ "type": "object",++ "required": [++ "comment"++ ],++ "properties": {++ "comment": {++ "type": "object",++ "required": [++ "id",++ "paste_id",++ "message",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "paste_id": {++ "type": "integer"++ },++ "user_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "The author's subject; null for an anonymous comment."++ },++ "author_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "parent_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "anon_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "is_deleted": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }},-- "url": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "message"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."++ "properties": {++ "message": {++ "type": "string"++ }++ }}-- }++ ]}},-- "community.post.created": {++ "community.paste": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",-- "title": "CommunityPostCreatedPayload",-- "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "$id": "https://openvibe.network/contracts/community/paste.v1.json",++ "title": "CommunityPaste",++ "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).","type": "object","required": [-- "post_id",-- "thread_id",-- "space",-- "author",++ "id",++ "app_id",++ "slug",++ "user_id",++ "owner_subject",++ "origin",++ "type",++ "title","visibility",-- "url"++ "url",++ "raw_url",++ "created_at",++ "is_owner"],-- "additionalProperties": false,"properties": {-- "post_id": {-- "type": "integer",-- "minimum": 1++ "id": {++ "type": "integer"},-- "thread_id": {-- "type": "integer",-- "minimum": 1++ "app_id": {++ "const": "community"},-- "space": {++ "slug": {++ "type": "string"++ },++ "user_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "The owner's subject (usr_…), or null for an anonymous paste."++ },++ "owner_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "origin": {"type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ "description": "user, or ai for a paste an AI service wrote."},-- "author": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }++ "type": {++ "enum": [++ "paste",++ "screenshot"++ ]++ },++ "title": {++ "type": "string"++ },++ "content": {++ "type": [++ "string",++ "null"++ ]++ },++ "language": {++ "type": [++ "string",++ "null"]},"visibility": {"enum": ["public",-- "members",-- "hidden"++ "unlisted",++ "private"]},-- "url": {++ "stream_id": {"type": [-- "string",++ "integer","null"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."-- }-- }-- }-- },-- "community.comment.created": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",-- "title": "CommunityCommentCreatedPayload",-- "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "comment_id",-- "thread_access_id",-- "ref",-- "author",-- "visibility",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "comment_id": {-- "type": "integer",-- "minimum": 1-- },-- "thread_access_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64++ "description": "The Live stream it was made during, when its stream_ref is one."},-- "ref": {-- "type": "object",-- "required": [-- "service",-- "type",-- "id"++ "stream_ref": {++ "type": [++ "object",++ "null"],-- "additionalProperties": false,-- "properties": {-- "service": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$"-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,39}$"-- },-- "id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200-- }-- }++ "description": "An EntityRef of where it was made (a Live stream, or a service's own source)."},-- "author": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }++ "screenshot_url": {++ "type": [++ "string",++ "null"]},-- "visibility": {-- "enum": [-- "public",-- "hidden",-- "locked"++ "metadata": {++ "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details."++ },++ "burn_after_read": {++ "type": "boolean"++ },++ "forked_from": {++ "type": [++ "integer",++ "null"]},-- "url": {++ "pinned": {++ "type": "boolean"++ },++ "views": {++ "type": "integer",++ "minimum": 0++ },++ "unique_views": {++ "type": "integer",++ "minimum": 0++ },++ "copies": {++ "type": "integer",++ "minimum": 0++ },++ "likes": {++ "type": "integer",++ "minimum": 0++ },++ "is_nsfw": {++ "type": "boolean"++ },++ "ai_summary": {++ "type": [++ "string",++ "null"++ ]++ },++ "ai_tags": {"type": ["string","null"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."-- }-- }-- }-- },-- "chat.moderation.action": {-- "version": "1.0.0",-- "owner": "chat",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",-- "title": "ChatModerationActionPayload",-- "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",-- "type": "object",-- "required": [-- "action_id",-- "action_type",-- "scope_type"-- ],-- "additionalProperties": false,-- "properties": {-- "action_id": {-- "type": "integer",-- "minimum": 1++ "description": "JSON text as the AI pass wrote it."},-- "action_type": {++ "ai_analyzed_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "revision": {++ "type": "integer"++ },++ "url": {"type": "string",-- "minLength": 1,-- "maxLength": 64,-- "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."++ "description": "/p/<slug>"},-- "scope_type": {++ "raw_url": {"type": "string",-- "minLength": 1,-- "maxLength": 32,-- "description": "site, channel, stream, room, …"++ "description": "/p/<slug>/raw"++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_owner": {++ "type": "boolean",++ "description": "Whether the caller owns it."},-- "scope_id": {++ "username": {"type": ["string",-- "integer","null"]},-- "actor_user_id": {++ "display_name": {"type": [-- "integer",++ "string","null"],-- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."++ "description": "OpenVibe AI for an AI paste."},-- "actor_subject": {++ "avatar_url": {"type": ["string","null"]},-- "target_user_id": {-- "type": [-- "integer",-- "null"-- ]-- },-- "target_subject": {++ "profile_color": {"type": ["string","null"]-- },-- "details": {-- "type": "object"}}}},-- "live.moderation.action": {++ "community.moderation-request": {"version": "1.0.0",-- "owner": "live",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",-- "title": "LiveModerationActionPayload",-- "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",-- "type": "object",-- "required": [-- "action_id",-- "action_type",-- "scope_type"-- ],-- "additionalProperties": false,-- "properties": {-- "action_id": {-- "type": "integer",-- "minimum": 1++ "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",++ "title": "CommunityModerationRequest",++ "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "visibility"++ ],++ "properties": {++ "visibility": {++ "enum": [++ "public",++ "hidden",++ "locked"++ ]++ }++ }},-- "action_type": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64,-- "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."++ {++ "type": "object",++ "anyOf": [++ {++ "required": [++ "pinned"++ ]++ },++ {++ "required": [++ "locked"++ ]++ }++ ],++ "properties": {++ "pinned": {++ "description": "Truthy pins the thread."++ },++ "locked": {++ "description": "Truthy locks it (announced as thread.locked / thread.unlocked)."++ }++ }},-- "scope_type": {-- "type": "string",-- "minLength": 1,-- "maxLength": 32,-- "description": "site, channel, stream, room, …"++ {++ "type": "object",++ "anyOf": [++ {++ "required": [++ "body"++ ]++ },++ {++ "required": [++ "body_markdown"++ ]++ }++ ],++ "properties": {++ "body": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40000++ },++ "body_markdown": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40000++ }++ }},-- "scope_id": {-- "type": [-- "string",-- "integer",-- "null"-- ]++ {++ "type": "object",++ "required": [++ "space",++ "webhook_url_ref"++ ],++ "properties": {++ "space": {++ "type": "string",++ "description": "The space's slug."++ },++ "webhook_url_ref": {++ "type": "string",++ "pattern": "^[A-Z0-9_]+$"++ },++ "enabled": {++ "type": "boolean",++ "description": "Default true."++ }++ }},-- "actor_user_id": {-- "type": [-- "integer",-- "null"++ {++ "type": "object",++ "required": [++ "enabled"],-- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."++ "properties": {++ "enabled": {++ "type": "boolean"++ }++ }},-- "actor_subject": {-- "type": [-- "string",-- "null"-- ]++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "community.moderation-result": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",++ "title": "CommunityModerationResult",++ "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "thread"++ ],++ "properties": {++ "thread": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": [++ "integer",++ "string"++ ]++ },++ "access_id": {++ "type": "string",++ "description": "A comment thread's browser id (cth_…)."++ },++ "ref": {++ "$ref": "../common/entity-ref.v1.json"++ },++ "visibility": {++ "type": "string"++ },++ "comment_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "space": {++ "type": "string"++ },++ "slug": {++ "type": "string"++ },++ "title": {++ "type": "string"++ },++ "pinned": {++ "type": "boolean"++ },++ "locked": {++ "type": "boolean"++ }++ }++ }++ }},-- "target_user_id": {-- "type": [-- "integer",-- "null"-- ]++ {++ "type": "object",++ "required": [++ "post"++ ],++ "properties": {++ "post": {++ "type": "object",++ "required": [++ "id",++ "thread_id",++ "revision",++ "deleted"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "thread_id": {++ "type": "integer"++ },++ "is_opening": {++ "type": "boolean"++ },++ "origin": {++ "type": "string"++ },++ "author": {++ "type": [++ "object",++ "null"++ ]++ },++ "body_markdown": {++ "type": [++ "string",++ "null"++ ]++ },++ "body_html": {++ "type": [++ "string",++ "null"++ ]++ },++ "revision": {++ "type": "integer"++ },++ "deleted": {++ "type": "boolean"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "can_edit": {++ "type": "boolean"++ },++ "attachments": {++ "type": "array"++ },++ "pastes": {++ "type": "array"++ }++ }++ }++ }},-- "target_subject": {-- "type": [-- "string",-- "null"-- ]++ {++ "type": "object",++ "required": [++ "ok"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "id": {++ "type": "integer"++ },++ "deleted": {++ "const": "thread"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "enabled"++ ],++ "anyOf": [++ {++ "required": [++ "deliveries"++ ]++ },++ {++ "required": [++ "mappings"++ ]++ }++ ],++ "properties": {++ "enabled": {++ "type": "boolean"++ },++ "deliveries": {++ "type": "array",++ "items": {++ "type": "object"++ }++ },++ "mappings": {++ "type": "array",++ "items": {++ "type": "object"++ }++ }++ }},-- "details": {-- "type": "object"++ {++ "type": "object",++ "required": [++ "mapping"++ ],++ "properties": {++ "mapping": {++ "type": "object",++ "required": [++ "id",++ "space",++ "webhook_url_ref",++ "webhook_configured",++ "enabled"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "space": {++ "type": "string"++ },++ "direction": {++ "type": "string"++ },++ "webhook_url_ref": {++ "type": "string"++ },++ "webhook_configured": {++ "type": "boolean"++ },++ "enabled": {++ "type": "boolean"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }}-- }++ ]}},-- "community.moderation.action": {++ "community.post-write-request": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",-- "title": "CommunityModerationActionPayload",-- "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",-- "type": "object",-- "required": [-- "action",-- "target",-- "actor_subject"-- ],-- "additionalProperties": false,-- "properties": {-- "action": {-- "enum": [-- "paste.edited",-- "paste.deleted",-- "paste.visibility_changed",-- "paste.censored",-- "pastes.bulk",-- "forks.deleted",-- "comment.deleted",-- "thread.deleted",-- "thread.locked",-- "thread.unlocked",-- "post.deleted"-- ]-- },-- "target": {++ "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",++ "title": "CommunityPostWriteRequest",++ "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",++ "anyOf": [++ {"type": "object",-- "required": [-- "type",-- "id"++ "anyOf": [++ {++ "required": [++ "body"++ ]++ },++ {++ "required": [++ "body_markdown"++ ]++ }],-- "additionalProperties": false,"properties": {-- "type": {-- "enum": [-- "paste",-- "paste_comment",-- "comment",-- "thread",-- "post",-- "pastes"-- ]++ "title": {++ "type": "string",++ "description": "A new thread's title."},-- "id": {++ "body": {"type": "string","minLength": 1,-- "maxLength": 200++ "maxLength": 40000,++ "description": "Markdown."},-- "owner_subject": {++ "body_markdown": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40000,++ "description": "Read when body is absent."++ },++ "members_only": {++ "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",++ "anyOf": [++ {++ "type": [++ "boolean",++ "null"++ ]++ },++ {++ "type": "string",++ "pattern": "^usr_"++ },++ {++ "type": "object",++ "required": [++ "owner"++ ],++ "properties": {++ "owner": {++ "type": "string",++ "pattern": "^usr_"++ }++ }++ }++ ]++ },++ "category": {++ "type": "string",++ "description": "A new thread only: the slug of one of the space's categories."++ },++ "attachments": {++ "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",++ "anyOf": [++ {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ {++ "type": "array",++ "maxItems": 4,++ "items": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ ]++ },++ "pastes": {++ "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.","type": [-- "string",-- "null"++ "array",++ "string"],-- "description": "Whose content it was, when known."++ "items": {++ "type": "string"++ }}}},-- "actor_subject": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "value"],-- "description": "The staff member (null only for a service acting without a person)."++ "properties": {++ "value": {++ "enum": [++ 1,++ -1,++ 0,++ "1",++ "-1",++ "0"++ ],++ "description": "A thread vote; 0 takes the vote back."++ }++ }},-- "reason": {-- "type": [-- "string",-- "null"++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "community.pulse-write-result": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",++ "title": "CommunityPulseWriteResult",++ "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "item",++ "created"],-- "maxLength": 500++ "properties": {++ "created": {++ "type": "boolean"++ },++ "item": {++ "type": "object",++ "required": [++ "id",++ "source",++ "title",++ "url",++ "origin",++ "label",++ "actor",++ "occurred_at"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "source": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": "string"++ }++ }++ },++ "title": {++ "type": "string",++ "maxLength": 300++ },++ "url": {++ "type": "string"++ },++ "origin": {++ "enum": [++ "user",++ "ai",++ "system"++ ]++ },++ "label": {++ "enum": [++ "AI",++ "System",++ null++ ]++ },++ "actor": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "occurred_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }},-- "details": {++ {"type": "object",-- "description": "Per action: the fields changed, a bulk action's name and count, … never the content."++ "required": [++ "removed"++ ],++ "properties": {++ "removed": {++ "type": "integer",++ "minimum": 0,++ "maximum": 1++ }++ }}-- }++ ]}}}@@@ -8795,6 +8795,573 @@ export interface BillingReceiptExternalPayload {};}++/** [email protected] (owner: billing) */++/**++ * billing.admin-request@1: bodies of billing.ledger.admin on OpenVibe.Billing (operators). POST /api/v1/admin/freeze: { on, reason? } (unfreezing processes the provider deliveries stored meanwhile; allowed while frozen). POST /api/v1/admin/adjustments: { from, to, amount, reason, relates_to? } moves value between two accounts of one currency. POST /api/v1/purchases/settle: { provider, provider_ref, subject?, amount_cents, bits, intent_id?, test? } books a provider receipt. POST /api/v1/admin/provider-events/:id/reprocess, POST /api/v1/admin/sweep and the GETs (freeze, reconcile, provider-events, import-holds) take no body. Adjustments and settlements need an Idempotency-Key and are refused while frozen.++ */++export type BillingAdminRequest =++ | {++ on: boolean;++ reason?: string | null;++ }++ | {++ from: {++ kind:++ | "user_credit"++ | "creator_payable"++ | "provider_clearing"++ | "platform_revenue"++ | "payouts_pending"++ | "refunds"++ | "import_adjustment"++ | "chargeback_loss"++ | "fx_conversion";++ owner?: string | null;++ currency: "vibes-bits" | "usd-cents";++ };++ to: {++ kind:++ | "user_credit"++ | "creator_payable"++ | "provider_clearing"++ | "platform_revenue"++ | "payouts_pending"++ | "refunds"++ | "import_adjustment"++ | "chargeback_loss"++ | "fx_conversion";++ owner?: string | null;++ currency: "vibes-bits" | "usd-cents";++ };++ /**++ * In the accounts' currency (at most 1,000,000,000).++ */++ amount: number | string;++ reason: string;++ relates_to?: string | null;++ }++ | {++ provider: string;++ /**++ * The provider's payment id.++ */++ provider_ref: string;++ /**++ * Who the Vibes are for.++ */++ subject?: SubjectRef | string;++ /**++ * Cents paid.++ */++ amount_cents?: number | string;++ /**++ * Vibes to credit.++ */++ bits?: number | string;++ intent_id?: string;++ test?: boolean;++ }++ | NoBody;++++/** [email protected] (owner: billing) */++/**++ * billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.++ */++export type BillingAdminResult =++ | {++ frozen: boolean;++ reason: string | null;++ frozen_at: string | null;++ frozen_by: unknown;++ processed_after_unfreeze?: unknown;++ }++ | {++ id: string;++ ok: boolean;++ trigger?: string | null;++ started_at?: string;++ finished_at?: string;++ checks: {++ id: string;++ ok: boolean;++ }[];++ warnings?: unknown[];++ totals?: unknown;++ }++ | {++ transaction: BillingTransaction;++ duplicate_receipt?: boolean;++ }++ | {++ events: {++ id: number;++ provider: string;++ provider_event_id?: string | null;++ type?: string | null;++ processed_at?: string | null;++ result?: unknown;++ attempts?: number;++ last_error?: string | null;++ }[];++ }++ | {++ event: {};++ }++ | {++ renewed: string[];++ expired: string[];++ canceled: string[];++ skipped: number;++ }++ | {++ holds: {}[];++ };++++/** [email protected] (owner: billing) */++/**++ * billing.balance-read-result@1: answers of billing.balance.read on OpenVibe.Billing. GET /api/v1/balances/:subject → billing.balance@1; GET /api/v1/transactions?subject=&limit=&cursor= → { transactions, next_cursor } (newest first, at most 200 a page; subject is required); GET /api/v1/transactions/:id → { transaction, reversed_by } (ids of the transactions that reverse it).++ */++export type BillingBalanceReadResult =++ | BillingBalance++ | {++ /**++ * @maxItems 200++ */++ transactions: BillingTransaction[];++ next_cursor: string | null;++ }++ | {++ transaction: BillingTransaction;++ reversed_by: string[];++ };++++/** [email protected] (owner: billing) */++/**++ * billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.++ */++export interface BillingBalance {++ subject: SubjectRef;++ currency: "vibes-bits";++ credit: number;++ payable: number;++ pending_payouts: number;++ /**++ * What payable is worth in US cents at today's rate.++ */++ payable_value_cents: number;++ bits_per_usd: number;++}++++/** [email protected] (owner: billing) */++/**++ * billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).++ */++export type BillingCashoutDecision =++ | {++ /**++ * The provider's payout id.++ */++ payout_reference: string;++ payout_provider?: string;++ }++ | {++ reason: string | null;++ }++ | NoBody;++++/** [email protected] (owner: billing) */++/**++ * billing.cashout-request@1: bodies of billing.cashout.request on OpenVibe.Billing. POST /api/v1/cashouts: { subject, amount, payout_method: { type, address } } moves amount bits (at least the minimum cashout) from the person's payable into payouts pending, in escrow until staff pay it (a PayPal address must be an email). POST /api/v1/recycle: { subject, amount } turns payable back into the person's own spendable credit. GET /api/v1/cashouts/:id takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).++ */++export type BillingCashoutRequest =++ | {++ /**++ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.++ */++ subject: SubjectRef | string;++ /**++ * Bits.++ */++ amount: number | string;++ payout_method: {++ /**++ * paypal, …++ */++ type: string;++ address: string;++ };++ }++ | {++ /**++ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.++ */++ subject: SubjectRef | string;++ /**++ * Bits.++ */++ amount: number | string;++ }++ | NoBody;++++/** [email protected] (owner: billing) */++/**++ * billing.cashout-result@1: answers about cashouts on OpenVibe.Billing. POST /api/v1/cashouts (201), GET /api/v1/cashouts/:id, POST /api/v1/cashouts/:id/approve and /deny → { cashout }; GET /api/v1/cashouts?status=&subject=&limit= → { cashouts }; POST /api/v1/recycle (201) → { transaction, balance }. A cashout the Events stream carries names the payout method's type only; this API answers the caller who may see the address.++ */++export type BillingCashoutResult =++ | {++ cashout: BillingCashout;++ }++ | {++ cashouts: BillingCashout[];++ }++ | {++ transaction: BillingTransaction;++ balance: BillingBalance;++ };++++/** [email protected] (owner: billing) */++/**++ * billing.cashout@1: one cashout as OpenVibe.Billing presents it (server/ops/cashouts.js present). Requested cashouts wait out an escrow; staff approve (with the provider's payout reference) or deny them.++ */++export interface BillingCashout {++ id: string;++ subject: SubjectRef;++ amount_bits: number;++ value_cents: number;++ status: "requested" | "paid" | "denied";++ payout_method: {++ type?: string;++ address?: string;++ } | null;++ escrow_until: string;++ request_txn: string;++ settle_txn?: string | null;++ payout_provider?: string | null;++ payout_reference?: string | null;++ reason?: string | null;++ created_at: string;++ updated_at?: string | null;++}++++/** [email protected] (owner: billing) */++/**++ * billing.entitlement-result@1: answers of billing.entitlement.check on OpenVibe.Billing. GET /api/v1/entitlements/:subject?streamer= → billing.entitlement@1; without streamer → { subject, entitlements } (every active one); GET /api/v1/subscriptions → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.++ */++export type BillingEntitlementResult =++ | BillingEntitlement++ | {++ subject: SubjectRef;++ entitlements: BillingEntitlement[];++ }++ | {++ subscriptions: BillingSubscription[];++ }++ | {++ subscription: BillingSubscription;++ entitlement: BillingEntitlement;++ };++++/** [email protected] (owner: billing) */++/**++ * billing.entitlement@1: whether a person is entitled to a streamer's subscriber perks now (OpenVibe.Billing server/ops/subscriptions.js entitlement): GET /api/v1/entitlements/:subject?streamer=, and inside subscription answers. Chained, already-paid future periods extend expires_at.++ */++export interface BillingEntitlement {++ subject: SubjectRef;++ streamer: SubjectRef;++ kind: string;++ active: boolean;++ expires_at: string | null;++ subscription: {++ id?: string;++ status?: string;++ auto_renew?: boolean | number;++ cancel_at_period_end?: boolean | number;++ provider?: string;++ } | null;++}++++/** [email protected] (owner: billing) */++/**++ * billing.intent-request@1: the body of POST /api/v1/intents on OpenVibe.Billing (billing.intent.create): a checkout with an enabled provider for a Vibes purchase ({ kind: purchase, bits } at least the minimum purchase) or a channel subscription ({ kind: subscription, streamer }; route direct pays the streamer's own account, and a direct PowerChat subscription names receiving_account). Billing prices it; the answer is { intent, checkout_url } (201). GET /api/v1/intents/:id and POST /api/v1/intents/:id/capture take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).++ */++export type BillingIntentRequest =++ | {++ /**++ * stripe, paypal, nowpayments, powerchat, ccbill (enabled ones only).++ */++ provider: string;++ kind: "purchase" | "subscription";++ /**++ * Who pays.++ */++ subject: SubjectRef | string;++ /**++ * Purchase: the Vibes to buy.++ */++ bits?: number | string;++ /**++ * Subscription: the channel's owner (never the payer).++ */++ streamer?: SubjectRef | string;++ /**++ * Subscription: direct, or the site route.++ */++ route?: string;++ /**++ * Direct PowerChat subscription: the streamer's PowerChat username.++ */++ receiving_account?: string;++ auto_renew?: boolean | number;++ success_url?: string;++ cancel_url?: string;++ }++ | NoBody;++++/** [email protected] (owner: billing) */++/**++ * billing.intent-result@1: answers of billing.intent.create on OpenVibe.Billing. POST /api/v1/intents → { intent, checkout_url } (null when the provider has no hosted checkout); GET /api/v1/intents/:id → { intent }; POST /api/v1/intents/:id/capture → { result, intent } (the settlement the provider's capture produced).++ */++export interface BillingIntentResult {++ intent: BillingIntent;++ checkout_url?: string | null;++ /**++ * Capture: what applying the provider's plan did.++ */++ result?: {++ [k: string]: unknown | undefined;++ };++}++++/** [email protected] (owner: billing) */++/**++ * billing.intent@1: a checkout intent as OpenVibe.Billing presents it (server/ops/intents.js present): what a person is about to pay a provider for (a Vibes purchase or a channel subscription), priced by Billing.++ */++export interface BillingIntent {++ id: string;++ provider: string;++ provider_ref: string | null;++ kind: "purchase" | "subscription";++ subject: SubjectRef;++ streamer: SubjectRef | null;++ amount_cents: number;++ fee_cents: number;++ bits: number;++ route: string | null;++ auto_renew?: number | boolean;++ /**++ * created, settled, failed, …++ */++ status: string;++ settled_txn: string | null;++ created_at: string;++ /**++ * PowerChat only: pcorder:<id> or pcsub:<id>, echoed back by its webhook.++ */++ checkout_ref?: string;++}++++/** [email protected] (owner: billing) */++/**++ * billing.subscription-request@1: the body of POST /api/v1/subscriptions on OpenVibe.Billing (billing.subscription.manage): pay one period of subscriber → streamer, from the subscriber's credit (source credit, the default) or from a provider receipt (source receipt, which also needs billing.ledger.admin: claiming money was received is operator grade). POST /api/v1/subscriptions/:id/cancel (cancels at the period end, with the provider too) and the GETs take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).++ */++export type BillingSubscriptionRequest =++ | {++ /**++ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.++ */++ subscriber: SubjectRef | string;++ /**++ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.++ */++ streamer: SubjectRef | string;++ source?: "credit" | "receipt";++ /**++ * Default: Billing's subscription price.++ */++ price_cents?: number | string;++ auto_renew?: boolean | number;++ receipt?: {++ provider: string;++ provider_ref: string;++ /**++ * Cents paid.++ */++ amount_cents?: number | string;++ fee_cents?: number | string;++ route?: string;++ subscription_ref?: string;++ test?: boolean;++ };++ }++ | NoBody;++++/** [email protected] (owner: billing) */++/**++ * billing.subscription-result@1: answers about subscriptions on OpenVibe.Billing. POST /api/v1/subscriptions (201) → { subscription, entitlement, transaction }; POST /api/v1/subscriptions/:id/cancel → { subscription, provider_sync }; GET /api/v1/subscriptions?subscriber=&streamer=&status= → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.++ */++export type BillingSubscriptionResult =++ | {++ subscription: BillingSubscription;++ /**++ * billing.entitlement@1 (the answer to POST /subscriptions carries the entitlement as booked).++ */++ entitlement?: {++ [k: string]: unknown | undefined;++ };++ transaction?: BillingTransaction;++ /**++ * not_needed, or how the provider was told.++ */++ provider_sync?: string;++ }++ | {++ subscriptions: BillingSubscription[];++ };++++/** [email protected] (owner: billing) */++/**++ * billing.subscription@1: one channel subscription as OpenVibe.Billing presents it (server/ops/subscriptions.js present).++ */++export interface BillingSubscription {++ id: string;++ subscriber: SubjectRef;++ streamer: SubjectRef;++ tier?: unknown;++ /**++ * credit, or the provider that takes the payments.++ */++ provider: string;++ provider_ref?: string | null;++ /**++ * site or direct (paid to the streamer's own provider account).++ */++ route?: string | null;++ /**++ * active, canceled, expired, …++ */++ status: string;++ auto_renew: boolean;++ cancel_at_period_end: boolean;++ price_cents?: number | null;++ current_period_end: string | null;++ created_at?: string;++ updated_at?: string | null;++}++++/** [email protected] (owner: billing) */++/**++ * billing.transaction@1: one journal transaction as OpenVibe.Billing's /api/v1 presents it (server/ledger.js present). Amounts are integers: bits for vibes-bits, cents for usd-cents; each transaction's entries balance per currency. Subjects are bare Network subject ids.++ */++export interface BillingTransaction {++ id: string;++ type:++ | "purchase"++ | "donation"++ | "subscription"++ | "subscription_share"++ | "cashout_request"++ | "cashout_paid"++ | "cashout_denied"++ | "recycle"++ | "refund"++ | "chargeback"++ | "adjustment"++ | "import";++ status: string;++ /**++ * Test money: never counted.++ */++ test: boolean;++ reverses_txn: string | null;++ from_subject: string | null;++ to_subject: string | null;++ provider: string | null;++ /**++ * <provider>:<provider payment id> for money that arrived from a provider.++ */++ receipt_ref: string | null;++ /**++ * Who posted it (as recorded).++ */++ actor?: {++ [k: string]: unknown | undefined;++ };++ /**++ * Per type: amount_bits, paid_cents, target EntityRef, message, rates…++ */++ metadata?: {++ [k: string]: unknown | undefined;++ };++ created_at: string;++ entries: {++ account: {++ kind:++ | "user_credit"++ | "creator_payable"++ | "provider_clearing"++ | "platform_revenue"++ | "payouts_pending"++ | "refunds"++ | "import_adjustment"++ | "chargeback_loss"++ | "fx_conversion";++ /**++ * A subject id, a provider slug, or null for a platform account.++ */++ owner?: string | null;++ currency: "vibes-bits" | "usd-cents";++ };++ /**++ * Signed: negative leaves the account.++ */++ amount: number;++ }[];++}++++/** [email protected] (owner: billing) */++/**++ * billing.transfer-request@1: bodies of billing.transfer.create on OpenVibe.Billing. POST /api/v1/transfers: { from, to, amount, kind?, target?, message? } moves amount bits from the sender's credit to the recipient's payable (a tip, donation or paid interaction; never to oneself). POST /api/v1/transfers/:id/refund: { amount?, reason? } gives a credit-funded donation back (all that remains refundable when amount is absent). Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).++ */++export type BillingTransferRequest =++ | {++ /**++ * The sender (their credit pays).++ */++ from: SubjectRef | string;++ /**++ * The recipient (their payable receives).++ */++ to: SubjectRef | string;++ /**++ * Bits.++ */++ amount: number | string;++ /**++ * Default donation.++ */++ kind?: "tip" | "donation" | "paid_interaction";++ target?: EntityRef;++ message?: string | null;++ }++ | {++ /**++ * Bits to give back; default all that remains refundable.++ */++ amount?: number | string;++ reason?: string | null;++ };++++/** [email protected] (owner: billing) */++/**++ * billing.transfer-result@1: answers of billing.transfer.create on OpenVibe.Billing (201). POST /api/v1/transfers → { transaction, balance: { credit } } (the sender's credit after it); POST /api/v1/transfers/:id/refund → { transaction } (type refund, reverses_txn the donation).++ */++export interface BillingTransferResult {++ transaction: BillingTransaction;++ balance?: {++ credit: number;++ };++}++/** [email protected] (owner: deals) *//*** deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.@@@ -13682,6 +14249,158 @@ export interface ChatModerationActionPayload {details?: {};}++/** [email protected] (owner: chat) */++/**++ * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.++ */++export type ChatSendRequest =++ | {++ type: "chat";++ /**++ * The text (trimmed by Chat).++ */++ message: string;++ /**++ * Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored).++ */++ reply_to_id?: number | string;++ /**++ * Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off).++ */++ auto_delete_minutes?: number | string;++ /**++ * The voice channel the sender is in, echoed on the broadcast.++ */++ voiceChannelId?: string;++ }++ | {++ /**++ * Live's boot id; placeholder message ids are mapped per boot.++ */++ boot?: string;++ /**++ * @maxItems 500++ */++ ops: {++ /**++ * Echoed on the result.++ */++ seq?: number;++ /**++ * A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0]).++ */++ op: string;++ args?: unknown[];++ /**++ * Live's placeholder id for the row this op inserts.++ */++ ref?: number;++ /**++ * Idempotency key: applied once.++ */++ key?: string;++ }[];++ };++++/** [email protected] (owner: chat) */++/**++ * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.++ */++export type ChatSendResult =++ | {++ type: "chat";++ /**++ * The stored message id.++ */++ id?: number;++ /**++ * Display name, or the anonymous id.++ */++ username: string;++ /**++ * The immutable login handle; null for an anonymous sender.++ */++ core_username?: string | null;++ user_id?: number | null;++ anon_id?: string | null;++ /**++ * The sender's role, anon for an anonymous sender.++ */++ role: string;++ message: string;++ stream_id: number | null;++ channel_user_id: number | null;++ /**++ * true for homepage global chat (no stream, no channel).++ */++ is_global: boolean;++ avatar_url?: string | null;++ profile_color?: string;++ filtered?: boolean;++ timestamp: string;++ auto_delete_at?: string | null;++ voiceChannelId?: string;++ reply_to?: {++ id: number;++ username: string;++ user_id?: number | null;++ /**++ * The parent's text, cut to 100 characters.++ */++ message: string;++ };++ /**++ * Cosmetic name effect.++ */++ nameFX?: {++ [k: string]: unknown | undefined;++ };++ /**++ * Cosmetic particle effect.++ */++ particleFX?: {++ [k: string]: unknown | undefined;++ };++ /**++ * Cosmetic hat.++ */++ hatFX?: {++ [k: string]: unknown | undefined;++ };++ /**++ * Cosmetic TTS voice effect.++ */++ voiceFX?: {++ [k: string]: unknown | undefined;++ };++ /**++ * The sender's equipped tag.++ */++ tag?: {++ [k: string]: unknown | undefined;++ };++ /**++ * The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows.++ */++ vip_badge?: {++ [k: string]: unknown | undefined;++ };++ }++ | {++ type: "system" | "error";++ message: string;++ }++ | {++ ok: true;++ results: {++ seq?: number;++ ok: boolean;++ result?: unknown;++ code?: string;++ error?: string;++ }[];++ };++/** [email protected] (owner: live) *//*** live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).@@@ -13742,3 +14461,395 @@ export interface CommunityModerationActionPayload {*/details?: {};}++++/** [email protected] (owner: community) */++/**++ * community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.++ */++export type CommunityPasteCreateRequest = {++ [k: string]: unknown | undefined;++} & {++ /**++ * The text (trimmed).++ */++ content?: string;++ /**++ * Multipart only: the image part.++ */++ screenshot?: string;++ /**++ * Trimmed to 200 characters; Untitled (Screenshot for an image) when blank.++ */++ title?: string | null;++ /**++ * A language hint; auto or absent sniffs it from the content.++ */++ language?: string;++ /**++ * public (default), unlisted or private.++ */++ visibility?: string;++ /**++ * Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else.++ */++ burn_after_read?: boolean | number | string;++ is_nsfw?: boolean | number | string;++ /**++ * The Live stream it was made during (digits).++ */++ stream_id?: number | string;++ /**++ * Multipart screenshot: the text shown under the image.++ */++ description?: string;++ /**++ * Multipart screenshot: the page it shows.++ */++ page_url?: string;++ /**++ * Multipart screenshot: the capturing browser.++ */++ user_agent?: string;++ /**++ * Service callers, public pastes only: the slug to use (409 if taken).++ */++ slug?: string;++ /**++ * Service callers only: a JSON object (at most 16 KB).++ */++ metadata?: {} | string;++ /**++ * Service callers only (cut to 2000 characters).++ */++ ai_summary?: string;++ /**++ * Service callers only, with ai_summary: JSON text or a value to serialise.++ */++ ai_tags?: {++ [k: string]: unknown | undefined;++ };++};++++/** [email protected] (owner: community) */++/**++ * community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.++ */++export type CommunityPasteModerateRequest =++ | {++ /**++ * @minItems 1++ */++ slugs: [string, ...string[]];++ action: "delete" | "public" | "unlisted" | "private";++ }++ | {++ /**++ * The multipart part named screenshot: the censored image.++ */++ screenshot: string;++ }++ | NoBody;++++/** [email protected] (owner: community) */++/**++ * community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).++ */++export type CommunityPasteModerateResult =++ | {++ done: number;++ skipped: number;++ }++ | {++ paste: CommunityPaste;++ }++ | {++ message: string;++ };++++/** [email protected] (owner: community) */++/**++ * community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.++ */++export type CommunityPasteWriteRequest =++ | {++ title?: string | null;++ content?: string;++ language?: string;++ /**++ * public, unlisted or private (anything else is public).++ */++ visibility?: string;++ is_nsfw?: boolean | number | string;++ /**++ * Staff only; ignored otherwise.++ */++ pinned?: boolean | number | string;++ }++ | {++ message: string;++ /**++ * The top-level comment this replies to.++ */++ parent_id?: number | string;++ /**++ * An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default).++ */++ anon_name?: string;++ }++ | NoBody;++++/** [email protected] (owner: community) */++/**++ * community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.++ */++export type CommunityPasteWriteResult =++ | {++ id?: number;++ slug?: string;++ url?: string;++ paste: CommunityPaste;++ }++ | {++ success: true;++ }++ | {++ liked: boolean;++ likes: number;++ }++ | {++ copies: number;++ }++ | {++ comment: {++ id: number;++ paste_id: number;++ /**++ * The author's subject; null for an anonymous comment.++ */++ user_id?: string | null;++ author_subject?: string | null;++ parent_id?: number | null;++ anon_name?: string | null;++ message: string;++ is_deleted?: number | boolean;++ created_at: string;++ updated_at?: string | null;++ username?: string | null;++ display_name?: string | null;++ avatar_url?: string | null;++ profile_color?: string | null;++ };++ }++ | {++ message: string;++ };++++/** [email protected] (owner: community) */++/**++ * community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).++ */++export interface CommunityPaste {++ id: number;++ app_id: "community";++ slug: string;++ /**++ * The owner's subject (usr_…), or null for an anonymous paste.++ */++ user_id: string | null;++ owner_subject: string | null;++ /**++ * user, or ai for a paste an AI service wrote.++ */++ origin: string;++ type: "paste" | "screenshot";++ title: string;++ content?: string | null;++ language?: string | null;++ visibility: "public" | "unlisted" | "private";++ /**++ * The Live stream it was made during, when its stream_ref is one.++ */++ stream_id?: number | null;++ /**++ * An EntityRef of where it was made (a Live stream, or a service's own source).++ */++ stream_ref?: {} | null;++ screenshot_url?: string | null;++ /**++ * Structured metadata a service attached (AI moments), or a screenshot's upload details.++ */++ metadata?: {++ [k: string]: unknown | undefined;++ };++ burn_after_read?: boolean;++ forked_from?: number | null;++ pinned?: boolean;++ views?: number;++ unique_views?: number;++ copies?: number;++ likes?: number;++ is_nsfw?: boolean;++ ai_summary?: string | null;++ /**++ * JSON text as the AI pass wrote it.++ */++ ai_tags?: string | null;++ ai_analyzed_at?: string | null;++ revision?: number;++ /**++ * /p/<slug>++ */++ url: string;++ /**++ * /p/<slug>/raw++ */++ raw_url: string;++ created_at: string;++ updated_at?: string | null;++ /**++ * Whether the caller owns it.++ */++ is_owner: boolean;++ username?: string | null;++ /**++ * OpenVibe AI for an AI paste.++ */++ display_name?: string | null;++ avatar_url?: string | null;++ profile_color?: string | null;++}++++/** [email protected] (owner: community) */++/**++ * community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.++ */++export type CommunityModerationRequest =++ | {++ visibility: "public" | "hidden" | "locked";++ }++ | {++ [k: string]: unknown | undefined;++ }++ | {++ /**++ * The space's slug.++ */++ space: string;++ webhook_url_ref: string;++ /**++ * Default true.++ */++ enabled?: boolean;++ }++ | {++ enabled: boolean;++ }++ | NoBody;++++/** [email protected] (owner: community) */++/**++ * community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.++ */++export type CommunityModerationResult =++ | {++ thread: {++ id: number | string;++ /**++ * A comment thread's browser id (cth_…).++ */++ access_id?: string;++ ref?: EntityRef;++ visibility?: string;++ comment_count?: number | null;++ space?: string;++ slug?: string;++ title?: string;++ pinned?: boolean;++ locked?: boolean;++ };++ }++ | {++ post: {++ id: number;++ thread_id: number;++ is_opening?: boolean;++ origin?: string;++ author?: {} | null;++ body_markdown?: string | null;++ body_html?: string | null;++ revision: number;++ deleted: boolean;++ created_at?: string | null;++ updated_at?: string | null;++ can_edit?: boolean;++ attachments?: unknown[];++ pastes?: unknown[];++ };++ }++ | {++ ok: true;++ id?: number;++ deleted?: "thread";++ }++ | {++ [k: string]: unknown | undefined;++ }++ | {++ mapping: {++ id: number;++ space: string;++ direction?: string;++ webhook_url_ref: string;++ webhook_configured: boolean;++ enabled: boolean;++ created_at?: string | null;++ };++ };++++/** [email protected] (owner: community) */++/**++ * community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).++ */++export type CommunityPostWriteRequest =++ | {++ [k: string]: unknown | undefined;++ }++ | {++ /**++ * A thread vote; 0 takes the vote back.++ */++ value: 1 | -1 | 0 | "1" | "-1" | "0";++ }++ | NoBody;++++/** [email protected] (owner: community) */++/**++ * community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.++ */++export type CommunityPulseWriteResult =++ | {++ created: boolean;++ item: {++ id: number;++ source: {++ service: string;++ type: string;++ id: string;++ };++ title: string;++ url: string;++ origin: "user" | "ai" | "system";++ label: "AI" | "System" | null;++ actor: {++ subject?: string | null;++ username?: string | null;++ display_name?: string | null;++ avatar_url?: string | null;++ profile_color?: string | null;++ } | null;++ occurred_at: string | null;++ };++ }++ | {++ removed: number;++ };@@@ -17,5 +17,7 @@"GET /api/v1/balances/:subject","GET /api/v1/transactions","GET /api/v1/transactions/:id"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "billing.balance-read-result@1"}@@@ -21,5 +21,7 @@"GET /api/v1/cashouts","POST /api/v1/cashouts/:id/approve","POST /api/v1/cashouts/:id/deny"-- ]++ ],++ "inputSchema": "billing.cashout-decision@1",++ "outputSchema": "billing.cashout-result@1"}@@@ -20,5 +20,7 @@"POST /api/v1/cashouts","POST /api/v1/recycle","GET /api/v1/cashouts/:id"-- ]++ ],++ "inputSchema": "billing.cashout-request@1",++ "outputSchema": "billing.cashout-result@1"}@@@ -17,5 +17,7 @@"GET /api/v1/entitlements/:subject","GET /api/v1/subscriptions","GET /api/v1/subscriptions/:id"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "billing.entitlement-result@1"}@@@ -17,5 +17,7 @@"POST /api/v1/intents","GET /api/v1/intents/:id","POST /api/v1/intents/:id/capture"-- ]++ ],++ "inputSchema": "billing.intent-request@1",++ "outputSchema": "billing.intent-result@1"}@@@ -24,5 +24,7 @@"POST /api/v1/admin/provider-events/:id/reprocess","POST /api/v1/admin/sweep","GET /api/v1/admin/import-holds"-- ]++ ],++ "inputSchema": "billing.admin-request@1",++ "outputSchema": "billing.admin-result@1"}@@@ -22,5 +22,7 @@"POST /api/v1/subscriptions/:id/cancel","GET /api/v1/subscriptions","GET /api/v1/subscriptions/:id"-- ]++ ],++ "inputSchema": "billing.subscription-request@1",++ "outputSchema": "billing.subscription-result@1"}@@@ -19,5 +19,7 @@"implementedBy": ["POST /api/v1/transfers","POST /api/v1/transfers/:id/refund"-- ]++ ],++ "inputSchema": "billing.transfer-request@1",++ "outputSchema": "billing.transfer-result@1"}@@@ -18,5 +18,7 @@"implementedBy": ["WS /ws/chat {type:\"chat\"}","POST /internal/live/calls"-- ]++ ],++ "inputSchema": "chat.send-request@1",++ "outputSchema": "chat.send-result@1"}@@@ -23,5 +23,7 @@"DELETE /api/v1/spaces/:space/threads/:slug","PUT|DELETE /api/v1/posts/:id","GET|POST|PUT /api/v1/relay/*"-- ]++ ],++ "inputSchema": "community.moderation-request@1",++ "outputSchema": "community.moderation-result@1"}@@@ -18,5 +18,6 @@],"implementedBy": ["POST /api/pastes (Community)"-- ]++ ],++ "inputSchema": "community.paste-create-request@1"}@@@ -19,5 +19,7 @@"POST /api/pastes/:slug/censor","POST /api/pastes/bulk","DELETE /api/pastes/:slug/comments/:id"-- ]++ ],++ "inputSchema": "community.paste-moderate-request@1",++ "outputSchema": "community.paste-moderate-result@1"}@@@ -19,5 +19,7 @@"implementedBy": ["PUT|DELETE /api/pastes/:slug","POST /api/pastes/:slug/{fork,like,copy,comments}"-- ]++ ],++ "inputSchema": "community.paste-write-request@1",++ "outputSchema": "community.paste-write-result@1"}@@@ -23,5 +23,6 @@"POST /api/v1/spaces/:space/threads/:slug/votes","PUT|DELETE /api/v1/posts/:id","DELETE /api/v1/spaces/:space/threads/:slug"-- ]++ ],++ "inputSchema": "community.post-write-request@1"}@@@ -17,5 +17,6 @@"implementedBy": ["GET /api/v1/pulse","GET /pulse"-- ]++ ],++ "inputSchema": "common.no-body@1"}@@@ -19,5 +19,6 @@"implementedBy": ["POST /api/v1/pulse/items","DELETE /api/v1/pulse/items/:service/:type/:id"-- ]++ ],++ "outputSchema": "community.pulse-write-result@1"}@@@ -18,5 +18,6 @@"GET /api/v1/spaces","GET /api/v1/spaces/:space","GET /s"-- ]++ ],++ "inputSchema": "common.no-body@1"}@@@ -21,5 +21,6 @@"GET /s/:space/t/:slug","GET /s/feed.xml","GET /s/:space/feed.xml"-- ]++ ],++ "inputSchema": "common.no-body@1"}
@@ -28,62 +28,6 @@
@@ -28,62 +28,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "billing.balance.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.cashout.manage",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.cashout.request",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.entitlement.check",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.intent.create",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.ledger.admin",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.subscription.manage",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "billing.transfer.create",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
{
{
"id": "blog.blog.configure",
"id": "blog.blog.configure",
"missing": [
"missing": [
@@ -168,13 +112,6 @@
@@ -168,13 +112,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "chat.message.send",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
{
{
"id": "codes.release.manage",
"id": "codes.release.manage",
"missing": [
"missing": [
@@ -187,63 +124,6 @@
@@ -187,63 +124,6 @@
"inputSchema"
"inputSchema"
]
]
},
},
- {
- "id": "community.comment.moderate",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "community.paste.create",
- "missing": [
- "inputSchema"
- ]
- },
- {
- "id": "community.paste.moderate",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "community.paste.write",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "community.post.create",
- "missing": [
- "inputSchema"
- ]
- },
- {
- "id": "community.pulse.read",
- "missing": [
- "inputSchema"
- ]
- },
- {
- "id": "community.pulse.write",
- "missing": [
- "outputSchema"
- ]
- },
- {
- "id": "community.space.read",
- "missing": [
- "inputSchema"
- ]
- },
- {
- "id": "community.thread.read",
- "missing": [
- "inputSchema"
- ]
- },
{
{
"id": "coupons.coupon.lookup",
"id": "coupons.coupon.lookup",
"missing": [
"missing": [
new file mode 100644
new file mode 100644
@@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/admin-request.v1.json",
+ "title": "BillingAdminRequest",
+ "description": "billing.admin-request@1: bodies of billing.ledger.admin on OpenVibe.Billing (operators). POST /api/v1/admin/freeze: { on, reason? } (unfreezing processes the provider deliveries stored meanwhile; allowed while frozen). POST /api/v1/admin/adjustments: { from, to, amount, reason, relates_to? } moves value between two accounts of one currency. POST /api/v1/purchases/settle: { provider, provider_ref, subject?, amount_cents, bits, intent_id?, test? } books a provider receipt. POST /api/v1/admin/provider-events/:id/reprocess, POST /api/v1/admin/sweep and the GETs (freeze, reconcile, provider-events, import-holds) take no body. Adjustments and settlements need an Idempotency-Key and are refused while frozen.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "on"
+ ],
+ "properties": {
+ "on": {
+ "type": "boolean"
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "from",
+ "to",
+ "amount",
+ "reason"
+ ],
+ "properties": {
+ "from": {
+ "type": "object",
+ "required": [
+ "kind",
+ "currency"
+ ],
+ "properties": {
+ "kind": {
+ "enum": [
+ "user_credit",
+ "creator_payable",
+ "provider_clearing",
+ "platform_revenue",
+ "payouts_pending",
+ "refunds",
+ "import_adjustment",
+ "chargeback_loss",
+ "fx_conversion"
+ ]
+ },
+ "owner": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "currency": {
+ "enum": [
+ "vibes-bits",
+ "usd-cents"
+ ]
+ }
+ }
+ },
+ "to": {
+ "type": "object",
+ "required": [
+ "kind",
+ "currency"
+ ],
+ "properties": {
+ "kind": {
+ "enum": [
+ "user_credit",
+ "creator_payable",
+ "provider_clearing",
+ "platform_revenue",
+ "payouts_pending",
+ "refunds",
+ "import_adjustment",
+ "chargeback_loss",
+ "fx_conversion"
+ ]
+ },
+ "owner": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "currency": {
+ "enum": [
+ "vibes-bits",
+ "usd-cents"
+ ]
+ }
+ }
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "In the accounts' currency (at most 1,000,000,000)."
+ },
+ "reason": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "relates_to": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "provider",
+ "provider_ref"
+ ],
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "provider_ref": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The provider's payment id."
+ },
+ "subject": {
+ "description": "Who the Vibes are for.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount_cents": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Cents paid."
+ },
+ "bits": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Vibes to credit."
+ },
+ "intent_id": {
+ "type": "string"
+ },
+ "test": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,215 @@
@@ -0,0 +1,215 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/admin-result.v1.json",
+ "title": "BillingAdminResult",
+ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "frozen",
+ "reason",
+ "frozen_at",
+ "frozen_by"
+ ],
+ "properties": {
+ "frozen": {
+ "type": "boolean"
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "frozen_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "frozen_by": {},
+ "processed_after_unfreeze": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "ok",
+ "checks"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "ok": {
+ "type": "boolean"
+ },
+ "trigger": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": "string"
+ },
+ "finished_at": {
+ "type": "string"
+ },
+ "checks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "ok"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "ok": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "warnings": {
+ "type": "array"
+ },
+ "totals": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "transaction"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "duplicate_receipt": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "events"
+ ],
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "provider"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "provider": {
+ "type": "string"
+ },
+ "provider_event_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "processed_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "result": {},
+ "attempts": {
+ "type": "integer"
+ },
+ "last_error": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "event"
+ ],
+ "properties": {
+ "event": {
+ "type": "object",
+ "required": [
+ "id",
+ "provider"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "renewed",
+ "expired",
+ "canceled",
+ "skipped"
+ ],
+ "properties": {
+ "renewed": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "expired": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "canceled": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "skipped": {
+ "type": "integer"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "holds"
+ ],
+ "properties": {
+ "holds": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/balance-read-result.v1.json",
+ "title": "BillingBalanceReadResult",
+ "description": "billing.balance-read-result@1: answers of billing.balance.read on OpenVibe.Billing. GET /api/v1/balances/:subject → billing.balance@1; GET /api/v1/transactions?subject=&limit=&cursor= → { transactions, next_cursor } (newest first, at most 200 a page; subject is required); GET /api/v1/transactions/:id → { transaction, reversed_by } (ids of the transactions that reverse it).",
+ "anyOf": [
+ {
+ "$ref": "balance.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "transactions",
+ "next_cursor"
+ ],
+ "properties": {
+ "transactions": {
+ "type": "array",
+ "maxItems": 200,
+ "items": {
+ "$ref": "transaction.v1.json"
+ }
+ },
+ "next_cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "transaction",
+ "reversed_by"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "reversed_by": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/balance.v1.json",
+ "title": "BillingBalance",
+ "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.",
+ "type": "object",
+ "required": ["subject", "currency", "credit", "payable", "pending_payouts", "payable_value_cents", "bits_per_usd"],
+ "properties": {
+ "subject": { "$ref": "../identity/subject-ref.v1.json" },
+ "currency": { "const": "vibes-bits" },
+ "credit": { "type": "integer" },
+ "payable": { "type": "integer" },
+ "pending_payouts": { "type": "integer" },
+ "payable_value_cents": { "type": "integer", "description": "What payable is worth in US cents at today's rate." },
+ "bits_per_usd": { "type": "number" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/cashout-decision.v1.json",
+ "title": "BillingCashoutDecision",
+ "description": "billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "payout_reference"
+ ],
+ "properties": {
+ "payout_reference": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The provider's payout id."
+ },
+ "payout_provider": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "reason"
+ ],
+ "properties": {
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/cashout-request.v1.json",
+ "title": "BillingCashoutRequest",
+ "description": "billing.cashout-request@1: bodies of billing.cashout.request on OpenVibe.Billing. POST /api/v1/cashouts: { subject, amount, payout_method: { type, address } } moves amount bits (at least the minimum cashout) from the person's payable into payouts pending, in escrow until staff pay it (a PayPal address must be an email). POST /api/v1/recycle: { subject, amount } turns payable back into the person's own spendable credit. GET /api/v1/cashouts/:id takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "subject",
+ "amount",
+ "payout_method"
+ ],
+ "properties": {
+ "subject": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits."
+ },
+ "payout_method": {
+ "type": "object",
+ "required": [
+ "type",
+ "address"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",
+ "description": "paypal, …"
+ },
+ "address": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 254
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "subject",
+ "amount"
+ ],
+ "properties": {
+ "subject": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits."
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
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/billing/cashout-result.v1.json",
+ "title": "BillingCashoutResult",
+ "description": "billing.cashout-result@1: answers about cashouts on OpenVibe.Billing. POST /api/v1/cashouts (201), GET /api/v1/cashouts/:id, POST /api/v1/cashouts/:id/approve and /deny → { cashout }; GET /api/v1/cashouts?status=&subject=&limit= → { cashouts }; POST /api/v1/recycle (201) → { transaction, balance }. A cashout the Events stream carries names the payout method's type only; this API answers the caller who may see the address.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "cashout"
+ ],
+ "properties": {
+ "cashout": {
+ "$ref": "cashout.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "cashouts"
+ ],
+ "properties": {
+ "cashouts": {
+ "type": "array",
+ "items": {
+ "$ref": "cashout.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "transaction",
+ "balance"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "balance": {
+ "$ref": "balance.v1.json"
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/cashout.v1.json",
+ "title": "BillingCashout",
+ "description": "billing.cashout@1: one cashout as OpenVibe.Billing presents it (server/ops/cashouts.js present). Requested cashouts wait out an escrow; staff approve (with the provider's payout reference) or deny them.",
+ "type": "object",
+ "required": ["id", "subject", "amount_bits", "value_cents", "status", "payout_method", "escrow_until", "request_txn", "created_at"],
+ "properties": {
+ "id": { "type": "string" },
+ "subject": { "$ref": "../identity/subject-ref.v1.json" },
+ "amount_bits": { "type": "integer", "minimum": 1 },
+ "value_cents": { "type": "integer", "minimum": 0 },
+ "status": { "enum": ["requested", "paid", "denied"] },
+ "payout_method": {
+ "type": ["object", "null"],
+ "properties": {
+ "type": { "type": "string" },
+ "address": { "type": "string" }
+ }
+ },
+ "escrow_until": { "type": "string" },
+ "request_txn": { "type": "string" },
+ "settle_txn": { "type": ["string", "null"] },
+ "payout_provider": { "type": ["string", "null"] },
+ "payout_reference": { "type": ["string", "null"] },
+ "reason": { "type": ["string", "null"] },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": ["string", "null"] }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/entitlement-result.v1.json",
+ "title": "BillingEntitlementResult",
+ "description": "billing.entitlement-result@1: answers of billing.entitlement.check on OpenVibe.Billing. GET /api/v1/entitlements/:subject?streamer= → billing.entitlement@1; without streamer → { subject, entitlements } (every active one); GET /api/v1/subscriptions → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",
+ "anyOf": [
+ {
+ "$ref": "entitlement.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "subject",
+ "entitlements"
+ ],
+ "properties": {
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "entitlements": {
+ "type": "array",
+ "items": {
+ "$ref": "entitlement.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "subscriptions"
+ ],
+ "properties": {
+ "subscriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "subscription.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "subscription",
+ "entitlement"
+ ],
+ "properties": {
+ "subscription": {
+ "$ref": "subscription.v1.json"
+ },
+ "entitlement": {
+ "$ref": "entitlement.v1.json"
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/entitlement.v1.json",
+ "title": "BillingEntitlement",
+ "description": "billing.entitlement@1: whether a person is entitled to a streamer's subscriber perks now (OpenVibe.Billing server/ops/subscriptions.js entitlement): GET /api/v1/entitlements/:subject?streamer=, and inside subscription answers. Chained, already-paid future periods extend expires_at.",
+ "type": "object",
+ "required": ["subject", "streamer", "kind", "active", "expires_at", "subscription"],
+ "properties": {
+ "subject": { "$ref": "../identity/subject-ref.v1.json" },
+ "streamer": { "$ref": "../identity/subject-ref.v1.json" },
+ "kind": { "type": "string" },
+ "active": { "type": "boolean" },
+ "expires_at": { "type": ["string", "null"] },
+ "subscription": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "string" },
+ "status": { "type": "string" },
+ "auto_renew": { "type": ["boolean", "integer"] },
+ "cancel_at_period_end": { "type": ["boolean", "integer"] },
+ "provider": { "type": "string" }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/intent-request.v1.json",
+ "title": "BillingIntentRequest",
+ "description": "billing.intent-request@1: the body of POST /api/v1/intents on OpenVibe.Billing (billing.intent.create): a checkout with an enabled provider for a Vibes purchase ({ kind: purchase, bits } at least the minimum purchase) or a channel subscription ({ kind: subscription, streamer }; route direct pays the streamer's own account, and a direct PowerChat subscription names receiving_account). Billing prices it; the answer is { intent, checkout_url } (201). GET /api/v1/intents/:id and POST /api/v1/intents/:id/capture take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "provider",
+ "kind",
+ "subject"
+ ],
+ "properties": {
+ "provider": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",
+ "description": "stripe, paypal, nowpayments, powerchat, ccbill (enabled ones only)."
+ },
+ "kind": {
+ "enum": [
+ "purchase",
+ "subscription"
+ ]
+ },
+ "subject": {
+ "description": "Who pays.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "bits": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Purchase: the Vibes to buy."
+ },
+ "streamer": {
+ "description": "Subscription: the channel's owner (never the payer).",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "route": {
+ "type": "string",
+ "description": "Subscription: direct, or the site route."
+ },
+ "receiving_account": {
+ "type": "string",
+ "description": "Direct PowerChat subscription: the streamer's PowerChat username."
+ },
+ "auto_renew": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "success_url": {
+ "type": "string"
+ },
+ "cancel_url": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/intent-result.v1.json",
+ "title": "BillingIntentResult",
+ "description": "billing.intent-result@1: answers of billing.intent.create on OpenVibe.Billing. POST /api/v1/intents → { intent, checkout_url } (null when the provider has no hosted checkout); GET /api/v1/intents/:id → { intent }; POST /api/v1/intents/:id/capture → { result, intent } (the settlement the provider's capture produced).",
+ "type": "object",
+ "required": [
+ "intent"
+ ],
+ "properties": {
+ "intent": {
+ "$ref": "intent.v1.json"
+ },
+ "checkout_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "result": {
+ "description": "Capture: what applying the provider's plan did."
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/intent.v1.json",
+ "title": "BillingIntent",
+ "description": "billing.intent@1: a checkout intent as OpenVibe.Billing presents it (server/ops/intents.js present): what a person is about to pay a provider for (a Vibes purchase or a channel subscription), priced by Billing.",
+ "type": "object",
+ "required": ["id", "provider", "provider_ref", "kind", "subject", "streamer", "amount_cents", "fee_cents", "bits", "route", "status", "settled_txn", "created_at"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^pi_" },
+ "provider": { "type": "string" },
+ "provider_ref": { "type": ["string", "null"] },
+ "kind": { "enum": ["purchase", "subscription"] },
+ "subject": { "$ref": "../identity/subject-ref.v1.json" },
+ "streamer": { "anyOf": [{ "$ref": "../identity/subject-ref.v1.json" }, { "type": "null" }] },
+ "amount_cents": { "type": "integer", "minimum": 0 },
+ "fee_cents": { "type": "integer", "minimum": 0 },
+ "bits": { "type": "integer", "minimum": 0 },
+ "route": { "type": ["string", "null"] },
+ "auto_renew": { "type": ["integer", "boolean"] },
+ "status": { "type": "string", "description": "created, settled, failed, …" },
+ "settled_txn": { "type": ["string", "null"] },
+ "created_at": { "type": "string" },
+ "checkout_ref": { "type": "string", "description": "PowerChat only: pcorder:<id> or pcsub:<id>, echoed back by its webhook." }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/subscription-request.v1.json",
+ "title": "BillingSubscriptionRequest",
+ "description": "billing.subscription-request@1: the body of POST /api/v1/subscriptions on OpenVibe.Billing (billing.subscription.manage): pay one period of subscriber → streamer, from the subscriber's credit (source credit, the default) or from a provider receipt (source receipt, which also needs billing.ledger.admin: claiming money was received is operator grade). POST /api/v1/subscriptions/:id/cancel (cancels at the period end, with the provider too) and the GETs take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscriber",
+ "streamer"
+ ],
+ "properties": {
+ "subscriber": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "streamer": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "source": {
+ "enum": [
+ "credit",
+ "receipt"
+ ]
+ },
+ "price_cents": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Default: Billing's subscription price."
+ },
+ "auto_renew": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "receipt": {
+ "type": "object",
+ "required": [
+ "provider",
+ "provider_ref"
+ ],
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "provider_ref": {
+ "type": "string"
+ },
+ "amount_cents": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Cents paid."
+ },
+ "fee_cents": {
+ "type": [
+ "integer",
+ "string"
+ ]
+ },
+ "route": {
+ "type": "string"
+ },
+ "subscription_ref": {
+ "type": "string"
+ },
+ "test": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/subscription-result.v1.json",
+ "title": "BillingSubscriptionResult",
+ "description": "billing.subscription-result@1: answers about subscriptions on OpenVibe.Billing. POST /api/v1/subscriptions (201) → { subscription, entitlement, transaction }; POST /api/v1/subscriptions/:id/cancel → { subscription, provider_sync }; GET /api/v1/subscriptions?subscriber=&streamer=&status= → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscription"
+ ],
+ "properties": {
+ "subscription": {
+ "$ref": "subscription.v1.json"
+ },
+ "entitlement": {
+ "description": "billing.entitlement@1 (the answer to POST /subscriptions carries the entitlement as booked)."
+ },
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "provider_sync": {
+ "type": "string",
+ "description": "not_needed, or how the provider was told."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "subscriptions"
+ ],
+ "properties": {
+ "subscriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "subscription.v1.json"
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/subscription.v1.json",
+ "title": "BillingSubscription",
+ "description": "billing.subscription@1: one channel subscription as OpenVibe.Billing presents it (server/ops/subscriptions.js present).",
+ "type": "object",
+ "required": ["id", "subscriber", "streamer", "provider", "status", "auto_renew", "cancel_at_period_end", "current_period_end"],
+ "properties": {
+ "id": { "type": "string" },
+ "subscriber": { "$ref": "../identity/subject-ref.v1.json" },
+ "streamer": { "$ref": "../identity/subject-ref.v1.json" },
+ "tier": {},
+ "provider": { "type": "string", "description": "credit, or the provider that takes the payments." },
+ "provider_ref": { "type": ["string", "null"] },
+ "route": { "type": ["string", "null"], "description": "site or direct (paid to the streamer's own provider account)." },
+ "status": { "type": "string", "description": "active, canceled, expired, …" },
+ "auto_renew": { "type": "boolean" },
+ "cancel_at_period_end": { "type": "boolean" },
+ "price_cents": { "type": ["integer", "null"] },
+ "current_period_end": { "type": ["string", "null"] },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": ["string", "null"] }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/transaction.v1.json",
+ "title": "BillingTransaction",
+ "description": "billing.transaction@1: one journal transaction as OpenVibe.Billing's /api/v1 presents it (server/ledger.js present). Amounts are integers: bits for vibes-bits, cents for usd-cents; each transaction's entries balance per currency. Subjects are bare Network subject ids.",
+ "type": "object",
+ "required": ["id", "type", "status", "test", "reverses_txn", "from_subject", "to_subject", "provider", "receipt_ref", "created_at", "entries"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^txn_" },
+ "type": { "enum": ["purchase", "donation", "subscription", "subscription_share", "cashout_request", "cashout_paid", "cashout_denied", "recycle", "refund", "chargeback", "adjustment", "import"] },
+ "status": { "type": "string" },
+ "test": { "type": "boolean", "description": "Test money: never counted." },
+ "reverses_txn": { "type": ["string", "null"] },
+ "from_subject": { "type": ["string", "null"] },
+ "to_subject": { "type": ["string", "null"] },
+ "provider": { "type": ["string", "null"] },
+ "receipt_ref": { "type": ["string", "null"], "description": "<provider>:<provider payment id> for money that arrived from a provider." },
+ "actor": { "description": "Who posted it (as recorded)." },
+ "metadata": { "description": "Per type: amount_bits, paid_cents, target EntityRef, message, rates…" },
+ "created_at": { "type": "string" },
+ "entries": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["account", "amount"],
+ "properties": {
+ "account": {
+ "type": "object",
+ "required": ["kind", "currency"],
+ "properties": {
+ "kind": { "enum": ["user_credit", "creator_payable", "provider_clearing", "platform_revenue", "payouts_pending", "refunds", "import_adjustment", "chargeback_loss", "fx_conversion"] },
+ "owner": { "type": ["string", "null"], "description": "A subject id, a provider slug, or null for a platform account." },
+ "currency": { "enum": ["vibes-bits", "usd-cents"] }
+ }
+ },
+ "amount": { "type": "integer", "description": "Signed: negative leaves the account." }
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/transfer-request.v1.json",
+ "title": "BillingTransferRequest",
+ "description": "billing.transfer-request@1: bodies of billing.transfer.create on OpenVibe.Billing. POST /api/v1/transfers: { from, to, amount, kind?, target?, message? } moves amount bits from the sender's credit to the recipient's payable (a tip, donation or paid interaction; never to oneself). POST /api/v1/transfers/:id/refund: { amount?, reason? } gives a credit-funded donation back (all that remains refundable when amount is absent). Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "from",
+ "to",
+ "amount"
+ ],
+ "properties": {
+ "from": {
+ "description": "The sender (their credit pays).",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "to": {
+ "description": "The recipient (their payable receives).",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits."
+ },
+ "kind": {
+ "enum": [
+ "tip",
+ "donation",
+ "paid_interaction"
+ ],
+ "description": "Default donation."
+ },
+ "target": {
+ "$ref": "../common/entity-ref.v1.json",
+ "description": "What the money is for (a stream, a paid message, …)."
+ },
+ "message": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500
+ }
+ }
+ },
+ {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits to give back; default all that remains refundable."
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/transfer-result.v1.json",
+ "title": "BillingTransferResult",
+ "description": "billing.transfer-result@1: answers of billing.transfer.create on OpenVibe.Billing (201). POST /api/v1/transfers → { transaction, balance: { credit } } (the sender's credit after it); POST /api/v1/transfers/:id/refund → { transaction } (type refund, reverses_txn the donation).",
+ "type": "object",
+ "required": [
+ "transaction"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "balance": {
+ "type": "object",
+ "required": [
+ "credit"
+ ],
+ "properties": {
+ "credit": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+}
@@ -817,6 +817,177 @@
@@ -817,6 +817,177 @@
"status": "active",
"status": "active",
"adr": "ADR-012"
"adr": "ADR-012"
},
},
+ {
+ "id": "billing.admin-request",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/admin-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.admin-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/admin-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.balance-read-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/balance-read-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.balance",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/balance.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.cashout-decision",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/cashout-decision.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.cashout-request",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/cashout-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.cashout-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/cashout-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.cashout",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/cashout.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.entitlement-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/entitlement-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.entitlement",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/entitlement.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.intent-request",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/intent-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.intent-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/intent-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.intent",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/intent.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.subscription-request",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/subscription-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.subscription-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/subscription-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.subscription",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/subscription.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.transaction",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/transaction.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.transfer-request",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/transfer-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "billing.transfer-result",
+ "version": "1.0.0",
+ "owner": "billing",
+ "schema": "billing/transfer-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "deals.watch.matched",
"id": "deals.watch.matched",
"version": "1.0.0",
"version": "1.0.0",
@@ -1609,6 +1780,24 @@
@@ -1609,6 +1780,24 @@
"status": "active",
"status": "active",
"adr": "ADR-022"
"adr": "ADR-022"
},
},
+ {
+ "id": "chat.send-request",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/send-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.send-result",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/send-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "live.moderation.action",
"id": "live.moderation.action",
"version": "1.0.0",
"version": "1.0.0",
@@ -1628,6 +1817,96 @@
@@ -1628,6 +1817,96 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active",
"status": "active",
"adr": "ADR-022"
"adr": "ADR-022"
+ },
+ {
+ "id": "community.paste-create-request",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/paste-create-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.paste-moderate-request",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/paste-moderate-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.paste-moderate-result",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/paste-moderate-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.paste-write-request",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/paste-write-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.paste-write-result",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/paste-write-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.paste",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/paste.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.moderation-request",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/moderation-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.moderation-result",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/moderation-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.post-write-request",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/post-write-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "community.pulse-write-result",
+ "version": "1.0.0",
+ "owner": "community",
+ "schema": "community/pulse-write-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
}
}
]
]
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
+ "title": "ChatSendRequest",
+ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "message"
+ ],
+ "properties": {
+ "type": {
+ "const": "chat"
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6000,
+ "description": "The text (trimmed by Chat)."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."
+ },
+ "auto_delete_minutes": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."
+ },
+ "voiceChannelId": {
+ "type": "string",
+ "description": "The voice channel the sender is in, echoed on the broadcast."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ops"
+ ],
+ "properties": {
+ "boot": {
+ "type": "string",
+ "maxLength": 64,
+ "description": "Live's boot id; placeholder message ids are mapped per boot."
+ },
+ "ops": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "op"
+ ],
+ "properties": {
+ "seq": {
+ "type": "integer",
+ "description": "Echoed on the result."
+ },
+ "op": {
+ "type": "string",
+ "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."
+ },
+ "args": {
+ "type": "array"
+ },
+ "ref": {
+ "type": "number",
+ "description": "Live's placeholder id for the row this op inserts."
+ },
+ "key": {
+ "type": "string",
+ "maxLength": 80,
+ "description": "Idempotency key: applied once."
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,200 @@
@@ -0,0 +1,200 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
+ "title": "ChatSendResult",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "username",
+ "role",
+ "message",
+ "stream_id",
+ "channel_user_id",
+ "is_global",
+ "timestamp"
+ ],
+ "properties": {
+ "type": {
+ "const": "chat"
+ },
+ "id": {
+ "type": "integer",
+ "description": "The stored message id."
+ },
+ "username": {
+ "type": "string",
+ "description": "Display name, or the anonymous id."
+ },
+ "core_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The immutable login handle; null for an anonymous sender."
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "role": {
+ "type": "string",
+ "description": "The sender's role, anon for an anonymous sender."
+ },
+ "message": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "is_global": {
+ "type": "boolean",
+ "description": "true for homepage global chat (no stream, no channel)."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": "string"
+ },
+ "filtered": {
+ "type": "boolean"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "auto_delete_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "voiceChannelId": {
+ "type": "string"
+ },
+ "reply_to": {
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "message"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string",
+ "description": "The parent's text, cut to 100 characters."
+ }
+ }
+ },
+ "nameFX": {
+ "description": "Cosmetic name effect."
+ },
+ "particleFX": {
+ "description": "Cosmetic particle effect."
+ },
+ "hatFX": {
+ "description": "Cosmetic hat."
+ },
+ "voiceFX": {
+ "description": "Cosmetic TTS voice effect."
+ },
+ "tag": {
+ "description": "The sender's equipped tag."
+ },
+ "vip_badge": {
+ "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "message"
+ ],
+ "properties": {
+ "type": {
+ "enum": [
+ "system",
+ "error"
+ ]
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "results"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "properties": {
+ "seq": {
+ "type": "integer"
+ },
+ "ok": {
+ "type": "boolean"
+ },
+ "result": {},
+ "code": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",
+ "title": "CommunityModerationRequest",
+ "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": ["visibility"],
+ "properties": { "visibility": { "enum": ["public", "hidden", "locked"] } }
+ },
+ {
+ "type": "object",
+ "anyOf": [
+ { "required": ["pinned"] },
+ { "required": ["locked"] }
+ ],
+ "properties": {
+ "pinned": { "description": "Truthy pins the thread." },
+ "locked": { "description": "Truthy locks it (announced as thread.locked / thread.unlocked)." }
+ }
+ },
+ {
+ "type": "object",
+ "anyOf": [
+ { "required": ["body"] },
+ { "required": ["body_markdown"] }
+ ],
+ "properties": {
+ "body": { "type": "string", "minLength": 1, "maxLength": 40000 },
+ "body_markdown": { "type": "string", "minLength": 1, "maxLength": 40000 }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["space", "webhook_url_ref"],
+ "properties": {
+ "space": { "type": "string", "description": "The space's slug." },
+ "webhook_url_ref": { "type": "string", "pattern": "^[A-Z0-9_]+$" },
+ "enabled": { "type": "boolean", "description": "Default true." }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["enabled"],
+ "properties": { "enabled": { "type": "boolean" } }
+ },
+ { "$ref": "../common/no-body.v1.json" }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",
+ "title": "CommunityModerationResult",
+ "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": ["thread"],
+ "properties": {
+ "thread": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": ["integer", "string"] },
+ "access_id": { "type": "string", "description": "A comment thread's browser id (cth_…)." },
+ "ref": { "$ref": "../common/entity-ref.v1.json" },
+ "visibility": { "type": "string" },
+ "comment_count": { "type": ["integer", "null"] },
+ "space": { "type": "string" },
+ "slug": { "type": "string" },
+ "title": { "type": "string" },
+ "pinned": { "type": "boolean" },
+ "locked": { "type": "boolean" }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["post"],
+ "properties": {
+ "post": {
+ "type": "object",
+ "required": ["id", "thread_id", "revision", "deleted"],
+ "properties": {
+ "id": { "type": "integer" },
+ "thread_id": { "type": "integer" },
+ "is_opening": { "type": "boolean" },
+ "origin": { "type": "string" },
+ "author": { "type": ["object", "null"] },
+ "body_markdown": { "type": ["string", "null"] },
+ "body_html": { "type": ["string", "null"] },
+ "revision": { "type": "integer" },
+ "deleted": { "type": "boolean" },
+ "created_at": { "type": ["string", "null"] },
+ "updated_at": { "type": ["string", "null"] },
+ "can_edit": { "type": "boolean" },
+ "attachments": { "type": "array" },
+ "pastes": { "type": "array" }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["ok"],
+ "properties": {
+ "ok": { "const": true },
+ "id": { "type": "integer" },
+ "deleted": { "const": "thread" }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["enabled"],
+ "anyOf": [
+ { "required": ["deliveries"] },
+ { "required": ["mappings"] }
+ ],
+ "properties": {
+ "enabled": { "type": "boolean" },
+ "deliveries": { "type": "array", "items": { "type": "object" } },
+ "mappings": { "type": "array", "items": { "type": "object" } }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["mapping"],
+ "properties": {
+ "mapping": {
+ "type": "object",
+ "required": ["id", "space", "webhook_url_ref", "webhook_configured", "enabled"],
+ "properties": {
+ "id": { "type": "integer" },
+ "space": { "type": "string" },
+ "direction": { "type": "string" },
+ "webhook_url_ref": { "type": "string" },
+ "webhook_configured": { "type": "boolean" },
+ "enabled": { "type": "boolean" },
+ "created_at": { "type": ["string", "null"] }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",
+ "title": "CommunityPasteCreateRequest",
+ "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.",
+ "type": "object",
+ "anyOf": [
+ { "required": ["content"] },
+ { "required": ["screenshot"] }
+ ],
+ "properties": {
+ "content": { "type": "string", "minLength": 1, "description": "The text (trimmed)." },
+ "screenshot": { "type": "string", "contentEncoding": "binary", "description": "Multipart only: the image part." },
+ "title": { "type": ["string", "null"], "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank." },
+ "language": { "type": "string", "description": "A language hint; auto or absent sniffs it from the content." },
+ "visibility": { "type": "string", "description": "public (default), unlisted or private." },
+ "burn_after_read": { "type": ["boolean", "integer", "string"], "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else." },
+ "is_nsfw": { "type": ["boolean", "integer", "string"] },
+ "stream_id": { "type": ["integer", "string"], "description": "The Live stream it was made during (digits)." },
+ "description": { "type": "string", "description": "Multipart screenshot: the text shown under the image." },
+ "page_url": { "type": "string", "description": "Multipart screenshot: the page it shows." },
+ "user_agent": { "type": "string", "description": "Multipart screenshot: the capturing browser." },
+ "slug": { "type": "string", "pattern": "^[A-Za-z0-9_-]{3,80}$", "description": "Service callers, public pastes only: the slug to use (409 if taken)." },
+ "metadata": { "type": ["object", "string"], "description": "Service callers only: a JSON object (at most 16 KB)." },
+ "ai_summary": { "type": "string", "description": "Service callers only (cut to 2000 characters)." },
+ "ai_tags": { "description": "Service callers only, with ai_summary: JSON text or a value to serialise." }
+ }
+}
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/community/paste-moderate-request.v1.json",
+ "title": "CommunityPasteModerateRequest",
+ "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "slugs",
+ "action"
+ ],
+ "properties": {
+ "slugs": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string"
+ }
+ },
+ "action": {
+ "enum": [
+ "delete",
+ "public",
+ "unlisted",
+ "private"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "screenshot"
+ ],
+ "properties": {
+ "screenshot": {
+ "type": "string",
+ "contentEncoding": "binary",
+ "description": "The multipart part named screenshot: the censored image."
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",
+ "title": "CommunityPasteModerateResult",
+ "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",
+ "anyOf": [
+ { "type": "object", "required": ["done", "skipped"], "properties": { "done": { "type": "integer", "minimum": 0 }, "skipped": { "type": "integer", "minimum": 0 } } },
+ { "type": "object", "required": ["paste"], "properties": { "paste": { "$ref": "paste.v1.json" } } },
+ { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",
+ "title": "CommunityPasteWriteRequest",
+ "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",
+ "anyOf": [
+ {
+ "type": "object",
+ "minProperties": 1,
+ "properties": {
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "content": {
+ "type": "string"
+ },
+ "language": {
+ "type": "string"
+ },
+ "visibility": {
+ "type": "string",
+ "description": "public, unlisted or private (anything else is public)."
+ },
+ "is_nsfw": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ]
+ },
+ "pinned": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ],
+ "description": "Staff only; ignored otherwise."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000
+ },
+ "parent_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The top-level comment this replies to."
+ },
+ "anon_name": {
+ "type": "string",
+ "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
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/community/paste-write-result.v1.json",
+ "title": "CommunityPasteWriteResult",
+ "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": ["paste"],
+ "properties": {
+ "id": { "type": "integer" },
+ "slug": { "type": "string" },
+ "url": { "type": "string" },
+ "paste": { "$ref": "paste.v1.json" }
+ }
+ },
+ { "type": "object", "required": ["success"], "properties": { "success": { "const": true } } },
+ { "type": "object", "required": ["liked", "likes"], "properties": { "liked": { "type": "boolean" }, "likes": { "type": "integer", "minimum": 0 } } },
+ { "type": "object", "required": ["copies"], "properties": { "copies": { "type": "integer", "minimum": 0 } } },
+ {
+ "type": "object",
+ "required": ["comment"],
+ "properties": {
+ "comment": {
+ "type": "object",
+ "required": ["id", "paste_id", "message", "created_at"],
+ "properties": {
+ "id": { "type": "integer" },
+ "paste_id": { "type": "integer" },
+ "user_id": { "type": ["string", "null"], "description": "The author's subject; null for an anonymous comment." },
+ "author_subject": { "type": ["string", "null"] },
+ "parent_id": { "type": ["integer", "null"] },
+ "anon_name": { "type": ["string", "null"] },
+ "message": { "type": "string" },
+ "is_deleted": { "type": ["integer", "boolean"] },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": ["string", "null"] },
+ "username": { "type": ["string", "null"] },
+ "display_name": { "type": ["string", "null"] },
+ "avatar_url": { "type": ["string", "null"] },
+ "profile_color": { "type": ["string", "null"] }
+ }
+ }
+ }
+ },
+ { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/paste.v1.json",
+ "title": "CommunityPaste",
+ "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).",
+ "type": "object",
+ "required": ["id", "app_id", "slug", "user_id", "owner_subject", "origin", "type", "title", "visibility", "url", "raw_url", "created_at", "is_owner"],
+ "properties": {
+ "id": { "type": "integer" },
+ "app_id": { "const": "community" },
+ "slug": { "type": "string" },
+ "user_id": { "type": ["string", "null"], "description": "The owner's subject (usr_…), or null for an anonymous paste." },
+ "owner_subject": { "type": ["string", "null"] },
+ "origin": { "type": "string", "description": "user, or ai for a paste an AI service wrote." },
+ "type": { "enum": ["paste", "screenshot"] },
+ "title": { "type": "string" },
+ "content": { "type": ["string", "null"] },
+ "language": { "type": ["string", "null"] },
+ "visibility": { "enum": ["public", "unlisted", "private"] },
+ "stream_id": { "type": ["integer", "null"], "description": "The Live stream it was made during, when its stream_ref is one." },
+ "stream_ref": { "type": ["object", "null"], "description": "An EntityRef of where it was made (a Live stream, or a service's own source)." },
+ "screenshot_url": { "type": ["string", "null"] },
+ "metadata": { "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details." },
+ "burn_after_read": { "type": "boolean" },
+ "forked_from": { "type": ["integer", "null"] },
+ "pinned": { "type": "boolean" },
+ "views": { "type": "integer", "minimum": 0 },
+ "unique_views": { "type": "integer", "minimum": 0 },
+ "copies": { "type": "integer", "minimum": 0 },
+ "likes": { "type": "integer", "minimum": 0 },
+ "is_nsfw": { "type": "boolean" },
+ "ai_summary": { "type": ["string", "null"] },
+ "ai_tags": { "type": ["string", "null"], "description": "JSON text as the AI pass wrote it." },
+ "ai_analyzed_at": { "type": ["string", "null"] },
+ "revision": { "type": "integer" },
+ "url": { "type": "string", "description": "/p/<slug>" },
+ "raw_url": { "type": "string", "description": "/p/<slug>/raw" },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": ["string", "null"] },
+ "is_owner": { "type": "boolean", "description": "Whether the caller owns it." },
+ "username": { "type": ["string", "null"] },
+ "display_name": { "type": ["string", "null"], "description": "OpenVibe AI for an AI paste." },
+ "avatar_url": { "type": ["string", "null"] },
+ "profile_color": { "type": ["string", "null"] }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",
+ "title": "CommunityPostWriteRequest",
+ "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "anyOf": [
+ { "required": ["body"] },
+ { "required": ["body_markdown"] }
+ ],
+ "properties": {
+ "title": { "type": "string", "description": "A new thread's title." },
+ "body": { "type": "string", "minLength": 1, "maxLength": 40000, "description": "Markdown." },
+ "body_markdown": { "type": "string", "minLength": 1, "maxLength": 40000, "description": "Read when body is absent." },
+ "members_only": {
+ "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",
+ "anyOf": [
+ { "type": ["boolean", "null"] },
+ { "type": "string", "pattern": "^usr_" },
+ { "type": "object", "required": ["owner"], "properties": { "owner": { "type": "string", "pattern": "^usr_" } } }
+ ]
+ },
+ "category": { "type": "string", "description": "A new thread only: the slug of one of the space's categories." },
+ "attachments": {
+ "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",
+ "anyOf": [
+ { "type": "string", "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$" },
+ { "type": "array", "maxItems": 4, "items": { "type": "string", "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$" } }
+ ]
+ },
+ "pastes": {
+ "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",
+ "type": ["array", "string"],
+ "items": { "type": "string" }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["value"],
+ "properties": {
+ "value": { "enum": [1, -1, 0, "1", "-1", "0"], "description": "A thread vote; 0 takes the vote back." }
+ }
+ },
+ { "$ref": "../common/no-body.v1.json" }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",
+ "title": "CommunityPulseWriteResult",
+ "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": ["item", "created"],
+ "properties": {
+ "created": { "type": "boolean" },
+ "item": {
+ "type": "object",
+ "required": ["id", "source", "title", "url", "origin", "label", "actor", "occurred_at"],
+ "properties": {
+ "id": { "type": "integer" },
+ "source": {
+ "type": "object",
+ "required": ["service", "type", "id"],
+ "properties": {
+ "service": { "type": "string" },
+ "type": { "type": "string" },
+ "id": { "type": "string" }
+ }
+ },
+ "title": { "type": "string", "maxLength": 300 },
+ "url": { "type": "string" },
+ "origin": { "enum": ["user", "ai", "system"] },
+ "label": { "enum": ["AI", "System", null] },
+ "actor": {
+ "type": ["object", "null"],
+ "properties": {
+ "subject": { "type": ["string", "null"] },
+ "username": { "type": ["string", "null"] },
+ "display_name": { "type": ["string", "null"] },
+ "avatar_url": { "type": ["string", "null"] },
+ "profile_color": { "type": ["string", "null"] }
+ }
+ },
+ "occurred_at": { "type": ["string", "null"] }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["removed"],
+ "properties": { "removed": { "type": "integer", "minimum": 0, "maximum": 1 } }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "on": "yes" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": { "kind": "wallet", "currency": "vibes-bits" }, "to": { "kind": "user_credit", "currency": "vibes-bits" }, "amount": 1, "reason": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": { "kind": "platform_revenue", "owner": null, "currency": "vibes-bits" }, "to": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": 250, "reason": "goodwill after the outage" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "on": true, "reason": "provider incident" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "renewed": "all" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "renewed": ["sub_01JAB2C3D4E5F6G7H8J9K0MNPW"], "expired": [], "canceled": [], "skipped": 0 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "frozen": false, "reason": null, "frozen_at": null, "frozen_by": null, "processed_after_unfreeze": { "processed": 2 } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "transactions": [ { "id": "txn_1" } ], "next_cursor": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "vibes-bits", "credit": 1200, "payable": 5500, "pending_payouts": 0, "payable_value_cents": 5500, "bits_per_usd": 100 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "transactions": [ { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] } ], "next_cursor": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "usd-cents", "credit": 1, "payable": 0, "pending_payouts": 0, "payable_value_cents": 0, "bits_per_usd": 100 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "vibes-bits", "credit": 1200, "payable": 5500, "pending_payouts": 0, "payable_value_cents": 5500, "bits_per_usd": 100 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "payout_reference": "" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "payout_reference": "PAYID-M5A7", "payout_provider": "paypal" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "reason": "address bounced" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": 0 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": 5000, "payout_method": { "type": "paypal", "address": "[email protected]" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": "1500" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "cashouts": [ { "id": "co_1" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "cashout": { "id": "co_01JAB2C3D4E5F6G7H8J9K0MNPT", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "amount_bits": 5000, "value_cents": 5000, "status": "requested", "payout_method": { "type": "paypal", "address": "[email protected]" }, "escrow_until": "2026-10-02T18:00:00.000Z", "request_txn": "txn_01JAB2C3D4E5F6G7H8J9K0MNPV", "settle_txn": null, "payout_provider": null, "payout_reference": null, "reason": null, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "transaction": { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] }, "balance": { "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "currency": "vibes-bits", "credit": 1200, "payable": 5500, "pending_payouts": 0, "payable_value_cents": 5500, "bits_per_usd": 100 } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "co_1", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "amount_bits": 0, "value_cents": 0, "status": "requested", "payout_method": null, "escrow_until": "x", "request_txn": "t", "created_at": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "co_01JAB2C3D4E5F6G7H8J9K0MNPT", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "amount_bits": 5000, "value_cents": 5000, "status": "requested", "payout_method": { "type": "paypal", "address": "[email protected]" }, "escrow_until": "2026-10-02T18:00:00.000Z", "request_txn": "txn_01JAB2C3D4E5F6G7H8J9K0MNPV", "settle_txn": null, "payout_provider": null, "payout_reference": null, "reason": null, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "entitlements": [] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "entitlements": [ { "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": "yes", "expires_at": null, "subscription": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "provider": "stripe", "kind": "gift", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "provider": "powerchat", "kind": "subscription", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "route": "direct", "receiving_account": "alice" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "provider": "stripe", "kind": "purchase", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "bits": 1000, "success_url": "https://openvibe.live/wallet?ok=1" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "checkout_url": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "intent": { "id": "pi_01JAB2C3D4E5F6G7H8J9K0MNPX", "provider": "stripe", "provider_ref": "cs_test_a1", "kind": "purchase", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": null, "amount_cents": 999, "fee_cents": 0, "bits": 1000, "route": null, "auto_renew": 0, "status": "created", "settled_txn": null, "created_at": "2026-09-25T18:00:00.000Z" }, "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_a1" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "pi_01JAB2C3D4E5F6G7H8J9K0MNPX", "provider": "stripe", "provider_ref": "cs_test_a1", "kind": "donation", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": null, "amount_cents": 999, "fee_cents": 0, "bits": 1000, "route": null, "auto_renew": 0, "status": "created", "settled_txn": null, "created_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "pi_01JAB2C3D4E5F6G7H8J9K0MNPX", "provider": "stripe", "provider_ref": "cs_test_a1", "kind": "purchase", "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": null, "amount_cents": 999, "fee_cents": 0, "bits": 1000, "route": null, "auto_renew": 0, "status": "created", "settled_txn": null, "created_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subscriber": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": "gift" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subscriber": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": "credit", "auto_renew": true }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subscription": { "id": "sub_1" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "subscriber": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "tier": 1, "provider": "credit", "provider_ref": null, "route": null, "status": "active", "auto_renew": true, "cancel_at_period_end": false, "price_cents": 499, "current_period_end": "2026-10-25T18:00:00.000Z", "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }, "provider_sync": "not_needed" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "subscriber": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "tier": 1, "provider": "credit", "provider_ref": null, "route": null, "status": "active", "auto_renew": true, "cancel_at_period_end": false, "price_cents": 499, "current_period_end": "2026-10-25T18:00:00.000Z", "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }, "entitlement": { "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "kind": "channel_subscription", "active": true, "expires_at": "2026-10-25T18:00:00.000Z", "subscription": { "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "status": "active", "auto_renew": 1, "cancel_at_period_end": 0, "provider": "credit" } }, "transaction": { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "sub_1", "subscriber": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "provider": "credit", "status": "active", "auto_renew": true, "cancel_at_period_end": false, "current_period_end": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "sub_01JAB2C3D4E5F6G7H8J9K0MNPW", "subscriber": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "streamer": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "tier": 1, "provider": "credit", "provider_ref": null, "route": null, "status": "active", "auto_renew": true, "cancel_at_period_end": false, "price_cents": 499, "current_period_end": "2026-10-25T18:00:00.000Z", "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "txn_1", "type": "gift", "status": "posted", "test": false, "reverses_txn": null, "from_subject": null, "to_subject": null, "provider": null, "receipt_ref": null, "created_at": "x", "entries": [] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": "gst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": "0", "kind": "bribe" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": -5 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "amount": 200, "reason": "sent twice" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "to": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "amount": 500, "kind": "tip", "target": { "service": "live", "type": "stream", "id": "812" }, "message": "gg" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "balance": { "credit": 700 } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "transaction": { "id": "txn_01JAB2C3D4E5F6G7H8J9K0MNPS", "type": "donation", "status": "posted", "test": false, "reverses_txn": null, "from_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "to_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "provider": null, "receipt_ref": null, "actor": { "type": "service", "id": "live" }, "metadata": { "kind": "tip", "amount_bits": 500, "message": "gg" }, "created_at": "2026-09-25T18:00:00.000Z", "entries": [ { "account": { "kind": "user_credit", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "currency": "vibes-bits" }, "amount": -500 }, { "account": { "kind": "creator_payable", "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "currency": "vibes-bits" }, "amount": 500 } ] }, "balance": { "credit": 700 } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "chat", "message": "" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "chat", "text": "hello" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "boot": "b-1727300000000", "ops": [{ "seq": 1, "op": "broadcastToStream", "args": [42, { "type": "chat", "username": "AI Viewer", "message": "hello chat" }] }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "chat", "message": "gg that was clean", "reply_to_id": 81234, "auto_delete_minutes": 30 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "chat", "username": "Alice", "message": "hi" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "notice", "message": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "ok": true, "results": [{ "seq": 1, "ok": true, "result": null }, { "seq": 2, "ok": false, "code": "capability.denied", "error": "chat.message.send not granted" }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "chat", "id": 81240, "username": "Alice", "core_username": "alice", "user_id": 17, "anon_id": null, "role": "user", "message": "gg that was clean", "stream_id": 42, "channel_user_id": 5, "is_global": false, "avatar_url": "https://openvibe.media/avatar/alice", "profile_color": "#3b82f6", "filtered": false, "timestamp": "2026-09-25T20:14:03.120Z", "auto_delete_at": null, "reply_to": { "id": 81234, "username": "bob", "user_id": 9, "message": "did you see that" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "type": "system", "message": "Slow down! You are sending messages too fast." }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "visibility": "deleted" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "space": "announcements", "webhook_url_ref": "https://discord.com/api/webhooks/1/abc" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "visibility": "locked" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "pinned": true }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "space": "announcements", "webhook_url_ref": "DISCORD_WEBHOOK_ANNOUNCEMENTS" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "ok": false }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "thread": { "id": 311, "access_id": "cth_01JAB2C3D4E5F6G7H8J9K0MNPQ", "ref": { "service": "live", "type": "vod", "id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "visibility": "locked", "comment_count": 14, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "mapping": { "id": 3, "space": "announcements", "direction": "out", "webhook_url_ref": "DISCORD_WEBHOOK_ANNOUNCEMENTS", "webhook_configured": true, "enabled": true, "created_at": "2026-09-25T18:00:00.000Z" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "ok": true, "id": 77, "deleted": "thread" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "content": "", "title": "blank" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "title": "no content", "visibility": "public" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "screenshot": "<bytes>", "title": "Bug on the dashboard", "description": "the chart overflows", "page_url": "https://openvibe.live/dashboard" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "title": "ffmpeg restream flags", "content": "ffmpeg -re -i in.flv -c copy -f flv rtmp://example/live", "language": "auto", "visibility": "unlisted", "stream_id": "812" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "slugs": ["k3JfQ2"], "action": "archive" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "slugs": ["k3JfQ2", "a8bC9d"], "action": "unlisted" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "screenshot": "<bytes>" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "done": -1, "skipped": 0 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "done": 2, "skipped": 0 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "message": "Comment deleted" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "title": 42 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "message": "this saved me an hour", "parent_id": 91 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "title": "ffmpeg restream flags (fixed)", "visibility": "private" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "liked": "yes" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "comment": { "id": 93, "paste_id": 4412, "user_id": null, "author_subject": null, "parent_id": null, "anon_name": "Anonymous", "message": "nice", "is_deleted": 0, "created_at": "2026-09-25 18:05:00", "updated_at": null, "username": null, "display_name": null, "avatar_url": null, "profile_color": null } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "liked": true, "likes": 6 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "paste": { "unique_views": 12, "id": 4412, "app_id": "community", "slug": "k3JfQ2", "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "owner_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "origin": "user", "type": "paste", "title": "ffmpeg restream flags", "content": "ffmpeg -re -i in.flv -c copy -f flv rtmp://example/live", "language": "text", "visibility": "public", "stream_id": 812, "stream_ref": { "service": "live", "type": "stream", "id": "812" }, "screenshot_url": null, "metadata": null, "burn_after_read": false, "forked_from": null, "pinned": false, "views": 30, "copies": 2, "likes": 5, "is_nsfw": false, "ai_summary": null, "ai_tags": null, "ai_analyzed_at": null, "revision": 1, "url": "/p/k3JfQ2", "raw_url": "/p/k3JfQ2/raw", "created_at": "2026-09-25 18:02:11", "updated_at": "2026-09-25 18:02:11", "is_owner": true, "username": "alice", "display_name": "Alice", "avatar_url": "https://openvibe.media/avatar/alice", "profile_color": "#3b82f6" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": 4412, "app_id": "media", "slug": "k3JfQ2", "user_id": null, "owner_subject": null, "origin": "user", "type": "paste", "title": "x", "visibility": "public", "url": "/p/k3JfQ2", "raw_url": "/p/k3JfQ2/raw", "created_at": "2026-09-25 18:02:11", "is_owner": false }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "unique_views": 12, "id": 4412, "app_id": "community", "slug": "k3JfQ2", "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "owner_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "origin": "user", "type": "paste", "title": "ffmpeg restream flags", "content": "ffmpeg -re -i in.flv -c copy -f flv rtmp://example/live", "language": "text", "visibility": "public", "stream_id": 812, "stream_ref": { "service": "live", "type": "stream", "id": "812" }, "screenshot_url": null, "metadata": null, "burn_after_read": false, "forked_from": null, "pinned": false, "views": 30, "copies": 2, "likes": 5, "is_nsfw": false, "ai_summary": null, "ai_tags": null, "ai_analyzed_at": null, "revision": 1, "url": "/p/k3JfQ2", "raw_url": "/p/k3JfQ2/raw", "created_at": "2026-09-25 18:02:11", "updated_at": "2026-09-25 18:02:11", "is_owner": true, "username": "alice", "display_name": "Alice", "avatar_url": "https://openvibe.media/avatar/alice", "profile_color": "#3b82f6" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "body": "x", "attachments": ["med_1", "med_2"] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "value": 2 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "title": "Restreaming to three platforms at once", "body": "What bitrates do you use?", "category": "help", "pastes": ["k3JfQ2"] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "body_markdown": "6000 kbps works for me", "attachments": ["med_01JAB2C3D4E5F6G7H8J9K0MNPQ"] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "value": -1 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "created": true, "item": { "id": 1, "title": "x", "url": "https://x", "origin": "robot" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "created": true, "item": { "id": 9001, "source": { "service": "live", "type": "clip", "id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "title": "Alice clipped a clean 1v3", "url": "https://openvibe.live/clip/med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "origin": "user", "label": null, "actor": { "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "username": "alice", "display_name": "Alice", "avatar_url": null, "profile_color": null }, "occurred_at": "2026-09-25T18:00:00.000Z" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "removed": 1 }
@@ -8338,1907 +8338,3573 @@
@@ -8338,1907 +8338,3573 @@
}
}
}
}
},
},
- "deals.watch.matched": {
+ "billing.admin-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.watch.matched.v1.json",
- "title": "DealsWatchMatchedPayload",
- "description": "deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.",
- "type": "object",
- "required": [
- "watch_id",
- "recipient",
- "kind",
- "query",
- "product_id",
- "max_price",
- "currency",
- "offer_id",
- "offer_url",
- "title",
- "observation"
- ],
- "additionalProperties": false,
- "properties": {
- "watch_id": {
- "type": "string",
- "pattern": "^dwt_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "recipient": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The watch owner's user subject id: who is notified."
- },
- "kind": {
- "enum": [
- "keyword",
- "product",
- "price_below"
- ]
- },
- "query": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "Keywords of a keyword watch, or of a price-below watch without a product."
- },
- "product_id": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 64,
- "description": "Deals product id of a product or price-below watch."
- },
- "max_price": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 40,
- "description": "Price-below threshold as entered (decimal text, e.g. 19.99)."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217 currency of max_price."
- },
- "offer_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "description": "The canonical (root) offer id (dof_…)."
- },
- "offer_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical offer page URL on Deals."
- },
- "title": {
- "type": "string",
- "minLength": 1,
- "maxLength": 500
- },
- "observation": {
+ "$id": "https://openvibe.network/contracts/billing/admin-request.v1.json",
+ "title": "BillingAdminRequest",
+ "description": "billing.admin-request@1: bodies of billing.ledger.admin on OpenVibe.Billing (operators). POST /api/v1/admin/freeze: { on, reason? } (unfreezing processes the provider deliveries stored meanwhile; allowed while frozen). POST /api/v1/admin/adjustments: { from, to, amount, reason, relates_to? } moves value between two accounts of one currency. POST /api/v1/purchases/settle: { provider, provider_ref, subject?, amount_cents, bits, intent_id?, test? } books a provider receipt. POST /api/v1/admin/provider-events/:id/reprocess, POST /api/v1/admin/sweep and the GETs (freeze, reconcile, provider-events, import-holds) take no body. Adjustments and settlements need an Idempotency-Key and are refused while frozen.",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "observed_at",
- "price",
- "currency",
- "availability"
+ "on"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "observed_at": {
- "type": "string",
- "format": "date-time"
- },
- "price": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 40,
- "description": "Decimal text as stated; null when the observation stated no price."
+ "on": {
+ "type": "boolean"
},
},
- "currency": {
+ "reason": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "pattern": "^[A-Z]{3}$"
- },
- "availability": {
- "enum": [
- "in_stock",
- "out_of_stock",
- "preorder",
- "discontinued",
- "limited",
- "sold_out",
- "online_only",
- "in_store_only",
- null
]
]
}
}
}
}
- }
- }
- }
- },
- "trade.alert.triggered": {
- "version": "1.0.0",
- "owner": "trade",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.alert.triggered.v1.json",
- "title": "TradeAlertTriggeredPayload",
- "description": "trade.alert.triggered v1 (OpenVibe.Trade server/domain/alerts.js deliver). One delivery of one private alert rule: a threshold crossed by a new observation (trigger kind observation), or a newly seen document of the instrument (filing_type / new_document rules, trigger kind document). Written in the transaction that recorded the observation or document; at most one per (rule, trigger). Envelope: subject { type: user, id: <rule owner usr_…> }, visibility subject, priority important, actor service:trade. Data only, never rendered notification text (ADR-020): OpenVibe.Network renders a TRADE_ALERT notification (server/notifications/events-consumer.js). Information only, not investment advice.",
- "type": "object",
- "required": [
- "delivery_id",
- "rule",
- "instrument",
- "trigger",
- "disclaimer"
- ],
- "additionalProperties": false,
- "properties": {
- "delivery_id": {
- "type": "string",
- "pattern": "^ald_[0-9A-HJKMNP-TV-Z]{26}$"
},
},
- "rule": {
+ {
"type": "object",
"type": "object",
- "additionalProperties": false,
"required": [
"required": [
- "id",
- "kind",
- "status",
- "instrument",
- "metric",
- "operator",
- "threshold",
- "unit",
- "currency",
- "form_types",
- "armed",
- "created_at",
- "updated_at"
+ "from",
+ "to",
+ "amount",
+ "reason"
],
],
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^alr_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "kind": {
- "enum": [
- "threshold",
- "filing_type",
- "new_document"
- ]
- },
- "status": {
- "enum": [
- "active",
- "deleted"
- ]
- },
- "instrument": {
+ "from": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id"
+ "kind",
+ "currency"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ "kind": {
+ "enum": [
+ "user_credit",
+ "creator_payable",
+ "provider_clearing",
+ "platform_revenue",
+ "payouts_pending",
+ "refunds",
+ "import_adjustment",
+ "chargeback_loss",
+ "fx_conversion"
+ ]
},
},
- "symbol": {
- "type": "string"
+ "owner": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "name": {
- "type": "string"
+ "currency": {
+ "enum": [
+ "vibes-bits",
+ "usd-cents"
+ ]
}
}
}
}
},
},
- "metric": {
- "type": [
- "string",
- "null"
+ "to": {
+ "type": "object",
+ "required": [
+ "kind",
+ "currency"
],
],
- "maxLength": 80,
- "description": "Threshold rules: e.g. price.close, volume, us-gaap:Revenues."
- },
- "operator": {
- "enum": [
- "above",
- "below",
- null
- ]
+ "properties": {
+ "kind": {
+ "enum": [
+ "user_credit",
+ "creator_payable",
+ "provider_clearing",
+ "platform_revenue",
+ "payouts_pending",
+ "refunds",
+ "import_adjustment",
+ "chargeback_loss",
+ "fx_conversion"
+ ]
+ },
+ "owner": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "currency": {
+ "enum": [
+ "vibes-bits",
+ "usd-cents"
+ ]
+ }
+ }
},
},
- "threshold": {
+ "amount": {
"type": [
"type": [
- "string",
- "null"
+ "integer",
+ "string"
],
],
- "maxLength": 64,
- "description": "Decimal text as entered."
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "In the accounts' currency (at most 1,000,000,000)."
},
},
- "unit": {
+ "reason": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "relates_to": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "maxLength": 20
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "provider",
+ "provider_ref"
+ ],
+ "properties": {
+ "provider": {
+ "type": "string"
},
},
- "currency": {
+ "provider_ref": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The provider's payment id."
+ },
+ "subject": {
+ "description": "Who the Vibes are for.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount_cents": {
"type": [
"type": [
- "string",
- "null"
+ "integer",
+ "string"
],
],
- "pattern": "^[A-Z]{3}$"
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Cents paid."
},
},
- "form_types": {
+ "bits": {
"type": [
"type": [
- "array",
- "null"
+ "integer",
+ "string"
],
],
- "items": {
- "type": "string",
- "maxLength": 20
- },
- "description": "filing_type rules: the 1–10 form types that trigger it (10-K, 8-K…)."
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Vibes to credit."
},
},
- "armed": {
+ "intent_id": {
+ "type": "string"
+ },
+ "test": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "billing.admin-result": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/admin-result.v1.json",
+ "title": "BillingAdminResult",
+ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "frozen",
+ "reason",
+ "frozen_at",
+ "frozen_by"
+ ],
+ "properties": {
+ "frozen": {
+ "type": "boolean"
+ },
+ "reason": {
"type": [
"type": [
- "boolean",
+ "string",
"null"
"null"
- ],
- "description": "Threshold rules: armed before this delivery (always true when it fired); null for document rules."
+ ]
},
},
- "created_at": {
- "type": "string",
- "format": "date-time"
+ "frozen_at": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "updated_at": {
- "type": "string",
- "format": "date-time"
- }
+ "frozen_by": {},
+ "processed_after_unfreeze": {}
}
}
},
},
- "instrument": {
+ {
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "symbol",
- "name",
- "url"
+ "ok",
+ "checks"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
"id": {
"id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ "type": "string"
},
},
- "symbol": {
- "type": "string",
- "minLength": 1,
- "maxLength": 32
+ "ok": {
+ "type": "boolean"
},
},
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 300
+ "trigger": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Instrument page on Trade."
+ "started_at": {
+ "type": "string"
+ },
+ "finished_at": {
+ "type": "string"
+ },
+ "checks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "ok"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "ok": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "warnings": {
+ "type": "array"
+ },
+ "totals": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "transaction"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "duplicate_receipt": {
+ "type": "boolean"
}
}
}
}
},
},
- "trigger": {
- "oneOf": [
- {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "kind",
- "id",
- "metric",
- "value",
- "unit",
- "currency",
- "observed_at",
- "retrieved_at",
- "source_key",
- "source_url"
- ],
- "properties": {
- "kind": {
- "const": "observation"
- },
- "id": {
- "type": "string",
- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "metric": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80
- },
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "description": "The decimal exactly as the source stated it."
- },
- "unit": {
- "type": "string",
- "minLength": 1,
- "maxLength": 20
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$"
- },
- "observed_at": {
- "type": "string",
- "format": "date-time"
- },
- "retrieved_at": {
- "type": "string",
- "format": "date-time"
- },
- "source_key": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "OpenVibe.Sources source key."
- },
- "source_url": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 2048
+ {
+ "type": "object",
+ "required": [
+ "events"
+ ],
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "provider"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "provider": {
+ "type": "string"
+ },
+ "provider_event_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "processed_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "result": {},
+ "attempts": {
+ "type": "integer"
+ },
+ "last_error": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
}
}
}
}
- },
- {
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "event"
+ ],
+ "properties": {
+ "event": {
"type": "object",
"type": "object",
- "additionalProperties": false,
"required": [
"required": [
- "kind",
"id",
"id",
- "form_type",
- "title",
- "url",
- "published_at",
- "retrieved_at",
- "source_key"
- ],
- "properties": {
- "kind": {
- "const": "document"
- },
- "id": {
- "type": "string",
- "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "form_type": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 20
- },
- "title": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 1000
- },
- "url": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 2048
- },
- "published_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "The source's own date; null if it gave none."
- },
- "retrieved_at": {
- "type": "string",
- "format": "date-time"
- },
- "source_key": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128
- }
+ "provider"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "renewed",
+ "expired",
+ "canceled",
+ "skipped"
+ ],
+ "properties": {
+ "renewed": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "expired": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "canceled": {
+ "type": "array",
+ "items": {
+ "type": "string"
}
}
+ },
+ "skipped": {
+ "type": "integer"
}
}
- ]
+ }
},
},
- "disclaimer": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "Currently \"Information only — not investment advice; no trading here.\""
+ {
+ "type": "object",
+ "required": [
+ "holds"
+ ],
+ "properties": {
+ "holds": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "tools.job.created": {
+ "billing.balance-read-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "tools",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.created.v1.json",
- "title": "ToolsJobCreatedPayload",
- "description": "tools.job.created v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A job was accepted and queued (POST /api/v1/jobs, or a retry of a failed job, which carries retry_of). An Idempotency-Key replay creates no job and no event. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor the owner when it is a subject (user, service, app or mod), else service:tools.",
- "type": "object",
- "required": [
- "job_id",
- "service",
- "type",
- "type_version",
- "state",
- "owner",
- "attempts",
- "max_attempts",
- "created_at",
- "retry_of"
- ],
- "additionalProperties": false,
- "properties": {
- "job_id": {
- "type": "string",
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "service": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$",
- "description": "The Tools satellite that runs the job (img, audio, docs…)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type, e.g. img.process."
- },
- "type_version": {
- "type": "integer",
- "minimum": 1
- },
- "state": {
- "const": "queued"
+ "$id": "https://openvibe.network/contracts/billing/balance-read-result.v1.json",
+ "title": "BillingBalanceReadResult",
+ "description": "billing.balance-read-result@1: answers of billing.balance.read on OpenVibe.Billing. GET /api/v1/balances/:subject → billing.balance@1; GET /api/v1/transactions?subject=&limit=&cursor= → { transactions, next_cursor } (newest first, at most 200 a page; subject is required); GET /api/v1/transactions/:id → { transaction, reversed_by } (ids of the transactions that reverse it).",
+ "anyOf": [
+ {
+ "$ref": "balance.v1.json"
},
},
- "owner": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
+ {
+ "type": "object",
+ "required": [
+ "transactions",
+ "next_cursor"
+ ],
+ "properties": {
+ "transactions": {
+ "type": "array",
+ "maxItems": 200,
+ "items": {
+ "$ref": "transaction.v1.json"
+ }
},
},
- {
- "type": "null"
+ "next_cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
- ],
- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
- },
- "attempts": {
- "type": "integer",
- "minimum": 0
- },
- "max_attempts": {
- "type": "integer",
- "minimum": 1
- },
- "created_at": {
- "type": "string",
- "format": "date-time"
+ }
},
},
- "retry_of": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "transaction",
+ "reversed_by"
],
],
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "reversed_by": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "tools.job.started": {
+ "billing.balance": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "tools",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.started.v1.json",
- "title": "ToolsJobStartedPayload",
- "description": "tools.job.started v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A worker claimed the job and it is running (attempts counts this attempt). A job requeued after a restart starts again and is announced again. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:tools.",
+ "$id": "https://openvibe.network/contracts/billing/balance.v1.json",
+ "title": "BillingBalance",
+ "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.",
"type": "object",
"type": "object",
"required": [
"required": [
- "job_id",
- "service",
- "type",
- "type_version",
- "state",
- "owner",
- "attempts",
- "max_attempts",
- "created_at",
- "retry_of",
- "started_at"
+ "subject",
+ "currency",
+ "credit",
+ "payable",
+ "pending_payouts",
+ "payable_value_cents",
+ "bits_per_usd"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "job_id": {
- "type": "string",
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "service": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$",
- "description": "The Tools satellite that runs the job (img, audio, docs…)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type, e.g. img.process."
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
},
},
- "type_version": {
- "type": "integer",
- "minimum": 1
+ "currency": {
+ "const": "vibes-bits"
},
},
- "state": {
- "const": "running"
+ "credit": {
+ "type": "integer"
},
},
- "owner": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
- ],
- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
+ "payable": {
+ "type": "integer"
},
},
- "attempts": {
- "type": "integer",
- "minimum": 0
+ "pending_payouts": {
+ "type": "integer"
},
},
- "max_attempts": {
+ "payable_value_cents": {
"type": "integer",
"type": "integer",
- "minimum": 1
- },
- "created_at": {
- "type": "string",
- "format": "date-time"
+ "description": "What payable is worth in US cents at today's rate."
},
},
- "retry_of": {
- "type": [
- "string",
- "null"
+ "bits_per_usd": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "billing.cashout-decision": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/cashout-decision.v1.json",
+ "title": "BillingCashoutDecision",
+ "description": "billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "payout_reference"
],
],
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ "properties": {
+ "payout_reference": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The provider's payout id."
+ },
+ "payout_provider": {
+ "type": "string"
+ }
+ }
},
},
- "started_at": {
- "type": "string",
- "format": "date-time"
+ {
+ "type": "object",
+ "required": [
+ "reason"
+ ],
+ "properties": {
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "tools.job.succeeded": {
+ "billing.cashout-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "tools",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.succeeded.v1.json",
- "title": "ToolsJobSucceededPayload",
- "description": "tools.job.succeeded v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job finished and its result files are stored (in Media when TOOLS_JOB_RESULTS=media). Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",
+ "$id": "https://openvibe.network/contracts/billing/cashout-request.v1.json",
+ "title": "BillingCashoutRequest",
+ "description": "billing.cashout-request@1: bodies of billing.cashout.request on OpenVibe.Billing. POST /api/v1/cashouts: { subject, amount, payout_method: { type, address } } moves amount bits (at least the minimum cashout) from the person's payable into payouts pending, in escrow until staff pay it (a PayPal address must be an email). POST /api/v1/recycle: { subject, amount } turns payable back into the person's own spendable credit. GET /api/v1/cashouts/:id takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "subject",
+ "amount",
+ "payout_method"
+ ],
+ "properties": {
+ "subject": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits."
+ },
+ "payout_method": {
+ "type": "object",
+ "required": [
+ "type",
+ "address"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",
+ "description": "paypal, …"
+ },
+ "address": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 254
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "subject",
+ "amount"
+ ],
+ "properties": {
+ "subject": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits."
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "billing.cashout-result": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/cashout-result.v1.json",
+ "title": "BillingCashoutResult",
+ "description": "billing.cashout-result@1: answers about cashouts on OpenVibe.Billing. POST /api/v1/cashouts (201), GET /api/v1/cashouts/:id, POST /api/v1/cashouts/:id/approve and /deny → { cashout }; GET /api/v1/cashouts?status=&subject=&limit= → { cashouts }; POST /api/v1/recycle (201) → { transaction, balance }. A cashout the Events stream carries names the payout method's type only; this API answers the caller who may see the address.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "cashout"
+ ],
+ "properties": {
+ "cashout": {
+ "$ref": "cashout.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "cashouts"
+ ],
+ "properties": {
+ "cashouts": {
+ "type": "array",
+ "items": {
+ "$ref": "cashout.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "transaction",
+ "balance"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "balance": {
+ "$ref": "balance.v1.json"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "billing.cashout": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/cashout.v1.json",
+ "title": "BillingCashout",
+ "description": "billing.cashout@1: one cashout as OpenVibe.Billing presents it (server/ops/cashouts.js present). Requested cashouts wait out an escrow; staff approve (with the provider's payout reference) or deny them.",
"type": "object",
"type": "object",
"required": [
"required": [
- "job_id",
- "service",
- "type",
- "type_version",
- "state",
- "owner",
- "attempts",
- "max_attempts",
- "created_at",
- "retry_of",
- "started_at",
- "finished_at",
- "expires_at",
- "result"
+ "id",
+ "subject",
+ "amount_bits",
+ "value_cents",
+ "status",
+ "payout_method",
+ "escrow_until",
+ "request_txn",
+ "created_at"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "job_id": {
- "type": "string",
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "service": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$",
- "description": "The Tools satellite that runs the job (img, audio, docs…)."
+ "id": {
+ "type": "string"
},
},
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type, e.g. img.process."
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
},
},
- "type_version": {
+ "amount_bits": {
"type": "integer",
"type": "integer",
"minimum": 1
"minimum": 1
},
},
- "state": {
- "const": "succeeded"
+ "value_cents": {
+ "type": "integer",
+ "minimum": 0
},
},
- "owner": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
+ "status": {
+ "enum": [
+ "requested",
+ "paid",
+ "denied"
+ ]
+ },
+ "payout_method": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "type": {
+ "type": "string"
},
},
- {
- "type": "null"
+ "address": {
+ "type": "string"
}
}
- ],
- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
- },
- "attempts": {
- "type": "integer",
- "minimum": 0
+ }
},
},
- "max_attempts": {
- "type": "integer",
- "minimum": 1
+ "escrow_until": {
+ "type": "string"
},
},
- "created_at": {
- "type": "string",
- "format": "date-time"
+ "request_txn": {
+ "type": "string"
},
},
- "retry_of": {
+ "settle_txn": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
- },
- "started_at": {
- "type": "string",
- "format": "date-time"
+ ]
},
},
- "finished_at": {
- "type": "string",
- "format": "date-time"
+ "payout_provider": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "expires_at": {
+ "payout_reference": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "format": "date-time",
- "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."
+ ]
},
},
- "result": {
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "billing.entitlement-result": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/entitlement-result.v1.json",
+ "title": "BillingEntitlementResult",
+ "description": "billing.entitlement-result@1: answers of billing.entitlement.check on OpenVibe.Billing. GET /api/v1/entitlements/:subject?streamer= → billing.entitlement@1; without streamer → { subject, entitlements } (every active one); GET /api/v1/subscriptions → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",
+ "anyOf": [
+ {
+ "$ref": "entitlement.v1.json"
+ },
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "files"
+ "subject",
+ "entitlements"
],
],
- "additionalProperties": false,
- "description": "Where the result files are, never their content, names or the tool's output data (GET /api/v1/jobs/:id has those, for the owner).",
"properties": {
"properties": {
- "files": {
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "entitlements": {
"type": "array",
"type": "array",
- "maxItems": 100,
"items": {
"items": {
- "type": "object",
- "required": [
- "index",
- "mime",
- "size",
- "sha256",
- "storage",
- "media"
- ],
- "additionalProperties": false,
- "properties": {
- "index": {
- "type": "integer",
- "minimum": 0,
- "description": "n in /api/v1/jobs/:id/files/:n."
- },
- "mime": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200
- },
- "size": {
- "type": "integer",
- "minimum": 0
- },
- "sha256": {
- "type": "string",
- "pattern": "^[0-9a-f]{64}$"
- },
- "storage": {
- "enum": [
- "local",
- "media"
- ],
- "description": "media: stored as an OpenVibe.Media object (TOOLS_JOB_RESULTS=media); local: kept on the satellite until the job expires."
- },
- "media": {
- "oneOf": [
- {
- "$ref": "../../media/media-ref.v1.json"
- },
- {
- "type": "null"
- }
- ],
- "description": "{ media_id, role: output } when storage is media."
- }
- }
+ "$ref": "entitlement.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "subscriptions"
+ ],
+ "properties": {
+ "subscriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "subscription.v1.json"
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "subscription",
+ "entitlement"
+ ],
+ "properties": {
+ "subscription": {
+ "$ref": "subscription.v1.json"
+ },
+ "entitlement": {
+ "$ref": "entitlement.v1.json"
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "tools.job.failed": {
+ "billing.entitlement": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "tools",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/tools.job.failed.v1.json",
- "title": "ToolsJobFailedPayload",
- "description": "tools.job.failed v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job failed: the tool refused the input, it timed out, or it was running when the satellite restarted and its type does not requeue. A cancelled job is not a failure and is not announced as one. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",
+ "$id": "https://openvibe.network/contracts/billing/entitlement.v1.json",
+ "title": "BillingEntitlement",
+ "description": "billing.entitlement@1: whether a person is entitled to a streamer's subscriber perks now (OpenVibe.Billing server/ops/subscriptions.js entitlement): GET /api/v1/entitlements/:subject?streamer=, and inside subscription answers. Chained, already-paid future periods extend expires_at.",
"type": "object",
"type": "object",
"required": [
"required": [
- "job_id",
- "service",
- "type",
- "type_version",
- "state",
- "owner",
- "attempts",
- "max_attempts",
- "created_at",
- "retry_of",
- "started_at",
- "finished_at",
+ "subject",
+ "streamer",
+ "kind",
+ "active",
"expires_at",
"expires_at",
- "error",
- "retryable"
+ "subscription"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "job_id": {
- "type": "string",
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "service": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$",
- "description": "The Tools satellite that runs the job (img, audio, docs…)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type, e.g. img.process."
- },
- "type_version": {
- "type": "integer",
- "minimum": 1
- },
- "state": {
- "const": "failed"
- },
- "owner": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
- ],
- "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
- },
- "attempts": {
- "type": "integer",
- "minimum": 0
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
},
},
- "max_attempts": {
- "type": "integer",
- "minimum": 1
+ "streamer": {
+ "$ref": "../identity/subject-ref.v1.json"
},
},
- "created_at": {
- "type": "string",
- "format": "date-time"
+ "kind": {
+ "type": "string"
},
},
- "retry_of": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ "active": {
+ "type": "boolean"
},
},
- "started_at": {
+ "expires_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "format": "date-time",
- "description": "null when the job failed before it ever ran."
- },
- "finished_at": {
- "type": "string",
- "format": "date-time"
+ ]
},
},
- "expires_at": {
+ "subscription": {
"type": [
"type": [
- "string",
+ "object",
"null"
"null"
],
],
- "format": "date-time",
- "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."
- },
- "error": {
- "type": "object",
- "required": [
- "status",
- "code",
- "detail"
- ],
- "additionalProperties": false,
- "description": "The job's problem (errors.problem@1) without request or trace ids.",
"properties": {
"properties": {
+ "id": {
+ "type": "string"
+ },
"status": {
"status": {
- "type": "integer",
- "minimum": 400,
- "maximum": 599
+ "type": "string"
},
},
- "code": {
- "type": "string",
- "pattern": "^tools\\.[a-z0-9_.]+$",
- "description": "tools.job.failed, tools.job.timeout, or a code the tool chose (tools.…: tools.pdf.too_many_pages, tools.pdf.wrong_password…)."
+ "auto_renew": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
},
},
- "detail": {
- "type": "string",
- "maxLength": 500,
- "description": "Human-readable reason; server paths are scrubbed."
+ "cancel_at_period_end": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "provider": {
+ "type": "string"
}
}
}
}
- },
- "retryable": {
- "type": "boolean"
}
}
}
}
}
}
},
},
- "live.stream.started": {
+ "billing.intent-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "live",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/live.stream.started.v1.json",
- "title": "LiveStreamStartedPayload",
- "description": "live.stream.started v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js createStream). A streams row went live: WebRTC/JSMPEG from the dashboard, RTMP ingest, WHIP, or an OpenRe.Stream session mirrored into Live. Written to Live's event_outbox in the transaction that inserts the row, so the event exists if and only if the stream did. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 1 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",
- "type": "object",
- "required": [
- "stream_id",
- "channel",
- "title",
- "category",
- "protocol",
- "is_nsfw",
- "started_at"
- ],
- "additionalProperties": false,
- "properties": {
- "stream_id": {
- "type": "integer",
- "minimum": 1,
- "description": "Live's streams row id (the envelope subject id)."
- },
- "channel": {
+ "$id": "https://openvibe.network/contracts/billing/intent-request.v1.json",
+ "title": "BillingIntentRequest",
+ "description": "billing.intent-request@1: the body of POST /api/v1/intents on OpenVibe.Billing (billing.intent.create): a checkout with an enabled provider for a Vibes purchase ({ kind: purchase, bits } at least the minimum purchase) or a channel subscription ({ kind: subscription, streamer }; route direct pays the streamer's own account, and a direct PowerChat subscription names receiving_account). Billing prices it; the answer is { intent, checkout_url } (201). GET /api/v1/intents/:id and POST /api/v1/intents/:id/capture take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "username",
- "display_name",
- "url"
+ "provider",
+ "kind",
+ "subject"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "username": {
- "type": "string",
- "minLength": 1,
- "description": "The streamer's Live username (the channel slug)."
- },
- "display_name": {
+ "provider": {
"type": "string",
"type": "string",
- "minLength": 1,
- "description": "The display name, or the username when there is none."
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]{1,39}$",
+ "description": "stripe, paypal, nowpayments, powerchat, ccbill (enabled ones only)."
},
},
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https://openvibe\\.live/",
- "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."
+ "kind": {
+ "enum": [
+ "purchase",
+ "subscription"
+ ]
},
},
"subject": {
"subject": {
- "type": "object",
- "required": [
- "type",
- "id"
+ "description": "Who pays.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "bits": {
+ "type": [
+ "integer",
+ "string"
],
],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Purchase: the Vibes to buy."
+ },
+ "streamer": {
+ "description": "Subscription: the channel's owner (never the payer).",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
},
},
- "id": {
+ {
"type": "string",
"type": "string",
"pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
"pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
}
}
- },
- "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."
+ ]
+ },
+ "route": {
+ "type": "string",
+ "description": "Subscription: direct, or the site route."
+ },
+ "receiving_account": {
+ "type": "string",
+ "description": "Direct PowerChat subscription: the streamer's PowerChat username."
+ },
+ "auto_renew": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "success_url": {
+ "type": "string"
+ },
+ "cancel_url": {
+ "type": "string"
}
}
}
}
},
},
- "title": {
- "type": "string"
- },
- "category": {
- "type": [
- "string",
- "null"
- ],
- "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."
- },
- "protocol": {
- "enum": [
- "jsmpeg",
- "webrtc",
- "rtmp"
- ],
- "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."
- },
- "is_nsfw": {
- "type": "boolean"
- },
- "started_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "UTC. null only for a row without a start time, which createStream never writes."
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "live.stream.ended": {
+ "billing.intent-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "live",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json",
- "title": "LiveStreamEndedPayload",
- "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",
+ "$id": "https://openvibe.network/contracts/billing/intent-result.v1.json",
+ "title": "BillingIntentResult",
+ "description": "billing.intent-result@1: answers of billing.intent.create on OpenVibe.Billing. POST /api/v1/intents → { intent, checkout_url } (null when the provider has no hosted checkout); GET /api/v1/intents/:id → { intent }; POST /api/v1/intents/:id/capture → { result, intent } (the settlement the provider's capture produced).",
"type": "object",
"type": "object",
"required": [
"required": [
- "stream_id",
- "channel",
- "title",
- "category",
- "protocol",
- "is_nsfw",
- "started_at",
- "ended_at",
- "duration_seconds"
+ "intent"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "stream_id": {
- "type": "integer",
- "minimum": 1,
- "description": "Live's streams row id (the envelope subject id)."
- },
- "channel": {
- "type": "object",
- "required": [
- "username",
- "display_name",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "username": {
- "type": "string",
- "minLength": 1,
- "description": "The streamer's Live username (the channel slug)."
- },
- "display_name": {
- "type": "string",
- "minLength": 1,
- "description": "The display name, or the username when there is none."
- },
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https://openvibe\\.live/",
- "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."
- },
- "subject": {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- },
- "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."
- }
- }
- },
- "title": {
- "type": "string"
- },
- "category": {
- "type": [
- "string",
- "null"
- ],
- "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."
- },
- "protocol": {
- "enum": [
- "jsmpeg",
- "webrtc",
- "rtmp"
- ],
- "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."
- },
- "is_nsfw": {
- "type": "boolean"
+ "intent": {
+ "$ref": "intent.v1.json"
},
},
- "started_at": {
+ "checkout_url": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "format": "date-time",
- "description": "UTC. null only for a row without a start time, which createStream never writes."
- },
- "ended_at": {
- "type": "string",
- "format": "date-time",
- "description": "UTC."
+ ]
},
},
- "duration_seconds": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 0,
- "description": "Whole seconds from started_at to ended_at; null when the row has no start time."
+ "result": {
+ "description": "Capture: what applying the provider's plan did."
}
}
}
}
}
}
},
},
- "live.release.deployed": {
+ "billing.intent": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "live",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/live.release.deployed.v1.json",
- "title": "LiveReleaseDeployedPayload",
- "description": "live.release.deployed v1 (OpenVibe.Live server/events/release-events.js envelopeFor, called by server/chat/deploy-notice.js announce). The first boot that runs commits never announced before: queued in the transaction that records them as announced (site setting deploy_last_announced), so a restart without new code emits nothing and a crash before that commit announces again on the next boot with the same subject. A consumer keys on subject.id (the head commit) to fold repeats. The commit subjects are public already (Live chat and /updates); the consumer decides what is shown. Envelope: subject { type: release, id: <head commit, 40 hex> }, visibility internal, priority low, actor service:live.",
+ "$id": "https://openvibe.network/contracts/billing/intent.v1.json",
+ "title": "BillingIntent",
+ "description": "billing.intent@1: a checkout intent as OpenVibe.Billing presents it (server/ops/intents.js present): what a person is about to pay a provider for (a Vibes purchase or a channel subscription), priced by Billing.",
"type": "object",
"type": "object",
"required": [
"required": [
- "service",
- "release",
- "commit",
- "previous",
- "commit_count",
- "commits",
- "deployed_at",
- "notes_url"
+ "id",
+ "provider",
+ "provider_ref",
+ "kind",
+ "subject",
+ "streamer",
+ "amount_cents",
+ "fee_cents",
+ "bits",
+ "route",
+ "status",
+ "settled_txn",
+ "created_at"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "service": {
- "const": "live"
- },
- "release": {
+ "id": {
"type": "string",
"type": "string",
- "pattern": "^[0-9a-f]{7,40}$",
- "description": "The head commit's short sha as git abbreviated it (its first 12 characters when the head is not in the list)."
+ "pattern": "^pi_"
},
},
- "commit": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$",
- "description": "The head commit (the envelope subject id)."
+ "provider": {
+ "type": "string"
},
},
- "previous": {
+ "provider_ref": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "pattern": "^[0-9a-f]{40}$",
- "description": "The head announced before this one; null on the first announcement."
+ ]
},
},
- "commit_count": {
+ "kind": {
+ "enum": [
+ "purchase",
+ "subscription"
+ ]
+ },
+ "subject": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "streamer": {
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "amount_cents": {
"type": "integer",
"type": "integer",
- "minimum": 1,
- "maximum": 40,
- "description": "How many new commits were found (git log previous..head, at most 40; only the head on a first run or after history was rewritten)."
+ "minimum": 0
},
},
- "commits": {
- "type": "array",
- "minItems": 1,
- "maxItems": 40,
- "description": "The new commits, newest first.",
- "items": {
- "type": "object",
- "required": [
- "hash",
- "short",
- "subject",
- "date"
- ],
- "additionalProperties": false,
- "properties": {
- "hash": {
- "type": "string",
- "pattern": "^[0-9a-f]{40}$"
- },
- "short": {
- "type": "string",
- "pattern": "^[0-9a-f]{4,40}$"
- },
- "subject": {
- "type": "string",
- "maxLength": 200,
- "description": "The commit subject, cut to 200 characters."
- },
- "date": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "Author date (git %aI, with the author's UTC offset)."
+ "fee_cents": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "bits": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "route": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "auto_renew": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "status": {
+ "type": "string",
+ "description": "created, settled, failed, …"
+ },
+ "settled_txn": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "checkout_ref": {
+ "type": "string",
+ "description": "PowerChat only: pcorder:<id> or pcsub:<id>, echoed back by its webhook."
+ }
+ }
+ }
+ },
+ "billing.subscription-request": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/subscription-request.v1.json",
+ "title": "BillingSubscriptionRequest",
+ "description": "billing.subscription-request@1: the body of POST /api/v1/subscriptions on OpenVibe.Billing (billing.subscription.manage): pay one period of subscriber → streamer, from the subscriber's credit (source credit, the default) or from a provider receipt (source receipt, which also needs billing.ledger.admin: claiming money was received is operator grade). POST /api/v1/subscriptions/:id/cancel (cancels at the period end, with the provider too) and the GETs take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscriber",
+ "streamer"
+ ],
+ "properties": {
+ "subscriber": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "streamer": {
+ "description": "A user SubjectRef, or its bare id (usr_…). Guests hold no money.",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "source": {
+ "enum": [
+ "credit",
+ "receipt"
+ ]
+ },
+ "price_cents": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Default: Billing's subscription price."
+ },
+ "auto_renew": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "receipt": {
+ "type": "object",
+ "required": [
+ "provider",
+ "provider_ref"
+ ],
+ "properties": {
+ "provider": {
+ "type": "string"
+ },
+ "provider_ref": {
+ "type": "string"
+ },
+ "amount_cents": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Cents paid."
+ },
+ "fee_cents": {
+ "type": [
+ "integer",
+ "string"
+ ]
+ },
+ "route": {
+ "type": "string"
+ },
+ "subscription_ref": {
+ "type": "string"
+ },
+ "test": {
+ "type": "boolean"
+ }
}
}
}
}
}
}
},
},
- "deployed_at": {
- "type": "string",
- "format": "date-time",
- "description": "When this boot announced it (UTC)."
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "billing.subscription-result": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/subscription-result.v1.json",
+ "title": "BillingSubscriptionResult",
+ "description": "billing.subscription-result@1: answers about subscriptions on OpenVibe.Billing. POST /api/v1/subscriptions (201) → { subscription, entitlement, transaction }; POST /api/v1/subscriptions/:id/cancel → { subscription, provider_sync }; GET /api/v1/subscriptions?subscriber=&streamer=&status= → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "subscription"
+ ],
+ "properties": {
+ "subscription": {
+ "$ref": "subscription.v1.json"
+ },
+ "entitlement": {
+ "description": "billing.entitlement@1 (the answer to POST /subscriptions carries the entitlement as booked)."
+ },
+ "transaction": {
+ "$ref": "transaction.v1.json"
+ },
+ "provider_sync": {
+ "type": "string",
+ "description": "not_needed, or how the provider was told."
+ }
+ }
},
},
- "notes_url": {
- "const": "https://openvibe.live/updates"
+ {
+ "type": "object",
+ "required": [
+ "subscriptions"
+ ],
+ "properties": {
+ "subscriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "subscription.v1.json"
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "media.job.proposed": {
+ "billing.subscription": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",
- "title": "MediaJobProposedPayload",
- "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "$id": "https://openvibe.network/contracts/billing/subscription.v1.json",
+ "title": "BillingSubscription",
+ "description": "billing.subscription@1: one channel subscription as OpenVibe.Billing presents it (server/ops/subscriptions.js present).",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "app_id",
- "object_id",
- "type",
+ "subscriber",
+ "streamer",
+ "provider",
"status",
"status",
- "attempts",
- "max_attempts",
- "error_code",
- "cancel_requested",
- "has_result",
- "run_after",
- "decided_at",
- "created_at",
- "updated_at",
- "started_at",
- "finished_at"
+ "auto_renew",
+ "cancel_at_period_end",
+ "current_period_end"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
"id": {
"id": {
- "type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ "type": "string"
},
},
- "app_id": {
+ "subscriber": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "streamer": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "tier": {},
+ "provider": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ "description": "credit, or the provider that takes the payments."
},
},
- "object_id": {
+ "provider_ref": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "route": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 200,
- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ "description": "site or direct (paid to the streamer's own provider account)."
},
},
- "type": {
+ "status": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ "description": "active, canceled, expired, …"
},
},
- "status": {
- "const": "proposed"
+ "auto_renew": {
+ "type": "boolean"
},
},
- "attempts": {
- "type": "integer",
- "minimum": 0,
- "description": "Attempts started so far."
+ "cancel_at_period_end": {
+ "type": "boolean"
},
},
- "max_attempts": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
- },
- "error_code": {
- "type": "null"
- },
- "cancel_requested": {
- "type": "boolean",
- "description": "Its owner asked a running job to stop."
- },
- "has_result": {
- "const": false,
- "description": "A proposal has no result."
+ "price_cents": {
+ "type": [
+ "integer",
+ "null"
+ ]
},
},
- "run_after": {
+ "current_period_end": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
- },
- "decided_at": {
- "type": "null",
- "description": "Nobody has decided yet."
+ ]
},
},
"created_at": {
"created_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ "type": "string"
},
},
"updated_at": {
"updated_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
- },
- "started_at": {
- "type": "null"
- },
- "finished_at": {
- "type": "null"
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
}
}
},
},
- "media.job.queued": {
+ "billing.transaction": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",
- "title": "MediaJobQueuedPayload",
- "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "$id": "https://openvibe.network/contracts/billing/transaction.v1.json",
+ "title": "BillingTransaction",
+ "description": "billing.transaction@1: one journal transaction as OpenVibe.Billing's /api/v1 presents it (server/ledger.js present). Amounts are integers: bits for vibes-bits, cents for usd-cents; each transaction's entries balance per currency. Subjects are bare Network subject ids.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "app_id",
- "object_id",
"type",
"type",
"status",
"status",
- "attempts",
- "max_attempts",
- "error_code",
- "cancel_requested",
- "has_result",
- "run_after",
- "decided_at",
+ "test",
+ "reverses_txn",
+ "from_subject",
+ "to_subject",
+ "provider",
+ "receipt_ref",
"created_at",
"created_at",
- "updated_at",
- "started_at",
- "finished_at"
+ "entries"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
- },
- "app_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
- },
- "object_id": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ "pattern": "^txn_"
},
},
"type": {
"type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ "enum": [
+ "purchase",
+ "donation",
+ "subscription",
+ "subscription_share",
+ "cashout_request",
+ "cashout_paid",
+ "cashout_denied",
+ "recycle",
+ "refund",
+ "chargeback",
+ "adjustment",
+ "import"
+ ]
},
},
"status": {
"status": {
- "const": "queued"
- },
- "attempts": {
- "type": "integer",
- "minimum": 0,
- "description": "Attempts started so far."
+ "type": "string"
},
},
- "max_attempts": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
+ "test": {
+ "type": "boolean",
+ "description": "Test money: never counted."
},
},
- "error_code": {
- "type": "null"
+ "reverses_txn": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "cancel_requested": {
- "type": "boolean",
- "description": "Its owner asked a running job to stop."
+ "from_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "has_result": {
- "const": false,
- "description": "A job that never ran has no result."
+ "to_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "run_after": {
+ "provider": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "A queued job waits until then (retry backoff); null otherwise."
+ ]
},
},
- "decided_at": {
+ "receipt_ref": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
+ "description": "<provider>:<provider payment id> for money that arrived from a provider."
},
},
- "created_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ "actor": {
+ "description": "Who posted it (as recorded)."
},
},
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ "metadata": {
+ "description": "Per type: amount_bits, paid_cents, target EntityRef, message, rates…"
},
},
- "started_at": {
- "type": "null"
+ "created_at": {
+ "type": "string"
},
},
- "finished_at": {
- "type": "null"
+ "entries": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "account",
+ "amount"
+ ],
+ "properties": {
+ "account": {
+ "type": "object",
+ "required": [
+ "kind",
+ "currency"
+ ],
+ "properties": {
+ "kind": {
+ "enum": [
+ "user_credit",
+ "creator_payable",
+ "provider_clearing",
+ "platform_revenue",
+ "payouts_pending",
+ "refunds",
+ "import_adjustment",
+ "chargeback_loss",
+ "fx_conversion"
+ ]
+ },
+ "owner": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "A subject id, a provider slug, or null for a platform account."
+ },
+ "currency": {
+ "enum": [
+ "vibes-bits",
+ "usd-cents"
+ ]
+ }
+ }
+ },
+ "amount": {
+ "type": "integer",
+ "description": "Signed: negative leaves the account."
+ }
+ }
+ }
}
}
}
}
}
}
},
},
- "media.job.started": {
+ "billing.transfer-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "billing",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",
- "title": "MediaJobStartedPayload",
- "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
- "type": "object",
- "required": [
- "id",
- "app_id",
- "object_id",
- "type",
- "status",
- "attempts",
- "max_attempts",
- "error_code",
- "cancel_requested",
- "has_result",
- "run_after",
- "decided_at",
- "created_at",
- "updated_at",
- "started_at",
- "finished_at"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
- },
- "app_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
- },
- "object_id": {
- "type": [
- "string",
- "null"
+ "$id": "https://openvibe.network/contracts/billing/transfer-request.v1.json",
+ "title": "BillingTransferRequest",
+ "description": "billing.transfer-request@1: bodies of billing.transfer.create on OpenVibe.Billing. POST /api/v1/transfers: { from, to, amount, kind?, target?, message? } moves amount bits from the sender's credit to the recipient's payable (a tip, donation or paid interaction; never to oneself). POST /api/v1/transfers/:id/refund: { amount?, reason? } gives a credit-funded donation back (all that remains refundable when amount is absent). Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "from",
+ "to",
+ "amount"
],
],
- "maxLength": 200,
- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
- },
- "status": {
- "const": "running",
- "description": "started carries the job's state, running."
- },
- "attempts": {
- "type": "integer",
- "minimum": 1,
- "description": "Attempts started so far, this one included."
- },
- "max_attempts": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
- },
- "error_code": {
- "type": [
- "string",
- "null"
- ],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
- },
- "cancel_requested": {
- "type": "boolean",
- "description": "Its owner asked a running job to stop."
- },
- "has_result": {
- "const": false,
- "description": "A running job has no result yet."
+ "properties": {
+ "from": {
+ "description": "The sender (their credit pays).",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "to": {
+ "description": "The recipient (their payable receives).",
+ "anyOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ ]
+ },
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits."
+ },
+ "kind": {
+ "enum": [
+ "tip",
+ "donation",
+ "paid_interaction"
+ ],
+ "description": "Default donation."
+ },
+ "target": {
+ "$ref": "../common/entity-ref.v1.json",
+ "description": "What the money is for (a stream, a paid message, …)."
+ },
+ "message": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500
+ }
+ }
},
},
- "run_after": {
- "type": "null",
- "description": "Cleared when the job is claimed."
+ {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 1,
+ "pattern": "^[1-9][0-9]*$",
+ "description": "Bits to give back; default all that remains refundable."
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ }
+ }
+ }
+ ]
+ }
+ },
+ "billing.transfer-result": {
+ "version": "1.0.0",
+ "owner": "billing",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/billing/transfer-result.v1.json",
+ "title": "BillingTransferResult",
+ "description": "billing.transfer-result@1: answers of billing.transfer.create on OpenVibe.Billing (201). POST /api/v1/transfers → { transaction, balance: { credit } } (the sender's credit after it); POST /api/v1/transfers/:id/refund → { transaction } (type refund, reverses_txn the donation).",
+ "type": "object",
+ "required": [
+ "transaction"
+ ],
+ "properties": {
+ "transaction": {
+ "$ref": "transaction.v1.json"
},
},
- "decided_at": {
- "type": [
- "string",
- "null"
+ "balance": {
+ "type": "object",
+ "required": [
+ "credit"
],
],
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
- },
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
- },
- "started_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "The first attempt's start."
- },
- "finished_at": {
- "type": "null"
+ "properties": {
+ "credit": {
+ "type": "integer"
+ }
+ }
}
}
}
}
}
}
},
},
- "media.job.retrying": {
+ "deals.watch.matched": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "deals",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",
- "title": "MediaJobRetryingPayload",
- "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.watch.matched.v1.json",
+ "title": "DealsWatchMatchedPayload",
+ "description": "deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.",
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "app_id",
- "object_id",
- "type",
- "status",
- "attempts",
- "max_attempts",
- "error_code",
- "cancel_requested",
- "has_result",
- "run_after",
- "decided_at",
- "created_at",
- "updated_at",
- "started_at",
- "finished_at"
+ "watch_id",
+ "recipient",
+ "kind",
+ "query",
+ "product_id",
+ "max_price",
+ "currency",
+ "offer_id",
+ "offer_url",
+ "title",
+ "observation"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": {
+ "watch_id": {
"type": "string",
"type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ "pattern": "^dwt_[0-9A-HJKMNP-TV-Z]{26}$"
},
},
- "app_id": {
+ "recipient": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The watch owner's user subject id: who is notified."
},
},
- "object_id": {
+ "kind": {
+ "enum": [
+ "keyword",
+ "product",
+ "price_below"
+ ]
+ },
+ "query": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
"maxLength": 200,
"maxLength": 200,
- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
- },
- "status": {
- "const": "queued",
- "description": "retrying carries the job's state, queued."
- },
- "attempts": {
- "type": "integer",
- "minimum": 1,
- "description": "Attempts started so far, the failed one included."
- },
- "max_attempts": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
+ "description": "Keywords of a keyword watch, or of a price-below watch without a product."
},
},
- "error_code": {
+ "product_id": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
- },
- "cancel_requested": {
- "type": "boolean",
- "description": "Its owner asked a running job to stop."
- },
- "has_result": {
- "const": false,
- "description": "A job waiting for its retry has no result."
+ "maxLength": 64,
+ "description": "Deals product id of a product or price-below watch."
},
},
- "run_after": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "Not before this time (the retry backoff)."
+ "max_price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Price-below threshold as entered (decimal text, e.g. 19.99)."
},
},
- "decided_at": {
+ "currency": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217 currency of max_price."
},
},
- "created_at": {
+ "offer_id": {
"type": "string",
"type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "The canonical (root) offer id (dof_…)."
},
},
- "updated_at": {
+ "offer_url": {
"type": "string",
"type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical offer page URL on Deals."
},
},
- "started_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "The first attempt's start."
- },
- "finished_at": {
- "type": "null"
- }
- }
- }
- },
- "media.job.succeeded": {
- "version": "1.0.0",
- "owner": "media",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",
- "title": "MediaJobSucceededPayload",
- "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
- "type": "object",
- "required": [
- "id",
- "app_id",
- "object_id",
- "type",
- "status",
- "attempts",
- "max_attempts",
- "error_code",
- "cancel_requested",
- "has_result",
- "run_after",
- "decided_at",
- "created_at",
- "updated_at",
- "started_at",
- "finished_at"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
- },
- "app_id": {
+ "title": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
- },
- "object_id": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 200,
- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
- },
- "status": {
- "const": "succeeded"
- },
- "attempts": {
- "type": "integer",
- "minimum": 1,
- "description": "Attempts started, the successful one included."
- },
- "max_attempts": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
- },
- "error_code": {
- "type": "null",
- "description": "A success clears the last failure."
- },
- "cancel_requested": {
- "type": "boolean",
- "description": "Its owner asked a running job to stop."
- },
- "has_result": {
- "type": "boolean",
- "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it."
- },
- "run_after": {
- "type": "null"
+ "maxLength": 500
},
},
- "decided_at": {
- "type": [
- "string",
- "null"
+ "observation": {
+ "type": "object",
+ "required": [
+ "id",
+ "observed_at",
+ "price",
+ "currency",
+ "availability"
],
],
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
- },
- "created_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
- },
- "updated_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
- },
- "started_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
- "description": "The first attempt's start."
- },
- "finished_at": {
- "type": "string",
- "format": "date-time",
- "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Decimal text as stated; null when the observation stated no price."
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$"
+ },
+ "availability": {
+ "enum": [
+ "in_stock",
+ "out_of_stock",
+ "preorder",
+ "discontinued",
+ "limited",
+ "sold_out",
+ "online_only",
+ "in_store_only",
+ null
+ ]
+ }
+ }
}
}
}
}
}
}
},
},
- "media.job.failed": {
+ "trade.alert.triggered": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "trade",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",
- "title": "MediaJobFailedPayload",
- "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.alert.triggered.v1.json",
+ "title": "TradeAlertTriggeredPayload",
+ "description": "trade.alert.triggered v1 (OpenVibe.Trade server/domain/alerts.js deliver). One delivery of one private alert rule: a threshold crossed by a new observation (trigger kind observation), or a newly seen document of the instrument (filing_type / new_document rules, trigger kind document). Written in the transaction that recorded the observation or document; at most one per (rule, trigger). Envelope: subject { type: user, id: <rule owner usr_…> }, visibility subject, priority important, actor service:trade. Data only, never rendered notification text (ADR-020): OpenVibe.Network renders a TRADE_ALERT notification (server/notifications/events-consumer.js). Information only, not investment advice.",
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "app_id",
- "object_id",
- "type",
- "status",
- "attempts",
- "max_attempts",
- "error_code",
- "cancel_requested",
- "has_result",
- "run_after",
- "decided_at",
- "created_at",
- "updated_at",
- "started_at",
- "finished_at"
+ "delivery_id",
+ "rule",
+ "instrument",
+ "trigger",
+ "disclaimer"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
- },
- "app_id": {
+ "delivery_id": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ "pattern": "^ald_[0-9A-HJKMNP-TV-Z]{26}$"
},
},
- "object_id": {
- "type": [
- "string",
- "null"
+ "rule": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "kind",
+ "status",
+ "instrument",
+ "metric",
+ "operator",
+ "threshold",
+ "unit",
+ "currency",
+ "form_types",
+ "armed",
+ "created_at",
+ "updated_at"
],
],
- "maxLength": 200,
- "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
- "maxLength": 80,
- "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
- },
- "status": {
- "const": "failed"
- },
- "attempts": {
- "type": "integer",
- "minimum": 1,
- "description": "Attempts started, the last failed one included."
- },
- "max_attempts": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
- },
- "error_code": {
- "type": [
- "string",
- "null"
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^alr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "kind": {
+ "enum": [
+ "threshold",
+ "filing_type",
+ "new_document"
+ ]
+ },
+ "status": {
+ "enum": [
+ "active",
+ "deleted"
+ ]
+ },
+ "instrument": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "symbol": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "metric": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 80,
+ "description": "Threshold rules: e.g. price.close, volume, us-gaap:Revenues."
+ },
+ "operator": {
+ "enum": [
+ "above",
+ "below",
+ null
+ ]
+ },
+ "threshold": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 64,
+ "description": "Decimal text as entered."
+ },
+ "unit": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 20
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$"
+ },
+ "form_types": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "string",
+ "maxLength": 20
+ },
+ "description": "filing_type rules: the 1–10 form types that trigger it (10-K, 8-K…)."
+ },
+ "armed": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Threshold rules: armed before this delivery (always true when it fired); null for document rules."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "instrument": {
+ "type": "object",
+ "required": [
+ "id",
+ "symbol",
+ "name",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "symbol": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 32
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Instrument page on Trade."
+ }
+ }
+ },
+ "trigger": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "id",
+ "metric",
+ "value",
+ "unit",
+ "currency",
+ "observed_at",
+ "retrieved_at",
+ "source_key",
+ "source_url"
+ ],
+ "properties": {
+ "kind": {
+ "const": "observation"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "metric": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "value": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "The decimal exactly as the source stated it."
+ },
+ "unit": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$"
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "retrieved_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "source_key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "OpenVibe.Sources source key."
+ },
+ "source_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2048
+ }
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "id",
+ "form_type",
+ "title",
+ "url",
+ "published_at",
+ "retrieved_at",
+ "source_key"
+ ],
+ "properties": {
+ "kind": {
+ "const": "document"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "form_type": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 20
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 1000
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2048
+ },
+ "published_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "The source's own date; null if it gave none."
+ },
+ "retrieved_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "source_key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128
+ }
+ }
+ }
+ ]
+ },
+ "disclaimer": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "Currently \"Information only — not investment advice; no trading here.\""
+ }
+ }
+ }
+ },
+ "tools.job.created": {
+ "version": "1.0.0",
+ "owner": "tools",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.created.v1.json",
+ "title": "ToolsJobCreatedPayload",
+ "description": "tools.job.created v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A job was accepted and queued (POST /api/v1/jobs, or a retry of a failed job, which carries retry_of). An Idempotency-Key replay creates no job and no event. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor the owner when it is a subject (user, service, app or mod), else service:tools.",
+ "type": "object",
+ "required": [
+ "job_id",
+ "service",
+ "type",
+ "type_version",
+ "state",
+ "owner",
+ "attempts",
+ "max_attempts",
+ "created_at",
+ "retry_of"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "job_id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
+ "description": "The Tools satellite that runs the job (img, audio, docs…)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type, e.g. img.process."
+ },
+ "type_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "state": {
+ "const": "queued"
+ },
+ "owner": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "retry_of": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ }
+ }
+ }
+ },
+ "tools.job.started": {
+ "version": "1.0.0",
+ "owner": "tools",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.started.v1.json",
+ "title": "ToolsJobStartedPayload",
+ "description": "tools.job.started v1 (OpenVibe.Tools apps/_shared/jobs/events.js). A worker claimed the job and it is running (attempts counts this attempt). A job requeued after a restart starts again and is announced again. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:tools.",
+ "type": "object",
+ "required": [
+ "job_id",
+ "service",
+ "type",
+ "type_version",
+ "state",
+ "owner",
+ "attempts",
+ "max_attempts",
+ "created_at",
+ "retry_of",
+ "started_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "job_id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
+ "description": "The Tools satellite that runs the job (img, audio, docs…)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type, e.g. img.process."
+ },
+ "type_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "state": {
+ "const": "running"
+ },
+ "owner": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "retry_of": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "tools.job.succeeded": {
+ "version": "1.0.0",
+ "owner": "tools",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.succeeded.v1.json",
+ "title": "ToolsJobSucceededPayload",
+ "description": "tools.job.succeeded v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job finished and its result files are stored (in Media when TOOLS_JOB_RESULTS=media). Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",
+ "type": "object",
+ "required": [
+ "job_id",
+ "service",
+ "type",
+ "type_version",
+ "state",
+ "owner",
+ "attempts",
+ "max_attempts",
+ "created_at",
+ "retry_of",
+ "started_at",
+ "finished_at",
+ "expires_at",
+ "result"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "job_id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
+ "description": "The Tools satellite that runs the job (img, audio, docs…)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type, e.g. img.process."
+ },
+ "type_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "state": {
+ "const": "succeeded"
+ },
+ "owner": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "retry_of": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "finished_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."
+ },
+ "result": {
+ "type": "object",
+ "required": [
+ "files"
+ ],
+ "additionalProperties": false,
+ "description": "Where the result files are, never their content, names or the tool's output data (GET /api/v1/jobs/:id has those, for the owner).",
+ "properties": {
+ "files": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "required": [
+ "index",
+ "mime",
+ "size",
+ "sha256",
+ "storage",
+ "media"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "index": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "n in /api/v1/jobs/:id/files/:n."
+ },
+ "mime": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "size": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sha256": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$"
+ },
+ "storage": {
+ "enum": [
+ "local",
+ "media"
+ ],
+ "description": "media: stored as an OpenVibe.Media object (TOOLS_JOB_RESULTS=media); local: kept on the satellite until the job expires."
+ },
+ "media": {
+ "oneOf": [
+ {
+ "$ref": "../../media/media-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "{ media_id, role: output } when storage is media."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tools.job.failed": {
+ "version": "1.0.0",
+ "owner": "tools",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/tools.job.failed.v1.json",
+ "title": "ToolsJobFailedPayload",
+ "description": "tools.job.failed v1 (OpenVibe.Tools apps/_shared/jobs/events.js). The job failed: the tool refused the input, it timed out, or it was running when the satellite restarted and its type does not requeue. A cancelled job is not a failure and is not announced as one. Emitted in the transaction that records the transition, beside the job's own SSE event. Carries ids, type, owner, state, times and result locations; never the job's input, its files, its output data or a session. Jobs of sandbox app tokens are not announced. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:tools.",
+ "type": "object",
+ "required": [
+ "job_id",
+ "service",
+ "type",
+ "type_version",
+ "state",
+ "owner",
+ "attempts",
+ "max_attempts",
+ "created_at",
+ "retry_of",
+ "started_at",
+ "finished_at",
+ "expires_at",
+ "error",
+ "retryable"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "job_id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
+ "description": "The Tools satellite that runs the job (img, audio, docs…)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type, e.g. img.process."
+ },
+ "type_version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "state": {
+ "const": "failed"
+ },
+ "owner": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Who submitted the job: a user (user:usr_…), a service, app or mod principal. null for an anonymous browser session; the session is never published."
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "retry_of": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The failed job this one retries (POST /api/v1/jobs/:id/retry)."
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "null when the job failed before it ever ran."
+ },
+ "finished_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When the job and its result files are pruned; null while something references the result (PUT /api/v1/jobs/:id/references/:ref)."
+ },
+ "error": {
+ "type": "object",
+ "required": [
+ "status",
+ "code",
+ "detail"
+ ],
+ "additionalProperties": false,
+ "description": "The job's problem (errors.problem@1) without request or trace ids.",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "minimum": 400,
+ "maximum": 599
+ },
+ "code": {
+ "type": "string",
+ "pattern": "^tools\\.[a-z0-9_.]+$",
+ "description": "tools.job.failed, tools.job.timeout, or a code the tool chose (tools.…: tools.pdf.too_many_pages, tools.pdf.wrong_password…)."
+ },
+ "detail": {
+ "type": "string",
+ "maxLength": 500,
+ "description": "Human-readable reason; server paths are scrubbed."
+ }
+ }
+ },
+ "retryable": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "live.stream.started": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.stream.started.v1.json",
+ "title": "LiveStreamStartedPayload",
+ "description": "live.stream.started v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js createStream). A streams row went live: WebRTC/JSMPEG from the dashboard, RTMP ingest, WHIP, or an OpenRe.Stream session mirrored into Live. Written to Live's event_outbox in the transaction that inserts the row, so the event exists if and only if the stream did. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 1 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",
+ "type": "object",
+ "required": [
+ "stream_id",
+ "channel",
+ "title",
+ "category",
+ "protocol",
+ "is_nsfw",
+ "started_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live's streams row id (the envelope subject id)."
+ },
+ "channel": {
+ "type": "object",
+ "required": [
+ "username",
+ "display_name",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The streamer's Live username (the channel slug)."
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The display name, or the username when there is none."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://openvibe\\.live/",
+ "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."
+ },
+ "subject": {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."
+ }
+ }
+ },
+ "title": {
+ "type": "string"
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."
+ },
+ "protocol": {
+ "enum": [
+ "jsmpeg",
+ "webrtc",
+ "rtmp"
+ ],
+ "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."
+ },
+ "is_nsfw": {
+ "type": "boolean"
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "UTC. null only for a row without a start time, which createStream never writes."
+ }
+ }
+ }
+ },
+ "live.stream.ended": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json",
+ "title": "LiveStreamEndedPayload",
+ "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",
+ "type": "object",
+ "required": [
+ "stream_id",
+ "channel",
+ "title",
+ "category",
+ "protocol",
+ "is_nsfw",
+ "started_at",
+ "ended_at",
+ "duration_seconds"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live's streams row id (the envelope subject id)."
+ },
+ "channel": {
+ "type": "object",
+ "required": [
+ "username",
+ "display_name",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The streamer's Live username (the channel slug)."
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The display name, or the username when there is none."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://openvibe\\.live/",
+ "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."
+ },
+ "subject": {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
+ "description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."
+ }
+ }
+ },
+ "title": {
+ "type": "string"
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."
+ },
+ "protocol": {
+ "enum": [
+ "jsmpeg",
+ "webrtc",
+ "rtmp"
+ ],
+ "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."
+ },
+ "is_nsfw": {
+ "type": "boolean"
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "UTC. null only for a row without a start time, which createStream never writes."
+ },
+ "ended_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "UTC."
+ },
+ "duration_seconds": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "Whole seconds from started_at to ended_at; null when the row has no start time."
+ }
+ }
+ }
+ },
+ "live.release.deployed": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.release.deployed.v1.json",
+ "title": "LiveReleaseDeployedPayload",
+ "description": "live.release.deployed v1 (OpenVibe.Live server/events/release-events.js envelopeFor, called by server/chat/deploy-notice.js announce). The first boot that runs commits never announced before: queued in the transaction that records them as announced (site setting deploy_last_announced), so a restart without new code emits nothing and a crash before that commit announces again on the next boot with the same subject. A consumer keys on subject.id (the head commit) to fold repeats. The commit subjects are public already (Live chat and /updates); the consumer decides what is shown. Envelope: subject { type: release, id: <head commit, 40 hex> }, visibility internal, priority low, actor service:live.",
+ "type": "object",
+ "required": [
+ "service",
+ "release",
+ "commit",
+ "previous",
+ "commit_count",
+ "commits",
+ "deployed_at",
+ "notes_url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "service": {
+ "const": "live"
+ },
+ "release": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{7,40}$",
+ "description": "The head commit's short sha as git abbreviated it (its first 12 characters when the head is not in the list)."
+ },
+ "commit": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$",
+ "description": "The head commit (the envelope subject id)."
+ },
+ "previous": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9a-f]{40}$",
+ "description": "The head announced before this one; null on the first announcement."
+ },
+ "commit_count": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 40,
+ "description": "How many new commits were found (git log previous..head, at most 40; only the head on a first run or after history was rewritten)."
+ },
+ "commits": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 40,
+ "description": "The new commits, newest first.",
+ "items": {
+ "type": "object",
+ "required": [
+ "hash",
+ "short",
+ "subject",
+ "date"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "hash": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ },
+ "short": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{4,40}$"
+ },
+ "subject": {
+ "type": "string",
+ "maxLength": 200,
+ "description": "The commit subject, cut to 200 characters."
+ },
+ "date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Author date (git %aI, with the author's UTC offset)."
+ }
+ }
+ }
+ },
+ "deployed_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When this boot announced it (UTC)."
+ },
+ "notes_url": {
+ "const": "https://openvibe.live/updates"
+ }
+ }
+ }
+ },
+ "media.job.proposed": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.job.proposed.v1.json",
+ "title": "MediaJobProposedPayload",
+ "description": "media.job.proposed v1 (OpenVibe.Media server/jobs/queue.js enqueue; server/events.js recordJob). A job was proposed and waits for its owner: approving it queues it (media.job.queued), cancelling it ends it (media.job.cancelled); the worker never runs a proposal. The size-invariant validator (invariant.scan) proposes object.split and object.remux this way. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "object_id",
+ "type",
+ "status",
+ "attempts",
+ "max_attempts",
+ "error_code",
+ "cancel_requested",
+ "has_result",
+ "run_after",
+ "decided_at",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "finished_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "object_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ },
+ "status": {
+ "const": "proposed"
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Attempts started so far."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "error_code": {
+ "type": "null"
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "Its owner asked a running job to stop."
+ },
+ "has_result": {
+ "const": false,
+ "description": "A proposal has no result."
+ },
+ "run_after": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "A queued job waits until then (retry backoff); null otherwise."
+ },
+ "decided_at": {
+ "type": "null",
+ "description": "Nobody has decided yet."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "started_at": {
+ "type": "null"
+ },
+ "finished_at": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ "media.job.queued": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.job.queued.v1.json",
+ "title": "MediaJobQueuedPayload",
+ "description": "media.job.queued v1 (OpenVibe.Media server/jobs/queue.js enqueue, approve; server/events.js recordJob). A job was accepted and queued: a new job (POST /api/v2/:app/jobs), or a proposal its owner approved (decided_at). An Idempotency-Key replay, or a request joined to an identical job that is still active, creates no job and no event. A failed attempt that is queued again is media.job.retrying, not this. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "object_id",
+ "type",
+ "status",
+ "attempts",
+ "max_attempts",
+ "error_code",
+ "cancel_requested",
+ "has_result",
+ "run_after",
+ "decided_at",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "finished_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "object_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ },
+ "status": {
+ "const": "queued"
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Attempts started so far."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "error_code": {
+ "type": "null"
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "Its owner asked a running job to stop."
+ },
+ "has_result": {
+ "const": false,
+ "description": "A job that never ran has no result."
+ },
+ "run_after": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "A queued job waits until then (retry backoff); null otherwise."
+ },
+ "decided_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "started_at": {
+ "type": "null"
+ },
+ "finished_at": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ "media.job.started": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.job.started.v1.json",
+ "title": "MediaJobStartedPayload",
+ "description": "media.job.started v1 (OpenVibe.Media server/jobs/queue.js claim; server/events.js recordJob). The worker claimed the job and it is running (status running): attempts counts this attempt, started_at stays the first attempt's start. A job that is retried starts again and is announced again; error_code is still the previous attempt's until the job finishes. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "object_id",
+ "type",
+ "status",
+ "attempts",
+ "max_attempts",
+ "error_code",
+ "cancel_requested",
+ "has_result",
+ "run_after",
+ "decided_at",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "finished_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "object_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ },
+ "status": {
+ "const": "running",
+ "description": "started carries the job's state, running."
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Attempts started so far, this one included."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "error_code": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "Its owner asked a running job to stop."
+ },
+ "has_result": {
+ "const": false,
+ "description": "A running job has no result yet."
+ },
+ "run_after": {
+ "type": "null",
+ "description": "Cleared when the job is claimed."
+ },
+ "decided_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
+ },
+ "finished_at": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ "media.job.retrying": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.job.retrying.v1.json",
+ "title": "MediaJobRetryingPayload",
+ "description": "media.job.retrying v1 (OpenVibe.Media server/jobs/queue.js fail with a retry; server/events.js recordJob). An attempt failed and the job is queued again (status queued) after a backoff (run_after); error_code says why when the handler gave a code. A running job nobody holds any more (the service restarted, or the worker stopped renewing its lease) is retried this way with error_code interrupted while it has attempts left. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority low, actor service:media.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "object_id",
+ "type",
+ "status",
+ "attempts",
+ "max_attempts",
+ "error_code",
+ "cancel_requested",
+ "has_result",
+ "run_after",
+ "decided_at",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "finished_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "object_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ },
+ "status": {
+ "const": "queued",
+ "description": "retrying carries the job's state, queued."
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Attempts started so far, the failed one included."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "error_code": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "Its owner asked a running job to stop."
+ },
+ "has_result": {
+ "const": false,
+ "description": "A job waiting for its retry has no result."
+ },
+ "run_after": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "Not before this time (the retry backoff)."
+ },
+ "decided_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
+ },
+ "finished_at": {
+ "type": "null"
+ }
+ }
+ }
+ },
+ "media.job.succeeded": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.job.succeeded.v1.json",
+ "title": "MediaJobSucceededPayload",
+ "description": "media.job.succeeded v1 (OpenVibe.Media server/jobs/queue.js succeed; server/events.js recordJob). The job finished. Its result (thumbnail.regenerate { url, kind, id }, object.split { source_id, parts, … }, object.remux { source_id, object_id, … }, invariant.scan { thresholds, counts, … }) is not in the event: has_result says whether there is one to GET. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "object_id",
+ "type",
+ "status",
+ "attempts",
+ "max_attempts",
+ "error_code",
+ "cancel_requested",
+ "has_result",
+ "run_after",
+ "decided_at",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "finished_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "object_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ },
+ "status": {
+ "const": "succeeded"
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Attempts started, the successful one included."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "error_code": {
+ "type": "null",
+ "description": "A success clears the last failure."
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "Its owner asked a running job to stop."
+ },
+ "has_result": {
+ "type": "boolean",
+ "description": "The job holds a result. The result itself is never in the event: GET the job with a tenant token for it."
+ },
+ "run_after": {
+ "type": "null"
+ },
+ "decided_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "When its owner approved it, or cancelled it while it was proposed or queued; null otherwise."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$",
+ "description": "The first attempt's start."
+ },
+ "finished_at": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,3})?Z$"
+ }
+ }
+ }
+ },
+ "media.job.failed": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.job.failed.v1.json",
+ "title": "MediaJobFailedPayload",
+ "description": "media.job.failed v1 (OpenVibe.Media server/jobs/queue.js fail; server/events.js recordJob). The job failed for good: a permanent error, or its last attempt (attempts = max_attempts) failed; error_code says why when the handler gave a code (GET the job for the message). A cancelled job is not a failure and is announced as media.job.cancelled. Emitted in the SQLite transaction that records the state change, so the event exists if and only if the change committed; it goes to OpenVibe.Events only (no app webhook). The payload is an event projection of the job (queue.jobEvent): its identity, state, counters, stable error_code, times and has_result. Deliberately left out, because events travel beyond the tenant: the tenant's params (object.split keeps whatever the caller sent), the caller's idempotency_key, the free-text error (raw handler or ffmpeg output), the result (thumbnail.regenerate's is the thumbnail URL, of a private VOD too), created_by/decided_by and owner_user_id (tenant-local user ids), and the handler's checkpoint, lease and request hash. GET the job with a tenant token for params/result/error (GET /api/v2/:app/jobs/:id with a token for app_id; has_result says whether there is a result to fetch). Times are ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.sssZ) or null. Jobs of developer-project sandbox tenants are not announced. Envelope: subject { type: job, id: <id> }, visibility internal, priority important, actor service:media.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "object_id",
+ "type",
+ "status",
+ "attempts",
+ "max_attempts",
+ "error_code",
+ "cancel_requested",
+ "has_result",
+ "run_after",
+ "decided_at",
+ "created_at",
+ "updated_at",
+ "started_at",
+ "finished_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mjob_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The job. GET /api/v2/:app/jobs/:id with a token for app_id answers the rest of it."
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the job: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "object_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 200,
+ "description": "The object the job works on; null for a tenant-wide job (invariant.scan)."
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9]*\\.[a-z0-9_.]+$",
+ "maxLength": 80,
+ "description": "Job type: thumbnail.regenerate, invariant.scan, object.split or object.remux."
+ },
+ "status": {
+ "const": "failed"
+ },
+ "attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Attempts started, the last failed one included."
+ },
+ "max_attempts": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "error_code": {
+ "type": [
+ "string",
+ "null"
],
],
"description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
"description": "Stable code of the last failure (media_unavailable, interrupted, cancelled, media.job.invalid…), or null when there was none or the handler gave no code. Never the message: GET the job for it."
},
},
@@ -14248,337 +15914,726 @@
@@ -14248,337 +15914,726 @@
"maxLength": 2048,
"maxLength": 2048,
"description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."
"description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."
},
},
- "previous_status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed"
+ "previous_status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed"
+ ],
+ "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."
+ },
+ "reason": {
+ "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",
+ "anyOf": [
+ {
+ "enum": [
+ "expiry",
+ "evidence",
+ "report",
+ "decay",
+ "staff"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^service:[\\s\\S]+$",
+ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ }
+ ]
+ }
+ }
+ }
+ },
+ "coupons.coupon.disabled": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",
+ "title": "CouponsCouponDisabledPayload",
+ "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "status",
+ "confidence",
+ "review_state",
+ "expires_at",
+ "canonical_url",
+ "previous_status",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "status": {
+ "const": "disabled"
+ },
+ "confidence": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."
+ },
+ "review_state": {
+ "enum": [
+ "pending",
+ "published"
+ ]
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Known expiry (UTC); null = expiry unknown."
+ },
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."
+ },
+ "previous_status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed",
+ "expired"
+ ],
+ "description": "Status before it was disabled."
+ },
+ "reason": {
+ "description": "source_removed, staff, or service:<client>[:<note>].",
+ "anyOf": [
+ {
+ "enum": [
+ "source_removed",
+ "staff"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^service:[\\s\\S]+$",
+ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ }
+ ]
+ }
+ }
+ }
+ },
+ "coupons.report.created": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",
+ "title": "CouponsReportCreatedPayload",
+ "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "outcome",
+ "channel",
+ "day"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant of the reported code."
+ },
+ "outcome": {
+ "enum": [
+ "worked",
+ "failed"
+ ]
+ },
+ "reason": {
+ "enum": [
+ "invalid",
+ "expired",
+ "min_spend_not_met",
+ "not_eligible",
+ "other"
+ ],
+ "description": "Why a failed code failed, when the reporter said. Never on a worked report."
+ },
+ "channel": {
+ "enum": [
+ "site",
+ "extension",
+ "api"
+ ],
+ "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."
+ },
+ "day": {
+ "type": "string",
+ "format": "date",
+ "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."
+ }
+ },
+ "if": {
+ "properties": {
+ "outcome": {
+ "const": "worked"
+ }
+ },
+ "required": [
+ "outcome"
+ ]
+ },
+ "then": {
+ "not": {
+ "required": [
+ "reason"
+ ]
+ }
+ }
+ }
+ },
+ "coupons.confidence.changed": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",
+ "title": "CouponsConfidenceChangedPayload",
+ "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "from",
+ "to",
+ "status"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "from": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Confidence before; null = no counted report."
+ },
+ "to": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Confidence after; null = no counted report left in the window."
+ },
+ "status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed",
+ "expired",
+ "disabled"
+ ],
+ "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."
+ }
+ }
+ }
+ },
+ "coupons.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",
+ "title": "CouponsIndexDocumentUpsertedPayload",
+ "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner",
+ "type",
+ "deleted"
],
],
- "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."
- },
- "reason": {
- "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",
- "anyOf": [
- {
+ "properties": {
+ "owner": {
+ "const": "coupons"
+ },
+ "type": {
"enum": [
"enum": [
- "expiry",
- "evidence",
- "report",
- "decay",
- "staff"
+ "coupon",
+ "merchant"
]
]
},
},
- {
+ "id": {
"type": "string",
"type": "string",
- "pattern": "^service:[\\s\\S]+$",
- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "deleted": {
+ "const": false
}
}
+ }
+ }
+ ]
+ }
+ },
+ "coupons.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",
+ "title": "CouponsIndexDocumentDeletedPayload",
+ "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "enum": [
+ "coupon",
+ "merchant"
]
]
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
}
}
}
}
}
}
},
},
- "coupons.coupon.disabled": {
+ "deals.offer.created": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "deals",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",
- "title": "CouponsCouponDisabledPayload",
- "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",
+ "title": "DealsOfferCreatedPayload",
+ "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
+ "offer_id",
+ "slug",
+ "url",
+ "title",
"status",
"status",
- "confidence",
- "review_state",
- "expires_at",
- "canonical_url",
- "previous_status",
- "reason"
+ "store",
+ "product_id",
+ "latest_observation",
+ "freshness",
+ "indexability"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
+ "offer_id": {
"type": "string",
"type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer id."
+ },
+ "slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 80,
+ "description": "Page slug: the slugified title plus the last six characters of the id."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
},
},
"status": {
"status": {
- "const": "disabled"
+ "const": "active",
+ "description": "A new offer is always active."
},
},
- "confidence": {
+ "store": {
"type": [
"type": [
- "number",
+ "string",
"null"
"null"
],
],
- "minimum": 0,
- "maximum": 1,
- "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."
- },
- "review_state": {
- "enum": [
- "pending",
- "published"
- ]
+ "minLength": 1,
+ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
},
},
- "expires_at": {
+ "product_id": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "format": "date-time",
- "description": "Known expiry (UTC); null = expiry unknown."
+ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Deals product the offer is attached to, if any."
},
},
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."
+ "latest_observation": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "price",
+ "currency",
+ "availability",
+ "observed_at"
+ ],
+ "additionalProperties": false,
+ "description": "The newest observation across the offer's merge group; null only when the group has none.",
+ "properties": {
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Decimal text as stated; null when no price was stated (never 0)."
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217; never assumed."
+ },
+ "availability": {
+ "enum": [
+ "in_stock",
+ "out_of_stock",
+ "preorder",
+ "discontinued",
+ "limited",
+ "sold_out",
+ "online_only",
+ "in_store_only",
+ null
+ ]
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
},
},
- "previous_status": {
+ "freshness": {
"enum": [
"enum": [
- "unknown",
- "reported_working",
- "reported_failed",
- "expired"
+ "fresh",
+ "stale",
+ "unobserved"
],
],
- "description": "Status before it was disabled."
+ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
},
},
- "reason": {
- "description": "source_removed, staff, or service:<client>[:<note>].",
- "anyOf": [
- {
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
+ "properties": {
+ "decision": {
"enum": [
"enum": [
- "source_removed",
- "staff"
+ "index",
+ "noindex"
]
]
},
},
- {
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ },
+ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ }
+ }
+ },
+ "imported_from": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",
+ "properties": {
+ "service": {
+ "const": "sources"
+ },
+ "type": {
+ "const": "item"
+ },
+ "id": {
"type": "string",
"type": "string",
- "pattern": "^service:[\\s\\S]+$",
- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"
}
}
- ]
+ }
}
}
}
}
}
}
},
},
- "coupons.report.created": {
+ "deals.offer.updated": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "deals",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",
- "title": "CouponsReportCreatedPayload",
- "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",
+ "title": "DealsOfferUpdatedPayload",
+ "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "outcome",
- "channel",
- "day"
+ "offer_id",
+ "slug",
+ "url",
+ "title",
+ "status",
+ "store",
+ "product_id",
+ "latest_observation",
+ "freshness",
+ "indexability",
+ "changed"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
+ "offer_id": {
"type": "string",
"type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant of the reported code."
- },
- "outcome": {
- "enum": [
- "worked",
- "failed"
- ]
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer id."
},
},
- "reason": {
- "enum": [
- "invalid",
- "expired",
- "min_spend_not_met",
- "not_eligible",
- "other"
- ],
- "description": "Why a failed code failed, when the reporter said. Never on a worked report."
+ "slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 80,
+ "description": "Page slug: the slugified title plus the last six characters of the id."
},
},
- "channel": {
- "enum": [
- "site",
- "extension",
- "api"
- ],
- "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical offer page URL on Deals (/d/<slug>)."
},
},
- "day": {
+ "title": {
"type": "string",
"type": "string",
- "format": "date",
- "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."
- }
- },
- "if": {
- "properties": {
- "outcome": {
- "const": "worked"
- }
+ "minLength": 1,
+ "maxLength": 200
},
},
- "required": [
- "outcome"
- ]
- },
- "then": {
- "not": {
- "required": [
- "reason"
+ "status": {
+ "enum": [
+ "active",
+ "expired",
+ "disabled"
]
]
- }
- }
- }
- },
- "coupons.confidence.changed": {
- "version": "1.0.0",
- "owner": "coupons",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",
- "title": "CouponsConfidenceChangedPayload",
- "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
- "type": "object",
- "required": [
- "merchant_id",
- "from",
- "to",
- "status"
- ],
- "additionalProperties": false,
- "properties": {
- "merchant_id": {
- "type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
},
},
- "from": {
+ "store": {
"type": [
"type": [
- "number",
+ "string",
"null"
"null"
],
],
- "minimum": 0,
- "maximum": 1,
- "description": "Confidence before; null = no counted report."
+ "minLength": 1,
+ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
},
},
- "to": {
+ "product_id": {
"type": [
"type": [
- "number",
+ "string",
"null"
"null"
],
],
- "minimum": 0,
- "maximum": 1,
- "description": "Confidence after; null = no counted report left in the window."
+ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Deals product the offer is attached to, if any."
},
},
- "status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed",
- "expired",
- "disabled"
+ "latest_observation": {
+ "type": [
+ "object",
+ "null"
],
],
- "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."
- }
- }
- }
- },
- "coupons.index_document.upserted": {
- "version": "1.0.0",
- "owner": "coupons",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",
- "title": "CouponsIndexDocumentUpsertedPayload",
- "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
- },
- {
- "type": "object",
"required": [
"required": [
- "owner",
- "type",
- "deleted"
+ "price",
+ "currency",
+ "availability",
+ "observed_at"
],
],
+ "additionalProperties": false,
+ "description": "The newest observation across the offer's merge group; null only when the group has none.",
"properties": {
"properties": {
- "owner": {
- "const": "coupons"
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Decimal text as stated; null when no price was stated (never 0)."
},
},
- "type": {
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217; never assumed."
+ },
+ "availability": {
"enum": [
"enum": [
- "coupon",
- "merchant"
+ "in_stock",
+ "out_of_stock",
+ "preorder",
+ "discontinued",
+ "limited",
+ "sold_out",
+ "online_only",
+ "in_store_only",
+ null
]
]
},
},
- "id": {
+ "observed_at": {
"type": "string",
"type": "string",
- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ "format": "date-time"
+ }
+ }
+ },
+ "freshness": {
+ "enum": [
+ "fresh",
+ "stale",
+ "unobserved"
+ ],
+ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
},
},
- "deleted": {
- "const": false
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ },
+ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
}
}
}
}
- }
- ]
- }
- },
- "coupons.index_document.deleted": {
- "version": "1.0.0",
- "owner": "coupons",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",
- "title": "CouponsIndexDocumentDeletedPayload",
- "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",
- "type": "object",
- "required": [
- "type",
- "id",
- "revision"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
+ },
+ "changed": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "title",
+ "description",
+ "category",
+ "expires_at",
+ "product_id",
+ "ai_summary",
+ "review_state",
+ "text_origin",
+ "observation",
+ "sources",
+ "status",
+ "merged",
+ "unmerged"
+ ]
+ },
+ "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."
+ },
+ "observation_id": {
+ "type": "string",
+ "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."
+ },
+ "source_removed": {
+ "type": "string",
+ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."
+ },
+ "moderation": {
"enum": [
"enum": [
- "coupon",
- "merchant"
- ]
+ "disabled",
+ "enabled"
+ ],
+ "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."
},
},
- "id": {
+ "merged_offer_id": {
"type": "string",
"type": "string",
- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [merged]: the duplicate now merged into this offer."
},
},
- "revision": {
- "type": "integer",
- "minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ "unmerged_offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [unmerged], on the former target: the offer split back out."
+ },
+ "unmerged_from": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [unmerged], on the restored offer: the offer it had been merged into."
}
}
}
}
}
}
},
},
- "deals.offer.created": {
+ "deals.offer.expired": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "deals",
"owner": "deals",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",
- "title": "DealsOfferCreatedPayload",
- "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",
+ "title": "DealsOfferExpiredPayload",
+ "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",
"type": "object",
"type": "object",
"required": [
"required": [
"offer_id",
"offer_id",
@@ -14590,7 +16645,8 @@
@@ -14590,7 +16645,8 @@
"product_id",
"product_id",
"latest_observation",
"latest_observation",
"freshness",
"freshness",
- "indexability"
+ "indexability",
+ "reason"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
@@ -14618,8 +16674,7 @@
@@ -14618,8 +16674,7 @@
"maxLength": 200
"maxLength": 200
},
},
"status": {
"status": {
- "const": "active",
- "description": "A new offer is always active."
+ "const": "expired"
},
},
"store": {
"store": {
"type": [
"type": [
@@ -14721,2642 +16776,3835 @@
@@ -14721,2642 +16776,3835 @@
}
}
}
}
},
},
- "imported_from": {
+ "reason": {
+ "type": "string",
+ "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",
+ "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."
+ }
+ }
+ }
+ },
+ "deals.vote.changed": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",
+ "title": "DealsVoteChangedPayload",
+ "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",
+ "type": "object",
+ "required": [
+ "offer_id",
+ "listing_id",
+ "value",
+ "previous",
+ "weight",
+ "votes",
+ "hotness"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer of the merge group; the vote row is stored on it."
+ },
+ "listing_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The offer the request addressed: the root, or an offer merged into it."
+ },
+ "value": {
+ "enum": [
+ 1,
+ -1,
+ 0
+ ],
+ "description": "The new effective vote; 0 = removed."
+ },
+ "previous": {
+ "enum": [
+ 1,
+ -1,
+ 0
+ ],
+ "description": "The effective vote before; always different from value."
+ },
+ "weight": {
+ "type": "number",
+ "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."
+ },
+ "votes": {
"type": "object",
"type": "object",
"required": [
"required": [
- "service",
- "type",
- "id"
+ "up",
+ "down",
+ "up_weight",
+ "down_weight"
],
],
"additionalProperties": false,
"additionalProperties": false,
- "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",
+ "description": "Tally of effective votes across the merge group after the change (a voter counts once).",
"properties": {
"properties": {
- "service": {
- "const": "sources"
+ "up": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "down": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "up_weight": {
+ "type": "number",
+ "description": "Sum of the up voters' weights, 4 decimals."
+ },
+ "down_weight": {
+ "type": "number",
+ "description": "Sum of the down voters' weights, 4 decimals."
+ }
+ }
+ },
+ "hotness": {
+ "type": "object",
+ "required": [
+ "formula",
+ "hot",
+ "score"
+ ],
+ "additionalProperties": false,
+ "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",
+ "properties": {
+ "formula": {
+ "type": "string",
+ "pattern": "^hot@[0-9]+$",
+ "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."
+ },
+ "hot": {
+ "type": "number"
+ },
+ "score": {
+ "type": "number",
+ "description": "up_weight - down_weight, 4 decimals."
+ }
+ }
+ }
+ }
+ }
+ },
+ "deals.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",
+ "title": "DealsIndexDocumentUpsertedPayload",
+ "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner",
+ "type",
+ "deleted"
+ ],
+ "properties": {
+ "owner": {
+ "const": "deals"
},
},
"type": {
"type": {
- "const": "item"
+ "enum": [
+ "offer",
+ "product"
+ ]
},
},
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "deleted": {
+ "const": false
}
}
}
}
}
}
+ ]
+ }
+ },
+ "deals.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",
+ "title": "DealsIndexDocumentDeletedPayload",
+ "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "offer"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ }
}
}
}
}
},
},
- "deals.offer.updated": {
+ "trade.observation.created": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "trade",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",
- "title": "DealsOfferUpdatedPayload",
- "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",
+ "title": "TradeObservationCreatedPayload",
+ "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "slug",
- "url",
- "title",
- "status",
- "store",
- "product_id",
- "latest_observation",
- "freshness",
- "indexability",
- "changed"
+ "instrument",
+ "id",
+ "metric",
+ "value",
+ "unit",
+ "currency",
+ "period",
+ "observed_at",
+ "retrieved_at",
+ "recorded_at",
+ "max_age_sec",
+ "source"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer id."
+ "instrument": {
+ "type": "object",
+ "required": [
+ "id",
+ "symbol"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "symbol": {
+ "type": "string",
+ "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"
+ }
+ }
},
},
- "slug": {
+ "id": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 80,
- "description": "Page slug: the slugified title plus the last six characters of the id."
+ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
},
},
- "url": {
+ "metric": {
"type": "string",
"type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",
+ "description": "e.g. price.close, volume, us-gaap:Revenues."
},
},
- "title": {
+ "value": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200
+ "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",
+ "maxLength": 64,
+ "description": "The decimal exactly as the source stated it (text, never rounded)."
},
},
- "status": {
- "enum": [
- "active",
- "expired",
- "disabled"
- ]
+ "unit": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",
+ "description": "e.g. USD, shares, USD/shares."
},
},
- "store": {
+ "currency": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "minLength": 1,
- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217 when the value is monetary; null otherwise."
},
},
- "product_id": {
+ "period": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Deals product the offer is attached to, if any."
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."
},
},
- "latest_observation": {
+ "observed_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the value was true, per the source."
+ },
+ "retrieved_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the source was fetched."
+ },
+ "recorded_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When Trade recorded the row."
+ },
+ "max_age_sec": {
"type": [
"type": [
- "object",
+ "integer",
"null"
"null"
],
],
+ "minimum": 60,
+ "maximum": 315360000,
+ "description": "The value's own maximum age, when the recorder gave one."
+ },
+ "source": {
+ "type": "object",
"required": [
"required": [
- "price",
- "currency",
- "availability",
- "observed_at"
+ "key",
+ "item_id",
+ "url",
+ "ref"
],
],
"additionalProperties": false,
"additionalProperties": false,
- "description": "The newest observation across the offer's merge group; null only when the group has none.",
"properties": {
"properties": {
- "price": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
+ "description": "OpenVibe.Sources source key."
+ },
+ "item_id": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 40,
- "description": "Decimal text as stated; null when no price was stated (never 0)."
+ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The Sources item the value came from, when there is one."
},
},
- "currency": {
+ "url": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217; never assumed."
- },
- "availability": {
- "enum": [
- "in_stock",
- "out_of_stock",
- "preorder",
- "discontinued",
- "limited",
- "sold_out",
- "online_only",
- "in_store_only",
- null
- ]
+ "pattern": "^https?://",
+ "description": "The source's page for the value (absolute http(s) URL, normalised)."
},
},
- "observed_at": {
+ "ref": {
"type": "string",
"type": "string",
- "format": "date-time"
- }
- }
- },
- "freshness": {
- "enum": [
- "fresh",
- "stale",
- "unobserved"
- ],
- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
- },
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
- },
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- },
- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ "minLength": 1,
+ "maxLength": 300,
+ "description": "The source's own reference for this datum; unique per source_key."
}
}
}
}
- },
- "changed": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": {
- "enum": [
- "title",
- "description",
- "category",
- "expires_at",
- "product_id",
- "ai_summary",
- "review_state",
- "text_origin",
- "observation",
- "sources",
- "status",
- "merged",
- "unmerged"
- ]
- },
- "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."
- },
- "observation_id": {
+ }
+ }
+ }
+ },
+ "trade.source.stale": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",
+ "title": "TradeSourceStalePayload",
+ "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
+ "type": "object",
+ "required": [
+ "source_key",
+ "stale",
+ "stale_since",
+ "last_success_at",
+ "stale_after_sec",
+ "upstream_status",
+ "evaluated_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "source_key": {
"type": "string",
"type": "string",
- "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
+ "description": "OpenVibe.Sources source key."
},
},
- "source_removed": {
+ "stale": {
+ "const": true
+ },
+ "stale_since": {
"type": "string",
"type": "string",
- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."
+ "format": "date-time",
+ "description": "Last success + window, or when Trade first saw the source if it never succeeded."
},
},
- "moderation": {
- "enum": [
- "disabled",
- "enabled"
+ "last_success_at": {
+ "type": [
+ "string",
+ "null"
],
],
- "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."
+ "format": "date-time",
+ "description": "Last known successful fetch; null when none is known."
},
},
- "merged_offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [merged]: the duplicate now merged into this offer."
+ "stale_after_sec": {
+ "type": "integer",
+ "description": "The staleness window applied, in seconds."
},
},
- "unmerged_offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [unmerged], on the former target: the offer split back out."
+ "upstream_status": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
},
},
- "unmerged_from": {
+ "evaluated_at": {
"type": "string",
"type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [unmerged], on the restored offer: the offer it had been merged into."
+ "format": "date-time"
}
}
}
}
}
}
},
},
- "deals.offer.expired": {
+ "trade.source.recovered": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "trade",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",
- "title": "DealsOfferExpiredPayload",
- "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",
+ "title": "TradeSourceRecoveredPayload",
+ "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "slug",
- "url",
- "title",
- "status",
- "store",
- "product_id",
- "latest_observation",
- "freshness",
- "indexability",
- "reason"
+ "source_key",
+ "stale",
+ "stale_since",
+ "last_success_at",
+ "stale_after_sec",
+ "upstream_status",
+ "evaluated_at"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
+ "source_key": {
"type": "string",
"type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer id."
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
+ "description": "OpenVibe.Sources source key."
},
},
- "slug": {
- "type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 80,
- "description": "Page slug: the slugified title plus the last six characters of the id."
+ "stale": {
+ "const": false
},
},
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ "stale_since": {
+ "type": "null",
+ "description": "Always null: the source is fresh."
},
},
- "title": {
+ "last_success_at": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200
+ "format": "date-time",
+ "description": "The successful fetch that made it fresh (or a later one)."
},
},
- "status": {
- "const": "expired"
+ "stale_after_sec": {
+ "type": "integer",
+ "description": "The staleness window applied, in seconds."
},
},
- "store": {
+ "upstream_status": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "minLength": 1,
- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
},
},
- "product_id": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Deals product the offer is attached to, if any."
+ "evaluated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "trade.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",
+ "title": "TradeIndexDocumentUpsertedPayload",
+ "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
},
},
- "latest_observation": {
- "type": [
- "object",
- "null"
- ],
+ {
+ "type": "object",
"required": [
"required": [
- "price",
- "currency",
- "availability",
- "observed_at"
+ "owner",
+ "type",
+ "deleted"
],
],
- "additionalProperties": false,
- "description": "The newest observation across the offer's merge group; null only when the group has none.",
"properties": {
"properties": {
- "price": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 40,
- "description": "Decimal text as stated; null when no price was stated (never 0)."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217; never assumed."
+ "owner": {
+ "const": "trade"
},
},
- "availability": {
- "enum": [
- "in_stock",
- "out_of_stock",
- "preorder",
- "discontinued",
- "limited",
- "sold_out",
- "online_only",
- "in_store_only",
- null
- ]
+ "type": {
+ "const": "instrument"
},
},
- "observed_at": {
+ "id": {
"type": "string",
"type": "string",
- "format": "date-time"
- }
- }
- },
- "freshness": {
- "enum": [
- "fresh",
- "stale",
- "unobserved"
- ],
- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
- },
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
},
},
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- },
- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ "deleted": {
+ "const": false
}
}
}
}
+ }
+ ]
+ }
+ },
+ "trade.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",
+ "title": "TradeIndexDocumentDeletedPayload",
+ "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "instrument"
},
},
- "reason": {
+ "id": {
"type": "string",
"type": "string",
- "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",
- "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
}
}
}
}
}
}
},
},
- "deals.vote.changed": {
+ "games.player.joined": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",
- "title": "DealsVoteChangedPayload",
- "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",
+ "title": "GamesPlayerJoinedPayload",
+ "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "listing_id",
- "value",
- "previous",
- "weight",
- "votes",
- "hotness"
+ "player",
+ "world",
+ "restored"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer of the merge group; the vote row is stored on it."
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
+ }
+ ]
},
},
- "listing_id": {
+ "world": {
"type": "string",
"type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The offer the request addressed: the root, or an offer merged into it."
- },
- "value": {
- "enum": [
- 1,
- -1,
- 0
- ],
- "description": "The new effective vote; 0 = removed."
- },
- "previous": {
- "enum": [
- 1,
- -1,
- 0
- ],
- "description": "The effective vote before; always different from value."
- },
- "weight": {
- "type": "number",
- "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
},
},
- "votes": {
- "type": "object",
- "required": [
- "up",
- "down",
- "up_weight",
- "down_weight"
- ],
- "additionalProperties": false,
- "description": "Tally of effective votes across the merge group after the change (a voter counts once).",
- "properties": {
- "up": {
- "type": "integer",
- "minimum": 0
- },
- "down": {
- "type": "integer",
- "minimum": 0
- },
- "up_weight": {
- "type": "number",
- "description": "Sum of the up voters' weights, 4 decimals."
+ "restored": {
+ "type": "boolean",
+ "description": "The character was loaded from the database (false: created by this join)."
+ }
+ }
+ }
+ },
+ "games.player.left": {
+ "version": "1.0.0",
+ "owner": "games",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",
+ "title": "GamesPlayerLeftPayload",
+ "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "type": "object",
+ "required": [
+ "player",
+ "world",
+ "session_seconds"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
},
},
- "down_weight": {
- "type": "number",
- "description": "Sum of the down voters' weights, 4 decimals."
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
}
}
- }
+ ]
},
},
- "hotness": {
- "type": "object",
- "required": [
- "formula",
- "hot",
- "score"
- ],
- "additionalProperties": false,
- "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",
- "properties": {
- "formula": {
- "type": "string",
- "pattern": "^hot@[0-9]+$",
- "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."
- },
- "hot": {
- "type": "number"
- },
- "score": {
- "type": "number",
- "description": "up_weight - down_weight, 4 decimals."
- }
- }
- }
- }
- }
- },
- "deals.index_document.upserted": {
- "version": "1.0.0",
- "owner": "deals",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",
- "title": "DealsIndexDocumentUpsertedPayload",
- "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
+ "world": {
+ "type": "string",
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
},
},
- {
- "type": "object",
- "required": [
- "owner",
- "type",
- "deleted"
- ],
- "properties": {
- "owner": {
- "const": "deals"
- },
- "type": {
- "enum": [
- "offer",
- "product"
- ]
- },
- "id": {
- "type": "string",
- "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "deleted": {
- "const": false
- }
- }
+ "session_seconds": {
+ "type": "integer",
+ "minimum": 0
}
}
- ]
+ }
}
}
},
},
- "deals.index_document.deleted": {
+ "games.skill.leveled": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",
- "title": "DealsIndexDocumentDeletedPayload",
- "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",
+ "title": "GamesSkillLeveledPayload",
+ "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "type",
- "id",
- "revision"
+ "player",
+ "world",
+ "skill",
+ "level",
+ "previous_level"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "type": {
- "const": "offer"
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
+ }
+ ]
},
},
- "id": {
+ "world": {
"type": "string",
"type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
},
},
- "revision": {
+ "skill": {
+ "type": "string",
+ "pattern": "^[a-z0-9_]+$",
+ "description": "Content skill id (e.g. mining, woodcutting)."
+ },
+ "level": {
+ "type": "integer",
+ "minimum": 2,
+ "description": "Level now persisted; always greater than previous_level."
+ },
+ "previous_level": {
"type": "integer",
"type": "integer",
"minimum": 1,
"minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ "description": "Level at the previous baseline (1 when the skill had none)."
}
}
}
}
}
}
},
},
- "trade.observation.created": {
+ "games.blueprint.unlocked": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",
- "title": "TradeObservationCreatedPayload",
- "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",
+ "title": "GamesBlueprintUnlockedPayload",
+ "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "instrument",
- "id",
- "metric",
- "value",
- "unit",
- "currency",
- "period",
- "observed_at",
- "retrieved_at",
- "recorded_at",
- "max_age_sec",
- "source"
+ "player",
+ "world",
+ "recipe"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "instrument": {
- "type": "object",
- "required": [
- "id",
- "symbol"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "symbol": {
- "type": "string",
- "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"
- }
- }
- },
- "id": {
- "type": "string",
- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "metric": {
- "type": "string",
- "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",
- "description": "e.g. price.close, volume, us-gaap:Revenues."
- },
- "value": {
- "type": "string",
- "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",
- "maxLength": 64,
- "description": "The decimal exactly as the source stated it (text, never rounded)."
- },
- "unit": {
- "type": "string",
- "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",
- "description": "e.g. USD, shares, USD/shares."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217 when the value is monetary; null otherwise."
- },
- "period": {
- "type": [
- "string",
- "null"
- ],
- "minLength": 1,
- "maxLength": 40,
- "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."
- },
- "observed_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the value was true, per the source."
- },
- "retrieved_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the source was fetched."
- },
- "recorded_at": {
- "type": "string",
- "format": "date-time",
- "description": "When Trade recorded the row."
- },
- "max_age_sec": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 60,
- "maximum": 315360000,
- "description": "The value's own maximum age, when the recorder gave one."
- },
- "source": {
- "type": "object",
- "required": [
- "key",
- "item_id",
- "url",
- "ref"
- ],
- "additionalProperties": false,
- "properties": {
- "key": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
- "description": "OpenVibe.Sources source key."
- },
- "item_id": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The Sources item the value came from, when there is one."
- },
- "url": {
- "type": [
- "string",
- "null"
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
],
],
- "pattern": "^https?://",
- "description": "The source's page for the value (absolute http(s) URL, normalised)."
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
},
},
- "ref": {
- "type": "string",
- "minLength": 1,
- "maxLength": 300,
- "description": "The source's own reference for this datum; unique per source_key."
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
}
}
- }
+ ]
+ },
+ "world": {
+ "type": "string",
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ },
+ "recipe": {
+ "type": "string",
+ "pattern": "^[a-z0-9_]+$",
+ "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."
}
}
}
}
}
}
},
},
- "trade.source.stale": {
+ "games.world.saved": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",
- "title": "TradeSourceStalePayload",
- "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",
+ "title": "GamesWorldSavedPayload",
+ "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "source_key",
- "stale",
- "stale_since",
- "last_success_at",
- "stale_after_sec",
- "upstream_status",
- "evaluated_at"
+ "world",
+ "reason",
+ "entities_written",
+ "players_written",
+ "since"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "source_key": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
- "description": "OpenVibe.Sources source key."
- },
- "stale": {
- "const": true
- },
- "stale_since": {
+ "world": {
"type": "string",
"type": "string",
- "format": "date-time",
- "description": "Last success + window, or when Trade first saw the source if it never succeeded."
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
},
},
- "last_success_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "Last known successful fetch; null when none is known."
+ "reason": {
+ "enum": [
+ "checkpoint",
+ "shutdown"
+ ]
},
},
- "stale_after_sec": {
+ "entities_written": {
"type": "integer",
"type": "integer",
- "description": "The staleness window applied, in seconds."
+ "minimum": 0,
+ "description": "World entity rows written since `since`."
},
},
- "upstream_status": {
- "type": [
- "string",
- "null"
- ],
- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
+ "players_written": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Character rows written by flushes since `since`."
},
},
- "evaluated_at": {
+ "since": {
"type": "string",
"type": "string",
- "format": "date-time"
+ "format": "date-time",
+ "description": "Start of the period these totals cover."
}
}
}
}
}
}
},
},
- "trade.source.recovered": {
+ "games.mod.installed": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",
- "title": "TradeSourceRecoveredPayload",
- "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",
+ "title": "GamesModInstalledPayload",
+ "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "source_key",
- "stale",
- "stale_since",
- "last_success_at",
- "stale_after_sec",
- "upstream_status",
- "evaluated_at"
+ "mod",
+ "requested",
+ "granted",
+ "status"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "source_key": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
- "description": "OpenVibe.Sources source key."
- },
- "stale": {
- "const": false
- },
- "stale_since": {
- "type": "null",
- "description": "Always null: the source is fresh."
- },
- "last_success_at": {
- "type": "string",
- "format": "date-time",
- "description": "The successful fetch that made it fresh (or a later one)."
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
+ ],
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
+ }
+ }
},
},
- "stale_after_sec": {
- "type": "integer",
- "description": "The staleness window applied, in seconds."
+ "requested": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "manifest.permissions.capabilities, sorted."
},
},
- "upstream_status": {
- "type": [
- "string",
- "null"
- ],
- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
+ "granted": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "The approved subset of requested, sorted (may be empty)."
},
},
- "evaluated_at": {
- "type": "string",
- "format": "date-time"
+ "status": {
+ "enum": [
+ "enabled",
+ "disabled"
+ ]
}
}
}
}
}
}
},
},
- "trade.index_document.upserted": {
+ "games.mod.enabled": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",
- "title": "TradeIndexDocumentUpsertedPayload",
- "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
- },
- {
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",
+ "title": "GamesModEnabledPayload",
+ "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
+ "type": "object",
+ "required": [
+ "mod"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "mod": {
"type": "object",
"type": "object",
"required": [
"required": [
- "owner",
- "type",
- "deleted"
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
],
],
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "owner": {
- "const": "trade"
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
},
},
- "type": {
- "const": "instrument"
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
},
},
- "id": {
+ "version": {
"type": "string",
"type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
},
},
- "deleted": {
- "const": false
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
}
}
}
}
}
}
- ]
+ }
}
}
},
},
- "trade.index_document.deleted": {
+ "games.mod.disabled": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",
- "title": "TradeIndexDocumentDeletedPayload",
- "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",
+ "title": "GamesModDisabledPayload",
+ "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "type",
- "id",
- "revision"
+ "mod"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "type": {
- "const": "instrument"
- },
- "id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "revision": {
- "type": "integer",
- "minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
+ ],
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
+ }
+ }
}
}
}
}
}
}
},
},
- "games.player.joined": {
+ "games.mod.grants_changed": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "games",
"owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",
- "title": "GamesPlayerJoinedPayload",
- "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",
+ "title": "GamesModGrantsChangedPayload",
+ "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "player",
- "world",
- "restored"
+ "mod",
+ "granted",
+ "revoked"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account",
- "subject"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "network"
- },
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
- ]
- }
- }
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
+ ],
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
},
},
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
],
],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "guest"
- }
- }
+ "description": "Install metadata set at install; never consulted by a grant check."
}
}
- ]
+ }
},
},
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ "granted": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Capabilities newly granted by this change."
},
},
- "restored": {
- "type": "boolean",
- "description": "The character was loaded from the database (false: created by this join)."
+ "revoked": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Capabilities whose grant this change revoked."
}
}
}
}
}
}
},
},
- "games.player.left": {
+ "games.mod.revoked": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "games",
"owner": "games",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",
- "title": "GamesPlayerLeftPayload",
- "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",
+ "title": "GamesModRevokedPayload",
+ "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "player",
- "world",
- "session_seconds"
+ "mod",
+ "revoked_grants"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account",
- "subject"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "network"
- },
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
- ]
- }
- }
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
+ ],
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
},
},
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
],
],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "guest"
- }
- }
+ "description": "Install metadata set at install; never consulted by a grant check."
}
}
+ }
+ },
+ "revoked_grants": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Grants active before the revoke, sorted."
+ },
+ "reason": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ }
+ }
+ }
+ },
+ "media.object.deleted": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",
+ "title": "MediaObjectDeletedPayload",
+ "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
+ "type": "object",
+ "required": [
+ "object_id",
+ "app_id",
+ "kind",
+ "legacy_ref",
+ "deleted_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "object_id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "kind": {
+ "enum": [
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
]
]
},
},
- "world": {
+ "legacy_ref": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300,
+ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
+ },
+ "deleted_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the change committed, ISO 8601 UTC."
+ }
+ }
+ }
+ },
+ "media.object.visibility_changed": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",
+ "title": "MediaObjectVisibilityChangedPayload",
+ "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
+ "type": "object",
+ "required": [
+ "object_id",
+ "app_id",
+ "kind",
+ "legacy_ref",
+ "visibility",
+ "previous_visibility",
+ "changed_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "object_id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "app_id": {
"type": "string",
"type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
},
},
- "session_seconds": {
- "type": "integer",
- "minimum": 0
+ "kind": {
+ "enum": [
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
+ ]
+ },
+ "legacy_ref": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300,
+ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ],
+ "description": "The new value."
+ },
+ "previous_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ],
+ "description": "The value before the change; never equal to visibility."
+ },
+ "changed_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the change committed, ISO 8601 UTC."
}
}
+ },
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "visibility",
+ "previous_visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "const": "public"
+ },
+ "previous_visibility": {
+ "const": "public"
+ }
+ }
+ },
+ {
+ "required": [
+ "visibility",
+ "previous_visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "const": "unlisted"
+ },
+ "previous_visibility": {
+ "const": "unlisted"
+ }
+ }
+ },
+ {
+ "required": [
+ "visibility",
+ "previous_visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "const": "private"
+ },
+ "previous_visibility": {
+ "const": "private"
+ }
+ }
+ }
+ ]
}
}
}
}
},
},
- "games.skill.leveled": {
+ "community.paste.created": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",
- "title": "GamesSkillLeveledPayload",
- "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",
+ "title": "CommunityPasteCreatedPayload",
+ "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
"type": "object",
"type": "object",
"required": [
"required": [
- "player",
- "world",
- "skill",
- "level",
- "previous_level"
+ "paste_id",
+ "type",
+ "origin",
+ "visibility",
+ "owner",
+ "url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account",
- "subject"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "network"
- },
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
- ]
- }
- }
- },
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "guest"
- }
- }
+ "paste_id": {
+ "type": "string",
+ "pattern": "^[a-z0-9-]{3,64}$",
+ "description": "The paste's slug."
+ },
+ "type": {
+ "enum": [
+ "paste",
+ "screenshot"
+ ]
+ },
+ "origin": {
+ "enum": [
+ "user",
+ "ai",
+ "import"
+ ],
+ "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private",
+ "hidden",
+ "deleted"
+ ]
+ },
+ "owner": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
}
}
]
]
},
},
- "world": {
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
+ }
+ }
+ }
+ },
+ "community.paste.updated": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",
+ "title": "CommunityPasteUpdatedPayload",
+ "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "type": "object",
+ "required": [
+ "paste_id",
+ "changed",
+ "visibility",
+ "revision",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "paste_id": {
"type": "string",
"type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ "pattern": "^[a-z0-9-]{3,64}$"
},
},
- "skill": {
- "type": "string",
- "pattern": "^[a-z0-9_]+$",
- "description": "Content skill id (e.g. mining, woodcutting)."
+ "changed": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "title",
+ "content",
+ "language",
+ "visibility",
+ "is_nsfw",
+ "pinned"
+ ]
+ }
},
},
- "level": {
- "type": "integer",
- "minimum": 2,
- "description": "Level now persisted; always greater than previous_level."
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private",
+ "hidden",
+ "deleted"
+ ]
},
},
- "previous_level": {
+ "revision": {
"type": "integer",
"type": "integer",
- "minimum": 1,
- "description": "Level at the previous baseline (1 when the skill had none)."
+ "minimum": 1
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
}
}
}
}
}
}
},
},
- "games.blueprint.unlocked": {
+ "community.paste.deleted": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",
- "title": "GamesBlueprintUnlockedPayload",
- "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",
+ "title": "CommunityPasteDeletedPayload",
+ "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
"type": "object",
"type": "object",
"required": [
"required": [
- "player",
- "world",
- "recipe"
+ "paste_id"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "paste_id": {
+ "type": "string",
+ "pattern": "^[a-z0-9-]{3,64}$"
+ }
+ }
+ }
+ },
+ "community.thread.created": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",
+ "title": "CommunityThreadCreatedPayload",
+ "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "type": "object",
+ "required": [
+ "thread_id",
+ "space",
+ "author",
+ "visibility",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "thread_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "space": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ },
+ "author": {
"oneOf": [
"oneOf": [
{
{
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account",
- "subject"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "network"
- },
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
- ]
- }
- }
+ "$ref": "../../identity/subject-ref.v1.json"
},
},
{
{
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "guest"
- }
- }
+ "type": "null"
}
}
]
]
},
},
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ "visibility": {
+ "enum": [
+ "public",
+ "members",
+ "hidden"
+ ]
},
},
- "recipe": {
- "type": "string",
- "pattern": "^[a-z0-9_]+$",
- "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
}
}
}
}
}
}
},
},
- "games.world.saved": {
+ "community.post.created": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",
- "title": "GamesWorldSavedPayload",
- "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",
+ "title": "CommunityPostCreatedPayload",
+ "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
"type": "object",
"type": "object",
"required": [
"required": [
- "world",
- "reason",
- "entities_written",
- "players_written",
- "since"
+ "post_id",
+ "thread_id",
+ "space",
+ "author",
+ "visibility",
+ "url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
- },
- "reason": {
- "enum": [
- "checkpoint",
- "shutdown"
- ]
- },
- "entities_written": {
+ "post_id": {
"type": "integer",
"type": "integer",
- "minimum": 0,
- "description": "World entity rows written since `since`."
+ "minimum": 1
},
},
- "players_written": {
+ "thread_id": {
"type": "integer",
"type": "integer",
- "minimum": 0,
- "description": "Character rows written by flushes since `since`."
+ "minimum": 1
},
},
- "since": {
+ "space": {
"type": "string",
"type": "string",
- "format": "date-time",
- "description": "Start of the period these totals cover."
+ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ },
+ "author": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "members",
+ "hidden"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
}
}
}
}
}
}
},
},
- "games.mod.installed": {
+ "community.comment.created": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",
- "title": "GamesModInstalledPayload",
- "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",
+ "title": "CommunityCommentCreatedPayload",
+ "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
"type": "object",
"type": "object",
"required": [
"required": [
- "mod",
- "requested",
- "granted",
- "status"
+ "comment_id",
+ "thread_access_id",
+ "ref",
+ "author",
+ "visibility",
+ "url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "mod": {
+ "comment_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "thread_access_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "ref": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "service",
+ "type",
+ "id"
],
],
"additionalProperties": false,
"additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
- "name": {
+ "service": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 80
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
},
},
- "version": {
+ "type": {
"type": "string",
"type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
},
},
- "target": {
+ "id": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
- },
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "minLength": 1,
+ "maxLength": 200
}
}
}
}
},
},
- "requested": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "manifest.permissions.capabilities, sorted."
- },
- "granted": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "The approved subset of requested, sorted (may be empty)."
+ "author": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
},
- "status": {
+ "visibility": {
"enum": [
"enum": [
- "enabled",
- "disabled"
+ "public",
+ "hidden",
+ "locked"
]
]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
}
}
}
}
}
}
},
},
- "games.mod.enabled": {
+ "chat.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "chat",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",
- "title": "GamesModEnabledPayload",
- "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
+ "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",
+ "title": "ChatModerationActionPayload",
+ "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
"type": "object",
"type": "object",
"required": [
"required": [
- "mod"
+ "action_id",
+ "action_type",
+ "scope_type"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "mod": {
- "type": "object",
- "required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
- ],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80
- },
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
- },
- "target": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
- },
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
- }
- }
+ "action_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "action_type": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."
+ },
+ "scope_type": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 32,
+ "description": "site, channel, stream, room, …"
+ },
+ "scope_id": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ]
+ },
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
+ },
+ "actor_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "target_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "details": {
+ "type": "object"
}
}
}
}
}
}
},
},
- "games.mod.disabled": {
+ "chat.send-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "chat",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",
- "title": "GamesModDisabledPayload",
- "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
- "type": "object",
- "required": [
- "mod"
- ],
- "additionalProperties": false,
- "properties": {
- "mod": {
+ "$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
+ "title": "ChatSendRequest",
+ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "type",
+ "message"
],
],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
+ "type": {
+ "const": "chat"
},
},
- "name": {
+ "message": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 80
+ "maxLength": 6000,
+ "description": "The text (trimmed by Chat)."
},
},
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."
},
},
- "target": {
+ "auto_delete_minutes": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."
+ },
+ "voiceChannelId": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ "description": "The voice channel the sender is in, echoed on the broadcast."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ops"
+ ],
+ "properties": {
+ "boot": {
+ "type": "string",
+ "maxLength": 64,
+ "description": "Live's boot id; placeholder message ids are mapped per boot."
},
},
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "ops": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "op"
+ ],
+ "properties": {
+ "seq": {
+ "type": "integer",
+ "description": "Echoed on the result."
+ },
+ "op": {
+ "type": "string",
+ "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."
+ },
+ "args": {
+ "type": "array"
+ },
+ "ref": {
+ "type": "number",
+ "description": "Live's placeholder id for the row this op inserts."
+ },
+ "key": {
+ "type": "string",
+ "maxLength": 80,
+ "description": "Idempotency key: applied once."
+ }
+ }
+ }
}
}
}
}
}
}
- }
+ ]
}
}
},
},
- "games.mod.grants_changed": {
+ "chat.send-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "chat",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",
- "title": "GamesModGrantsChangedPayload",
- "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
- "type": "object",
- "required": [
- "mod",
- "granted",
- "revoked"
- ],
- "additionalProperties": false,
- "properties": {
- "mod": {
+ "$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
+ "title": "ChatSendResult",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "type",
+ "username",
+ "role",
+ "message",
+ "stream_id",
+ "channel_user_id",
+ "is_global",
+ "timestamp"
],
],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
+ "type": {
+ "const": "chat"
+ },
"id": {
"id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
+ "type": "integer",
+ "description": "The stored message id."
},
},
- "name": {
+ "username": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 80
+ "description": "Display name, or the anonymous id."
},
},
- "version": {
+ "core_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The immutable login handle; null for an anonymous sender."
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "role": {
"type": "string",
"type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
+ "description": "The sender's role, anon for an anonymous sender."
},
},
- "target": {
+ "message": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "is_global": {
+ "type": "boolean",
+ "description": "true for homepage global chat (no stream, no channel)."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": "string"
+ },
+ "filtered": {
+ "type": "boolean"
+ },
+ "timestamp": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ "format": "date-time"
},
},
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
+ "auto_delete_at": {
+ "type": [
+ "string",
+ "null"
],
],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "format": "date-time"
+ },
+ "voiceChannelId": {
+ "type": "string"
+ },
+ "reply_to": {
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "message"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string",
+ "description": "The parent's text, cut to 100 characters."
+ }
+ }
+ },
+ "nameFX": {
+ "description": "Cosmetic name effect."
+ },
+ "particleFX": {
+ "description": "Cosmetic particle effect."
+ },
+ "hatFX": {
+ "description": "Cosmetic hat."
+ },
+ "voiceFX": {
+ "description": "Cosmetic TTS voice effect."
+ },
+ "tag": {
+ "description": "The sender's equipped tag."
+ },
+ "vip_badge": {
+ "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "message"
+ ],
+ "properties": {
+ "type": {
+ "enum": [
+ "system",
+ "error"
+ ]
+ },
+ "message": {
+ "type": "string"
}
}
}
}
},
},
- "granted": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "Capabilities newly granted by this change."
- },
- "revoked": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "Capabilities whose grant this change revoked."
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "results"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "properties": {
+ "seq": {
+ "type": "integer"
+ },
+ "ok": {
+ "type": "boolean"
+ },
+ "result": {},
+ "code": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "games.mod.revoked": {
+ "live.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "live",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",
- "title": "GamesModRevokedPayload",
- "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",
+ "title": "LiveModerationActionPayload",
+ "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
"type": "object",
"type": "object",
"required": [
"required": [
- "mod",
- "revoked_grants"
+ "action_id",
+ "action_type",
+ "scope_type"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "mod": {
- "type": "object",
- "required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
- ],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80
- },
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
- },
- "target": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
- },
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
- }
- }
+ "action_id": {
+ "type": "integer",
+ "minimum": 1
},
},
- "revoked_grants": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "Grants active before the revoke, sorted."
+ "action_type": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."
},
},
- "reason": {
+ "scope_type": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 500
+ "maxLength": 32,
+ "description": "site, channel, stream, room, …"
+ },
+ "scope_id": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ]
+ },
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
+ },
+ "actor_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "target_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "details": {
+ "type": "object"
}
}
}
}
}
}
},
},
- "media.object.deleted": {
+ "community.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",
- "title": "MediaObjectDeletedPayload",
- "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",
+ "title": "CommunityModerationActionPayload",
+ "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",
"type": "object",
"type": "object",
"required": [
"required": [
- "object_id",
- "app_id",
- "kind",
- "legacy_ref",
- "deleted_at"
+ "action",
+ "target",
+ "actor_subject"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "object_id": {
- "type": "string",
- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "app_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
- },
- "kind": {
+ "action": {
"enum": [
"enum": [
- "vod",
- "clip",
- "file",
- "thumbnail",
- "screenshot",
- "avatar",
- "asset"
+ "paste.edited",
+ "paste.deleted",
+ "paste.visibility_changed",
+ "paste.censored",
+ "pastes.bulk",
+ "forks.deleted",
+ "comment.deleted",
+ "thread.deleted",
+ "thread.locked",
+ "thread.unlocked",
+ "post.deleted"
]
]
},
},
- "legacy_ref": {
+ "target": {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "enum": [
+ "paste",
+ "paste_comment",
+ "comment",
+ "thread",
+ "post",
+ "pastes"
+ ]
+ },
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "owner_subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Whose content it was, when known."
+ }
+ }
+ },
+ "actor_subject": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 300,
- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
+ "description": "The staff member (null only for a service acting without a person)."
},
},
- "deleted_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the change committed, ISO 8601 UTC."
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500
+ },
+ "details": {
+ "type": "object",
+ "description": "Per action: the fields changed, a bulk action's name and count, … never the content."
}
}
}
}
}
}
},
},
- "media.object.visibility_changed": {
+ "community.paste-create-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",
- "title": "MediaObjectVisibilityChangedPayload",
- "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
+ "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",
+ "title": "CommunityPasteCreateRequest",
+ "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.",
"type": "object",
"type": "object",
- "required": [
- "object_id",
- "app_id",
- "kind",
- "legacy_ref",
- "visibility",
- "previous_visibility",
- "changed_at"
+ "anyOf": [
+ {
+ "required": [
+ "content"
+ ]
+ },
+ {
+ "required": [
+ "screenshot"
+ ]
+ }
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "object_id": {
- "type": "string",
- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "app_id": {
+ "content": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ "description": "The text (trimmed)."
},
},
- "kind": {
- "enum": [
- "vod",
- "clip",
- "file",
- "thumbnail",
- "screenshot",
- "avatar",
- "asset"
- ]
+ "screenshot": {
+ "type": "string",
+ "contentEncoding": "binary",
+ "description": "Multipart only: the image part."
},
},
- "legacy_ref": {
+ "title": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 300,
- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
+ "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank."
+ },
+ "language": {
+ "type": "string",
+ "description": "A language hint; auto or absent sniffs it from the content."
},
},
"visibility": {
"visibility": {
- "enum": [
- "public",
- "unlisted",
- "private"
+ "type": "string",
+ "description": "public (default), unlisted or private."
+ },
+ "burn_after_read": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
],
],
- "description": "The new value."
+ "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else."
},
},
- "previous_visibility": {
- "enum": [
- "public",
- "unlisted",
- "private"
+ "is_nsfw": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "string"
],
],
- "description": "The value before the change; never equal to visibility."
+ "description": "The Live stream it was made during (digits)."
},
},
- "changed_at": {
+ "description": {
"type": "string",
"type": "string",
- "format": "date-time",
- "description": "When the change committed, ISO 8601 UTC."
+ "description": "Multipart screenshot: the text shown under the image."
+ },
+ "page_url": {
+ "type": "string",
+ "description": "Multipart screenshot: the page it shows."
+ },
+ "user_agent": {
+ "type": "string",
+ "description": "Multipart screenshot: the capturing browser."
+ },
+ "slug": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{3,80}$",
+ "description": "Service callers, public pastes only: the slug to use (409 if taken)."
+ },
+ "metadata": {
+ "type": [
+ "object",
+ "string"
+ ],
+ "description": "Service callers only: a JSON object (at most 16 KB)."
+ },
+ "ai_summary": {
+ "type": "string",
+ "description": "Service callers only (cut to 2000 characters)."
+ },
+ "ai_tags": {
+ "description": "Service callers only, with ai_summary: JSON text or a value to serialise."
}
}
- },
- "not": {
- "anyOf": [
- {
- "required": [
- "visibility",
- "previous_visibility"
- ],
- "properties": {
- "visibility": {
- "const": "public"
- },
- "previous_visibility": {
- "const": "public"
- }
- }
- },
- {
- "required": [
- "visibility",
- "previous_visibility"
- ],
- "properties": {
- "visibility": {
- "const": "unlisted"
- },
- "previous_visibility": {
- "const": "unlisted"
- }
- }
- },
- {
- "required": [
- "visibility",
- "previous_visibility"
- ],
- "properties": {
- "visibility": {
- "const": "private"
- },
- "previous_visibility": {
- "const": "private"
- }
- }
- }
- ]
}
}
}
}
},
},
- "community.paste.created": {
+ "community.paste-moderate-request": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",
- "title": "CommunityPasteCreatedPayload",
- "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "paste_id",
- "type",
- "origin",
- "visibility",
- "owner",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "paste_id": {
- "type": "string",
- "pattern": "^[a-z0-9-]{3,64}$",
- "description": "The paste's slug."
- },
- "type": {
- "enum": [
- "paste",
- "screenshot"
- ]
- },
- "origin": {
- "enum": [
- "user",
- "ai",
- "import"
+ "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",
+ "title": "CommunityPasteModerateRequest",
+ "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "slugs",
+ "action"
],
],
- "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."
- },
- "visibility": {
- "enum": [
- "public",
- "unlisted",
- "private",
- "hidden",
- "deleted"
- ]
- },
- "owner": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
+ "properties": {
+ "slugs": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string"
+ }
},
},
- {
- "type": "null"
+ "action": {
+ "enum": [
+ "delete",
+ "public",
+ "unlisted",
+ "private"
+ ]
}
}
- ]
+ }
},
},
- "url": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "screenshot"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
+ "properties": {
+ "screenshot": {
+ "type": "string",
+ "contentEncoding": "binary",
+ "description": "The multipart part named screenshot: the censored image."
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "community.paste.updated": {
+ "community.paste-moderate-result": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",
- "title": "CommunityPasteUpdatedPayload",
- "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "paste_id",
- "changed",
- "visibility",
- "revision",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "paste_id": {
- "type": "string",
- "pattern": "^[a-z0-9-]{3,64}$"
- },
- "changed": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": {
- "enum": [
- "title",
- "content",
- "language",
- "visibility",
- "is_nsfw",
- "pinned"
- ]
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",
+ "title": "CommunityPasteModerateResult",
+ "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "done",
+ "skipped"
+ ],
+ "properties": {
+ "done": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "skipped": {
+ "type": "integer",
+ "minimum": 0
+ }
}
}
},
},
- "visibility": {
- "enum": [
- "public",
- "unlisted",
- "private",
- "hidden",
- "deleted"
- ]
- },
- "revision": {
- "type": "integer",
- "minimum": 1
+ {
+ "type": "object",
+ "required": [
+ "paste"
+ ],
+ "properties": {
+ "paste": {
+ "$ref": "paste.v1.json"
+ }
+ }
},
},
- "url": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "message"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "community.paste.deleted": {
+ "community.paste-write-request": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",
- "title": "CommunityPasteDeletedPayload",
- "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "paste_id"
- ],
- "additionalProperties": false,
- "properties": {
- "paste_id": {
- "type": "string",
- "pattern": "^[a-z0-9-]{3,64}$"
+ "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",
+ "title": "CommunityPasteWriteRequest",
+ "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",
+ "anyOf": [
+ {
+ "type": "object",
+ "minProperties": 1,
+ "properties": {
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "content": {
+ "type": "string"
+ },
+ "language": {
+ "type": "string"
+ },
+ "visibility": {
+ "type": "string",
+ "description": "public, unlisted or private (anything else is public)."
+ },
+ "is_nsfw": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ]
+ },
+ "pinned": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ],
+ "description": "Staff only; ignored otherwise."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000
+ },
+ "parent_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The top-level comment this replies to."
+ },
+ "anon_name": {
+ "type": "string",
+ "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "community.thread.created": {
+ "community.paste-write-result": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",
- "title": "CommunityThreadCreatedPayload",
- "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "thread_id",
- "space",
- "author",
- "visibility",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "thread_id": {
- "type": "integer",
- "minimum": 1
+ "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",
+ "title": "CommunityPasteWriteResult",
+ "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "paste"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "paste": {
+ "$ref": "paste.v1.json"
+ }
+ }
},
},
- "space": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ {
+ "type": "object",
+ "required": [
+ "success"
+ ],
+ "properties": {
+ "success": {
+ "const": true
+ }
+ }
},
},
- "author": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
+ {
+ "type": "object",
+ "required": [
+ "liked",
+ "likes"
+ ],
+ "properties": {
+ "liked": {
+ "type": "boolean"
},
},
- {
- "type": "null"
+ "likes": {
+ "type": "integer",
+ "minimum": 0
}
}
- ]
+ }
},
},
- "visibility": {
- "enum": [
- "public",
- "members",
- "hidden"
- ]
+ {
+ "type": "object",
+ "required": [
+ "copies"
+ ],
+ "properties": {
+ "copies": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "comment"
+ ],
+ "properties": {
+ "comment": {
+ "type": "object",
+ "required": [
+ "id",
+ "paste_id",
+ "message",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "paste_id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The author's subject; null for an anonymous comment."
+ },
+ "author_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "parent_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "anon_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "is_deleted": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
},
},
- "url": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "message"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "community.post.created": {
+ "community.paste": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",
- "title": "CommunityPostCreatedPayload",
- "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "$id": "https://openvibe.network/contracts/community/paste.v1.json",
+ "title": "CommunityPaste",
+ "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).",
"type": "object",
"type": "object",
"required": [
"required": [
- "post_id",
- "thread_id",
- "space",
- "author",
+ "id",
+ "app_id",
+ "slug",
+ "user_id",
+ "owner_subject",
+ "origin",
+ "type",
+ "title",
"visibility",
"visibility",
- "url"
+ "url",
+ "raw_url",
+ "created_at",
+ "is_owner"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "post_id": {
- "type": "integer",
- "minimum": 1
+ "id": {
+ "type": "integer"
},
},
- "thread_id": {
- "type": "integer",
- "minimum": 1
+ "app_id": {
+ "const": "community"
},
},
- "space": {
+ "slug": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The owner's subject (usr_…), or null for an anonymous paste."
+ },
+ "owner_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "origin": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ "description": "user, or ai for a paste an AI service wrote."
},
},
- "author": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
+ "type": {
+ "enum": [
+ "paste",
+ "screenshot"
+ ]
+ },
+ "title": {
+ "type": "string"
+ },
+ "content": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "language": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
"visibility": {
"visibility": {
"enum": [
"enum": [
"public",
"public",
- "members",
- "hidden"
+ "unlisted",
+ "private"
]
]
},
},
- "url": {
+ "stream_id": {
"type": [
"type": [
- "string",
+ "integer",
"null"
"null"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
- }
- }
- }
- },
- "community.comment.created": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",
- "title": "CommunityCommentCreatedPayload",
- "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "comment_id",
- "thread_access_id",
- "ref",
- "author",
- "visibility",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "comment_id": {
- "type": "integer",
- "minimum": 1
- },
- "thread_access_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64
+ "description": "The Live stream it was made during, when its stream_ref is one."
},
},
- "ref": {
- "type": "object",
- "required": [
- "service",
- "type",
- "id"
+ "stream_ref": {
+ "type": [
+ "object",
+ "null"
],
],
- "additionalProperties": false,
- "properties": {
- "service": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$"
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,39}$"
- },
- "id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200
- }
- }
+ "description": "An EntityRef of where it was made (a Live stream, or a service's own source)."
},
},
- "author": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
+ "screenshot_url": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
- "visibility": {
- "enum": [
- "public",
- "hidden",
- "locked"
+ "metadata": {
+ "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details."
+ },
+ "burn_after_read": {
+ "type": "boolean"
+ },
+ "forked_from": {
+ "type": [
+ "integer",
+ "null"
]
]
},
},
- "url": {
+ "pinned": {
+ "type": "boolean"
+ },
+ "views": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "unique_views": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "copies": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "likes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "is_nsfw": {
+ "type": "boolean"
+ },
+ "ai_summary": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ai_tags": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
- }
- }
- }
- },
- "chat.moderation.action": {
- "version": "1.0.0",
- "owner": "chat",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",
- "title": "ChatModerationActionPayload",
- "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
- "type": "object",
- "required": [
- "action_id",
- "action_type",
- "scope_type"
- ],
- "additionalProperties": false,
- "properties": {
- "action_id": {
- "type": "integer",
- "minimum": 1
+ "description": "JSON text as the AI pass wrote it."
},
},
- "action_type": {
+ "ai_analyzed_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "url": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 64,
- "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."
+ "description": "/p/<slug>"
},
},
- "scope_type": {
+ "raw_url": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 32,
- "description": "site, channel, stream, room, …"
+ "description": "/p/<slug>/raw"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_owner": {
+ "type": "boolean",
+ "description": "Whether the caller owns it."
},
},
- "scope_id": {
+ "username": {
"type": [
"type": [
"string",
"string",
- "integer",
"null"
"null"
]
]
},
},
- "actor_user_id": {
+ "display_name": {
"type": [
"type": [
- "integer",
+ "string",
"null"
"null"
],
],
- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
+ "description": "OpenVibe AI for an AI paste."
},
},
- "actor_subject": {
+ "avatar_url": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
- "target_user_id": {
- "type": [
- "integer",
- "null"
- ]
- },
- "target_subject": {
+ "profile_color": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
- },
- "details": {
- "type": "object"
}
}
}
}
}
}
},
},
- "live.moderation.action": {
+ "community.moderation-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "live",
+ "owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",
- "title": "LiveModerationActionPayload",
- "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
- "type": "object",
- "required": [
- "action_id",
- "action_type",
- "scope_type"
- ],
- "additionalProperties": false,
- "properties": {
- "action_id": {
- "type": "integer",
- "minimum": 1
+ "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",
+ "title": "CommunityModerationRequest",
+ "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "enum": [
+ "public",
+ "hidden",
+ "locked"
+ ]
+ }
+ }
},
},
- "action_type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."
+ {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "pinned"
+ ]
+ },
+ {
+ "required": [
+ "locked"
+ ]
+ }
+ ],
+ "properties": {
+ "pinned": {
+ "description": "Truthy pins the thread."
+ },
+ "locked": {
+ "description": "Truthy locks it (announced as thread.locked / thread.unlocked)."
+ }
+ }
},
},
- "scope_type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 32,
- "description": "site, channel, stream, room, …"
+ {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "body"
+ ]
+ },
+ {
+ "required": [
+ "body_markdown"
+ ]
+ }
+ ],
+ "properties": {
+ "body": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40000
+ },
+ "body_markdown": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40000
+ }
+ }
},
},
- "scope_id": {
- "type": [
- "string",
- "integer",
- "null"
- ]
+ {
+ "type": "object",
+ "required": [
+ "space",
+ "webhook_url_ref"
+ ],
+ "properties": {
+ "space": {
+ "type": "string",
+ "description": "The space's slug."
+ },
+ "webhook_url_ref": {
+ "type": "string",
+ "pattern": "^[A-Z0-9_]+$"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Default true."
+ }
+ }
},
},
- "actor_user_id": {
- "type": [
- "integer",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "enabled"
],
],
- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
},
},
- "actor_subject": {
- "type": [
- "string",
- "null"
- ]
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "community.moderation-result": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",
+ "title": "CommunityModerationResult",
+ "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "thread"
+ ],
+ "properties": {
+ "thread": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "integer",
+ "string"
+ ]
+ },
+ "access_id": {
+ "type": "string",
+ "description": "A comment thread's browser id (cth_…)."
+ },
+ "ref": {
+ "$ref": "../common/entity-ref.v1.json"
+ },
+ "visibility": {
+ "type": "string"
+ },
+ "comment_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "space": {
+ "type": "string"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "pinned": {
+ "type": "boolean"
+ },
+ "locked": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
},
},
- "target_user_id": {
- "type": [
- "integer",
- "null"
- ]
+ {
+ "type": "object",
+ "required": [
+ "post"
+ ],
+ "properties": {
+ "post": {
+ "type": "object",
+ "required": [
+ "id",
+ "thread_id",
+ "revision",
+ "deleted"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "thread_id": {
+ "type": "integer"
+ },
+ "is_opening": {
+ "type": "boolean"
+ },
+ "origin": {
+ "type": "string"
+ },
+ "author": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "body_markdown": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "body_html": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "can_edit": {
+ "type": "boolean"
+ },
+ "attachments": {
+ "type": "array"
+ },
+ "pastes": {
+ "type": "array"
+ }
+ }
+ }
+ }
},
},
- "target_subject": {
- "type": [
- "string",
- "null"
- ]
+ {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "id": {
+ "type": "integer"
+ },
+ "deleted": {
+ "const": "thread"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "enabled"
+ ],
+ "anyOf": [
+ {
+ "required": [
+ "deliveries"
+ ]
+ },
+ {
+ "required": [
+ "mappings"
+ ]
+ }
+ ],
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "deliveries": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "mappings": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ }
},
},
- "details": {
- "type": "object"
+ {
+ "type": "object",
+ "required": [
+ "mapping"
+ ],
+ "properties": {
+ "mapping": {
+ "type": "object",
+ "required": [
+ "id",
+ "space",
+ "webhook_url_ref",
+ "webhook_configured",
+ "enabled"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "string"
+ },
+ "direction": {
+ "type": "string"
+ },
+ "webhook_url_ref": {
+ "type": "string"
+ },
+ "webhook_configured": {
+ "type": "boolean"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "community.moderation.action": {
+ "community.post-write-request": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",
- "title": "CommunityModerationActionPayload",
- "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",
- "type": "object",
- "required": [
- "action",
- "target",
- "actor_subject"
- ],
- "additionalProperties": false,
- "properties": {
- "action": {
- "enum": [
- "paste.edited",
- "paste.deleted",
- "paste.visibility_changed",
- "paste.censored",
- "pastes.bulk",
- "forks.deleted",
- "comment.deleted",
- "thread.deleted",
- "thread.locked",
- "thread.unlocked",
- "post.deleted"
- ]
- },
- "target": {
+ "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",
+ "title": "CommunityPostWriteRequest",
+ "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
- "required": [
- "type",
- "id"
+ "anyOf": [
+ {
+ "required": [
+ "body"
+ ]
+ },
+ {
+ "required": [
+ "body_markdown"
+ ]
+ }
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "type": {
- "enum": [
- "paste",
- "paste_comment",
- "comment",
- "thread",
- "post",
- "pastes"
- ]
+ "title": {
+ "type": "string",
+ "description": "A new thread's title."
},
},
- "id": {
+ "body": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 200
+ "maxLength": 40000,
+ "description": "Markdown."
},
},
- "owner_subject": {
+ "body_markdown": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40000,
+ "description": "Read when body is absent."
+ },
+ "members_only": {
+ "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",
+ "anyOf": [
+ {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^usr_"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner"
+ ],
+ "properties": {
+ "owner": {
+ "type": "string",
+ "pattern": "^usr_"
+ }
+ }
+ }
+ ]
+ },
+ "category": {
+ "type": "string",
+ "description": "A new thread only: the slug of one of the space's categories."
+ },
+ "attachments": {
+ "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ {
+ "type": "array",
+ "maxItems": 4,
+ "items": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ ]
+ },
+ "pastes": {
+ "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",
"type": [
"type": [
- "string",
- "null"
+ "array",
+ "string"
],
],
- "description": "Whose content it was, when known."
+ "items": {
+ "type": "string"
+ }
}
}
}
}
},
},
- "actor_subject": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "value"
],
],
- "description": "The staff member (null only for a service acting without a person)."
+ "properties": {
+ "value": {
+ "enum": [
+ 1,
+ -1,
+ 0,
+ "1",
+ "-1",
+ "0"
+ ],
+ "description": "A thread vote; 0 takes the vote back."
+ }
+ }
},
},
- "reason": {
- "type": [
- "string",
- "null"
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "community.pulse-write-result": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",
+ "title": "CommunityPulseWriteResult",
+ "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "item",
+ "created"
],
],
- "maxLength": 500
+ "properties": {
+ "created": {
+ "type": "boolean"
+ },
+ "item": {
+ "type": "object",
+ "required": [
+ "id",
+ "source",
+ "title",
+ "url",
+ "origin",
+ "label",
+ "actor",
+ "occurred_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ }
+ }
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 300
+ },
+ "url": {
+ "type": "string"
+ },
+ "origin": {
+ "enum": [
+ "user",
+ "ai",
+ "system"
+ ]
+ },
+ "label": {
+ "enum": [
+ "AI",
+ "System",
+ null
+ ]
+ },
+ "actor": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "occurred_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
},
},
- "details": {
+ {
"type": "object",
"type": "object",
- "description": "Per action: the fields changed, a bulk action's name and count, … never the content."
+ "required": [
+ "removed"
+ ],
+ "properties": {
+ "removed": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1
+ }
+ }
}
}
- }
+ ]
}
}
}
}
}
}
@@ -8795,6 +8795,573 @@ export interface BillingReceiptExternalPayload {
@@ -8795,6 +8795,573 @@ export interface BillingReceiptExternalPayload {
};
};
}
}
+/** [email protected] (owner: billing) */
+/**
+ * billing.admin-request@1: bodies of billing.ledger.admin on OpenVibe.Billing (operators). POST /api/v1/admin/freeze: { on, reason? } (unfreezing processes the provider deliveries stored meanwhile; allowed while frozen). POST /api/v1/admin/adjustments: { from, to, amount, reason, relates_to? } moves value between two accounts of one currency. POST /api/v1/purchases/settle: { provider, provider_ref, subject?, amount_cents, bits, intent_id?, test? } books a provider receipt. POST /api/v1/admin/provider-events/:id/reprocess, POST /api/v1/admin/sweep and the GETs (freeze, reconcile, provider-events, import-holds) take no body. Adjustments and settlements need an Idempotency-Key and are refused while frozen.
+ */
+export type BillingAdminRequest =
+ | {
+ on: boolean;
+ reason?: string | null;
+ }
+ | {
+ from: {
+ kind:
+ | "user_credit"
+ | "creator_payable"
+ | "provider_clearing"
+ | "platform_revenue"
+ | "payouts_pending"
+ | "refunds"
+ | "import_adjustment"
+ | "chargeback_loss"
+ | "fx_conversion";
+ owner?: string | null;
+ currency: "vibes-bits" | "usd-cents";
+ };
+ to: {
+ kind:
+ | "user_credit"
+ | "creator_payable"
+ | "provider_clearing"
+ | "platform_revenue"
+ | "payouts_pending"
+ | "refunds"
+ | "import_adjustment"
+ | "chargeback_loss"
+ | "fx_conversion";
+ owner?: string | null;
+ currency: "vibes-bits" | "usd-cents";
+ };
+ /**
+ * In the accounts' currency (at most 1,000,000,000).
+ */
+ amount: number | string;
+ reason: string;
+ relates_to?: string | null;
+ }
+ | {
+ provider: string;
+ /**
+ * The provider's payment id.
+ */
+ provider_ref: string;
+ /**
+ * Who the Vibes are for.
+ */
+ subject?: SubjectRef | string;
+ /**
+ * Cents paid.
+ */
+ amount_cents?: number | string;
+ /**
+ * Vibes to credit.
+ */
+ bits?: number | string;
+ intent_id?: string;
+ test?: boolean;
+ }
+ | NoBody;
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.
+ */
+export type BillingAdminResult =
+ | {
+ frozen: boolean;
+ reason: string | null;
+ frozen_at: string | null;
+ frozen_by: unknown;
+ processed_after_unfreeze?: unknown;
+ }
+ | {
+ id: string;
+ ok: boolean;
+ trigger?: string | null;
+ started_at?: string;
+ finished_at?: string;
+ checks: {
+ id: string;
+ ok: boolean;
+ }[];
+ warnings?: unknown[];
+ totals?: unknown;
+ }
+ | {
+ transaction: BillingTransaction;
+ duplicate_receipt?: boolean;
+ }
+ | {
+ events: {
+ id: number;
+ provider: string;
+ provider_event_id?: string | null;
+ type?: string | null;
+ processed_at?: string | null;
+ result?: unknown;
+ attempts?: number;
+ last_error?: string | null;
+ }[];
+ }
+ | {
+ event: {};
+ }
+ | {
+ renewed: string[];
+ expired: string[];
+ canceled: string[];
+ skipped: number;
+ }
+ | {
+ holds: {}[];
+ };
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.balance-read-result@1: answers of billing.balance.read on OpenVibe.Billing. GET /api/v1/balances/:subject → billing.balance@1; GET /api/v1/transactions?subject=&limit=&cursor= → { transactions, next_cursor } (newest first, at most 200 a page; subject is required); GET /api/v1/transactions/:id → { transaction, reversed_by } (ids of the transactions that reverse it).
+ */
+export type BillingBalanceReadResult =
+ | BillingBalance
+ | {
+ /**
+ * @maxItems 200
+ */
+ transactions: BillingTransaction[];
+ next_cursor: string | null;
+ }
+ | {
+ transaction: BillingTransaction;
+ reversed_by: string[];
+ };
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.
+ */
+export interface BillingBalance {
+ subject: SubjectRef;
+ currency: "vibes-bits";
+ credit: number;
+ payable: number;
+ pending_payouts: number;
+ /**
+ * What payable is worth in US cents at today's rate.
+ */
+ payable_value_cents: number;
+ bits_per_usd: number;
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).
+ */
+export type BillingCashoutDecision =
+ | {
+ /**
+ * The provider's payout id.
+ */
+ payout_reference: string;
+ payout_provider?: string;
+ }
+ | {
+ reason: string | null;
+ }
+ | NoBody;
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.cashout-request@1: bodies of billing.cashout.request on OpenVibe.Billing. POST /api/v1/cashouts: { subject, amount, payout_method: { type, address } } moves amount bits (at least the minimum cashout) from the person's payable into payouts pending, in escrow until staff pay it (a PayPal address must be an email). POST /api/v1/recycle: { subject, amount } turns payable back into the person's own spendable credit. GET /api/v1/cashouts/:id takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).
+ */
+export type BillingCashoutRequest =
+ | {
+ /**
+ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.
+ */
+ subject: SubjectRef | string;
+ /**
+ * Bits.
+ */
+ amount: number | string;
+ payout_method: {
+ /**
+ * paypal, …
+ */
+ type: string;
+ address: string;
+ };
+ }
+ | {
+ /**
+ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.
+ */
+ subject: SubjectRef | string;
+ /**
+ * Bits.
+ */
+ amount: number | string;
+ }
+ | NoBody;
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.cashout-result@1: answers about cashouts on OpenVibe.Billing. POST /api/v1/cashouts (201), GET /api/v1/cashouts/:id, POST /api/v1/cashouts/:id/approve and /deny → { cashout }; GET /api/v1/cashouts?status=&subject=&limit= → { cashouts }; POST /api/v1/recycle (201) → { transaction, balance }. A cashout the Events stream carries names the payout method's type only; this API answers the caller who may see the address.
+ */
+export type BillingCashoutResult =
+ | {
+ cashout: BillingCashout;
+ }
+ | {
+ cashouts: BillingCashout[];
+ }
+ | {
+ transaction: BillingTransaction;
+ balance: BillingBalance;
+ };
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.cashout@1: one cashout as OpenVibe.Billing presents it (server/ops/cashouts.js present). Requested cashouts wait out an escrow; staff approve (with the provider's payout reference) or deny them.
+ */
+export interface BillingCashout {
+ id: string;
+ subject: SubjectRef;
+ amount_bits: number;
+ value_cents: number;
+ status: "requested" | "paid" | "denied";
+ payout_method: {
+ type?: string;
+ address?: string;
+ } | null;
+ escrow_until: string;
+ request_txn: string;
+ settle_txn?: string | null;
+ payout_provider?: string | null;
+ payout_reference?: string | null;
+ reason?: string | null;
+ created_at: string;
+ updated_at?: string | null;
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.entitlement-result@1: answers of billing.entitlement.check on OpenVibe.Billing. GET /api/v1/entitlements/:subject?streamer= → billing.entitlement@1; without streamer → { subject, entitlements } (every active one); GET /api/v1/subscriptions → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.
+ */
+export type BillingEntitlementResult =
+ | BillingEntitlement
+ | {
+ subject: SubjectRef;
+ entitlements: BillingEntitlement[];
+ }
+ | {
+ subscriptions: BillingSubscription[];
+ }
+ | {
+ subscription: BillingSubscription;
+ entitlement: BillingEntitlement;
+ };
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.entitlement@1: whether a person is entitled to a streamer's subscriber perks now (OpenVibe.Billing server/ops/subscriptions.js entitlement): GET /api/v1/entitlements/:subject?streamer=, and inside subscription answers. Chained, already-paid future periods extend expires_at.
+ */
+export interface BillingEntitlement {
+ subject: SubjectRef;
+ streamer: SubjectRef;
+ kind: string;
+ active: boolean;
+ expires_at: string | null;
+ subscription: {
+ id?: string;
+ status?: string;
+ auto_renew?: boolean | number;
+ cancel_at_period_end?: boolean | number;
+ provider?: string;
+ } | null;
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.intent-request@1: the body of POST /api/v1/intents on OpenVibe.Billing (billing.intent.create): a checkout with an enabled provider for a Vibes purchase ({ kind: purchase, bits } at least the minimum purchase) or a channel subscription ({ kind: subscription, streamer }; route direct pays the streamer's own account, and a direct PowerChat subscription names receiving_account). Billing prices it; the answer is { intent, checkout_url } (201). GET /api/v1/intents/:id and POST /api/v1/intents/:id/capture take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).
+ */
+export type BillingIntentRequest =
+ | {
+ /**
+ * stripe, paypal, nowpayments, powerchat, ccbill (enabled ones only).
+ */
+ provider: string;
+ kind: "purchase" | "subscription";
+ /**
+ * Who pays.
+ */
+ subject: SubjectRef | string;
+ /**
+ * Purchase: the Vibes to buy.
+ */
+ bits?: number | string;
+ /**
+ * Subscription: the channel's owner (never the payer).
+ */
+ streamer?: SubjectRef | string;
+ /**
+ * Subscription: direct, or the site route.
+ */
+ route?: string;
+ /**
+ * Direct PowerChat subscription: the streamer's PowerChat username.
+ */
+ receiving_account?: string;
+ auto_renew?: boolean | number;
+ success_url?: string;
+ cancel_url?: string;
+ }
+ | NoBody;
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.intent-result@1: answers of billing.intent.create on OpenVibe.Billing. POST /api/v1/intents → { intent, checkout_url } (null when the provider has no hosted checkout); GET /api/v1/intents/:id → { intent }; POST /api/v1/intents/:id/capture → { result, intent } (the settlement the provider's capture produced).
+ */
+export interface BillingIntentResult {
+ intent: BillingIntent;
+ checkout_url?: string | null;
+ /**
+ * Capture: what applying the provider's plan did.
+ */
+ result?: {
+ [k: string]: unknown | undefined;
+ };
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.intent@1: a checkout intent as OpenVibe.Billing presents it (server/ops/intents.js present): what a person is about to pay a provider for (a Vibes purchase or a channel subscription), priced by Billing.
+ */
+export interface BillingIntent {
+ id: string;
+ provider: string;
+ provider_ref: string | null;
+ kind: "purchase" | "subscription";
+ subject: SubjectRef;
+ streamer: SubjectRef | null;
+ amount_cents: number;
+ fee_cents: number;
+ bits: number;
+ route: string | null;
+ auto_renew?: number | boolean;
+ /**
+ * created, settled, failed, …
+ */
+ status: string;
+ settled_txn: string | null;
+ created_at: string;
+ /**
+ * PowerChat only: pcorder:<id> or pcsub:<id>, echoed back by its webhook.
+ */
+ checkout_ref?: string;
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.subscription-request@1: the body of POST /api/v1/subscriptions on OpenVibe.Billing (billing.subscription.manage): pay one period of subscriber → streamer, from the subscriber's credit (source credit, the default) or from a provider receipt (source receipt, which also needs billing.ledger.admin: claiming money was received is operator grade). POST /api/v1/subscriptions/:id/cancel (cancels at the period end, with the provider too) and the GETs take no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).
+ */
+export type BillingSubscriptionRequest =
+ | {
+ /**
+ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.
+ */
+ subscriber: SubjectRef | string;
+ /**
+ * A user SubjectRef, or its bare id (usr_…). Guests hold no money.
+ */
+ streamer: SubjectRef | string;
+ source?: "credit" | "receipt";
+ /**
+ * Default: Billing's subscription price.
+ */
+ price_cents?: number | string;
+ auto_renew?: boolean | number;
+ receipt?: {
+ provider: string;
+ provider_ref: string;
+ /**
+ * Cents paid.
+ */
+ amount_cents?: number | string;
+ fee_cents?: number | string;
+ route?: string;
+ subscription_ref?: string;
+ test?: boolean;
+ };
+ }
+ | NoBody;
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.subscription-result@1: answers about subscriptions on OpenVibe.Billing. POST /api/v1/subscriptions (201) → { subscription, entitlement, transaction }; POST /api/v1/subscriptions/:id/cancel → { subscription, provider_sync }; GET /api/v1/subscriptions?subscriber=&streamer=&status= → { subscriptions }; GET /api/v1/subscriptions/:id → { subscription, entitlement }.
+ */
+export type BillingSubscriptionResult =
+ | {
+ subscription: BillingSubscription;
+ /**
+ * billing.entitlement@1 (the answer to POST /subscriptions carries the entitlement as booked).
+ */
+ entitlement?: {
+ [k: string]: unknown | undefined;
+ };
+ transaction?: BillingTransaction;
+ /**
+ * not_needed, or how the provider was told.
+ */
+ provider_sync?: string;
+ }
+ | {
+ subscriptions: BillingSubscription[];
+ };
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.subscription@1: one channel subscription as OpenVibe.Billing presents it (server/ops/subscriptions.js present).
+ */
+export interface BillingSubscription {
+ id: string;
+ subscriber: SubjectRef;
+ streamer: SubjectRef;
+ tier?: unknown;
+ /**
+ * credit, or the provider that takes the payments.
+ */
+ provider: string;
+ provider_ref?: string | null;
+ /**
+ * site or direct (paid to the streamer's own provider account).
+ */
+ route?: string | null;
+ /**
+ * active, canceled, expired, …
+ */
+ status: string;
+ auto_renew: boolean;
+ cancel_at_period_end: boolean;
+ price_cents?: number | null;
+ current_period_end: string | null;
+ created_at?: string;
+ updated_at?: string | null;
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.transaction@1: one journal transaction as OpenVibe.Billing's /api/v1 presents it (server/ledger.js present). Amounts are integers: bits for vibes-bits, cents for usd-cents; each transaction's entries balance per currency. Subjects are bare Network subject ids.
+ */
+export interface BillingTransaction {
+ id: string;
+ type:
+ | "purchase"
+ | "donation"
+ | "subscription"
+ | "subscription_share"
+ | "cashout_request"
+ | "cashout_paid"
+ | "cashout_denied"
+ | "recycle"
+ | "refund"
+ | "chargeback"
+ | "adjustment"
+ | "import";
+ status: string;
+ /**
+ * Test money: never counted.
+ */
+ test: boolean;
+ reverses_txn: string | null;
+ from_subject: string | null;
+ to_subject: string | null;
+ provider: string | null;
+ /**
+ * <provider>:<provider payment id> for money that arrived from a provider.
+ */
+ receipt_ref: string | null;
+ /**
+ * Who posted it (as recorded).
+ */
+ actor?: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * Per type: amount_bits, paid_cents, target EntityRef, message, rates…
+ */
+ metadata?: {
+ [k: string]: unknown | undefined;
+ };
+ created_at: string;
+ entries: {
+ account: {
+ kind:
+ | "user_credit"
+ | "creator_payable"
+ | "provider_clearing"
+ | "platform_revenue"
+ | "payouts_pending"
+ | "refunds"
+ | "import_adjustment"
+ | "chargeback_loss"
+ | "fx_conversion";
+ /**
+ * A subject id, a provider slug, or null for a platform account.
+ */
+ owner?: string | null;
+ currency: "vibes-bits" | "usd-cents";
+ };
+ /**
+ * Signed: negative leaves the account.
+ */
+ amount: number;
+ }[];
+}
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.transfer-request@1: bodies of billing.transfer.create on OpenVibe.Billing. POST /api/v1/transfers: { from, to, amount, kind?, target?, message? } moves amount bits from the sender's credit to the recipient's payable (a tip, donation or paid interaction; never to oneself). POST /api/v1/transfers/:id/refund: { amount?, reason? } gives a credit-funded donation back (all that remains refundable when amount is absent). Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).
+ */
+export type BillingTransferRequest =
+ | {
+ /**
+ * The sender (their credit pays).
+ */
+ from: SubjectRef | string;
+ /**
+ * The recipient (their payable receives).
+ */
+ to: SubjectRef | string;
+ /**
+ * Bits.
+ */
+ amount: number | string;
+ /**
+ * Default donation.
+ */
+ kind?: "tip" | "donation" | "paid_interaction";
+ target?: EntityRef;
+ message?: string | null;
+ }
+ | {
+ /**
+ * Bits to give back; default all that remains refundable.
+ */
+ amount?: number | string;
+ reason?: string | null;
+ };
+
+/** [email protected] (owner: billing) */
+/**
+ * billing.transfer-result@1: answers of billing.transfer.create on OpenVibe.Billing (201). POST /api/v1/transfers → { transaction, balance: { credit } } (the sender's credit after it); POST /api/v1/transfers/:id/refund → { transaction } (type refund, reverses_txn the donation).
+ */
+export interface BillingTransferResult {
+ transaction: BillingTransaction;
+ balance?: {
+ credit: number;
+ };
+}
+
/** [email protected] (owner: deals) */
/** [email protected] (owner: deals) */
/**
/**
* deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.
* deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.
@@ -13682,6 +14249,158 @@ export interface ChatModerationActionPayload {
@@ -13682,6 +14249,158 @@ export interface ChatModerationActionPayload {
details?: {};
details?: {};
}
}
+/** [email protected] (owner: chat) */
+/**
+ * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.
+ */
+export type ChatSendRequest =
+ | {
+ type: "chat";
+ /**
+ * The text (trimmed by Chat).
+ */
+ message: string;
+ /**
+ * Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored).
+ */
+ reply_to_id?: number | string;
+ /**
+ * Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off).
+ */
+ auto_delete_minutes?: number | string;
+ /**
+ * The voice channel the sender is in, echoed on the broadcast.
+ */
+ voiceChannelId?: string;
+ }
+ | {
+ /**
+ * Live's boot id; placeholder message ids are mapped per boot.
+ */
+ boot?: string;
+ /**
+ * @maxItems 500
+ */
+ ops: {
+ /**
+ * Echoed on the result.
+ */
+ seq?: number;
+ /**
+ * A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0]).
+ */
+ op: string;
+ args?: unknown[];
+ /**
+ * Live's placeholder id for the row this op inserts.
+ */
+ ref?: number;
+ /**
+ * Idempotency key: applied once.
+ */
+ key?: string;
+ }[];
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.
+ */
+export type ChatSendResult =
+ | {
+ type: "chat";
+ /**
+ * The stored message id.
+ */
+ id?: number;
+ /**
+ * Display name, or the anonymous id.
+ */
+ username: string;
+ /**
+ * The immutable login handle; null for an anonymous sender.
+ */
+ core_username?: string | null;
+ user_id?: number | null;
+ anon_id?: string | null;
+ /**
+ * The sender's role, anon for an anonymous sender.
+ */
+ role: string;
+ message: string;
+ stream_id: number | null;
+ channel_user_id: number | null;
+ /**
+ * true for homepage global chat (no stream, no channel).
+ */
+ is_global: boolean;
+ avatar_url?: string | null;
+ profile_color?: string;
+ filtered?: boolean;
+ timestamp: string;
+ auto_delete_at?: string | null;
+ voiceChannelId?: string;
+ reply_to?: {
+ id: number;
+ username: string;
+ user_id?: number | null;
+ /**
+ * The parent's text, cut to 100 characters.
+ */
+ message: string;
+ };
+ /**
+ * Cosmetic name effect.
+ */
+ nameFX?: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * Cosmetic particle effect.
+ */
+ particleFX?: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * Cosmetic hat.
+ */
+ hatFX?: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * Cosmetic TTS voice effect.
+ */
+ voiceFX?: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * The sender's equipped tag.
+ */
+ tag?: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows.
+ */
+ vip_badge?: {
+ [k: string]: unknown | undefined;
+ };
+ }
+ | {
+ type: "system" | "error";
+ message: string;
+ }
+ | {
+ ok: true;
+ results: {
+ seq?: number;
+ ok: boolean;
+ result?: unknown;
+ code?: string;
+ error?: string;
+ }[];
+ };
+
/** [email protected] (owner: live) */
/** [email protected] (owner: live) */
/**
/**
* live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).
* live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).
@@ -13742,3 +14461,395 @@ export interface CommunityModerationActionPayload {
@@ -13742,3 +14461,395 @@ export interface CommunityModerationActionPayload {
*/
*/
details?: {};
details?: {};
}
}
+
+/** [email protected] (owner: community) */
+/**
+ * community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.
+ */
+export type CommunityPasteCreateRequest = {
+ [k: string]: unknown | undefined;
+} & {
+ /**
+ * The text (trimmed).
+ */
+ content?: string;
+ /**
+ * Multipart only: the image part.
+ */
+ screenshot?: string;
+ /**
+ * Trimmed to 200 characters; Untitled (Screenshot for an image) when blank.
+ */
+ title?: string | null;
+ /**
+ * A language hint; auto or absent sniffs it from the content.
+ */
+ language?: string;
+ /**
+ * public (default), unlisted or private.
+ */
+ visibility?: string;
+ /**
+ * Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else.
+ */
+ burn_after_read?: boolean | number | string;
+ is_nsfw?: boolean | number | string;
+ /**
+ * The Live stream it was made during (digits).
+ */
+ stream_id?: number | string;
+ /**
+ * Multipart screenshot: the text shown under the image.
+ */
+ description?: string;
+ /**
+ * Multipart screenshot: the page it shows.
+ */
+ page_url?: string;
+ /**
+ * Multipart screenshot: the capturing browser.
+ */
+ user_agent?: string;
+ /**
+ * Service callers, public pastes only: the slug to use (409 if taken).
+ */
+ slug?: string;
+ /**
+ * Service callers only: a JSON object (at most 16 KB).
+ */
+ metadata?: {} | string;
+ /**
+ * Service callers only (cut to 2000 characters).
+ */
+ ai_summary?: string;
+ /**
+ * Service callers only, with ai_summary: JSON text or a value to serialise.
+ */
+ ai_tags?: {
+ [k: string]: unknown | undefined;
+ };
+};
+
+/** [email protected] (owner: community) */
+/**
+ * community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.
+ */
+export type CommunityPasteModerateRequest =
+ | {
+ /**
+ * @minItems 1
+ */
+ slugs: [string, ...string[]];
+ action: "delete" | "public" | "unlisted" | "private";
+ }
+ | {
+ /**
+ * The multipart part named screenshot: the censored image.
+ */
+ screenshot: string;
+ }
+ | NoBody;
+
+/** [email protected] (owner: community) */
+/**
+ * community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).
+ */
+export type CommunityPasteModerateResult =
+ | {
+ done: number;
+ skipped: number;
+ }
+ | {
+ paste: CommunityPaste;
+ }
+ | {
+ message: string;
+ };
+
+/** [email protected] (owner: community) */
+/**
+ * community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.
+ */
+export type CommunityPasteWriteRequest =
+ | {
+ title?: string | null;
+ content?: string;
+ language?: string;
+ /**
+ * public, unlisted or private (anything else is public).
+ */
+ visibility?: string;
+ is_nsfw?: boolean | number | string;
+ /**
+ * Staff only; ignored otherwise.
+ */
+ pinned?: boolean | number | string;
+ }
+ | {
+ message: string;
+ /**
+ * The top-level comment this replies to.
+ */
+ parent_id?: number | string;
+ /**
+ * An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default).
+ */
+ anon_name?: string;
+ }
+ | NoBody;
+
+/** [email protected] (owner: community) */
+/**
+ * community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.
+ */
+export type CommunityPasteWriteResult =
+ | {
+ id?: number;
+ slug?: string;
+ url?: string;
+ paste: CommunityPaste;
+ }
+ | {
+ success: true;
+ }
+ | {
+ liked: boolean;
+ likes: number;
+ }
+ | {
+ copies: number;
+ }
+ | {
+ comment: {
+ id: number;
+ paste_id: number;
+ /**
+ * The author's subject; null for an anonymous comment.
+ */
+ user_id?: string | null;
+ author_subject?: string | null;
+ parent_id?: number | null;
+ anon_name?: string | null;
+ message: string;
+ is_deleted?: number | boolean;
+ created_at: string;
+ updated_at?: string | null;
+ username?: string | null;
+ display_name?: string | null;
+ avatar_url?: string | null;
+ profile_color?: string | null;
+ };
+ }
+ | {
+ message: string;
+ };
+
+/** [email protected] (owner: community) */
+/**
+ * community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).
+ */
+export interface CommunityPaste {
+ id: number;
+ app_id: "community";
+ slug: string;
+ /**
+ * The owner's subject (usr_…), or null for an anonymous paste.
+ */
+ user_id: string | null;
+ owner_subject: string | null;
+ /**
+ * user, or ai for a paste an AI service wrote.
+ */
+ origin: string;
+ type: "paste" | "screenshot";
+ title: string;
+ content?: string | null;
+ language?: string | null;
+ visibility: "public" | "unlisted" | "private";
+ /**
+ * The Live stream it was made during, when its stream_ref is one.
+ */
+ stream_id?: number | null;
+ /**
+ * An EntityRef of where it was made (a Live stream, or a service's own source).
+ */
+ stream_ref?: {} | null;
+ screenshot_url?: string | null;
+ /**
+ * Structured metadata a service attached (AI moments), or a screenshot's upload details.
+ */
+ metadata?: {
+ [k: string]: unknown | undefined;
+ };
+ burn_after_read?: boolean;
+ forked_from?: number | null;
+ pinned?: boolean;
+ views?: number;
+ unique_views?: number;
+ copies?: number;
+ likes?: number;
+ is_nsfw?: boolean;
+ ai_summary?: string | null;
+ /**
+ * JSON text as the AI pass wrote it.
+ */
+ ai_tags?: string | null;
+ ai_analyzed_at?: string | null;
+ revision?: number;
+ /**
+ * /p/<slug>
+ */
+ url: string;
+ /**
+ * /p/<slug>/raw
+ */
+ raw_url: string;
+ created_at: string;
+ updated_at?: string | null;
+ /**
+ * Whether the caller owns it.
+ */
+ is_owner: boolean;
+ username?: string | null;
+ /**
+ * OpenVibe AI for an AI paste.
+ */
+ display_name?: string | null;
+ avatar_url?: string | null;
+ profile_color?: string | null;
+}
+
+/** [email protected] (owner: community) */
+/**
+ * community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.
+ */
+export type CommunityModerationRequest =
+ | {
+ visibility: "public" | "hidden" | "locked";
+ }
+ | {
+ [k: string]: unknown | undefined;
+ }
+ | {
+ /**
+ * The space's slug.
+ */
+ space: string;
+ webhook_url_ref: string;
+ /**
+ * Default true.
+ */
+ enabled?: boolean;
+ }
+ | {
+ enabled: boolean;
+ }
+ | NoBody;
+
+/** [email protected] (owner: community) */
+/**
+ * community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.
+ */
+export type CommunityModerationResult =
+ | {
+ thread: {
+ id: number | string;
+ /**
+ * A comment thread's browser id (cth_…).
+ */
+ access_id?: string;
+ ref?: EntityRef;
+ visibility?: string;
+ comment_count?: number | null;
+ space?: string;
+ slug?: string;
+ title?: string;
+ pinned?: boolean;
+ locked?: boolean;
+ };
+ }
+ | {
+ post: {
+ id: number;
+ thread_id: number;
+ is_opening?: boolean;
+ origin?: string;
+ author?: {} | null;
+ body_markdown?: string | null;
+ body_html?: string | null;
+ revision: number;
+ deleted: boolean;
+ created_at?: string | null;
+ updated_at?: string | null;
+ can_edit?: boolean;
+ attachments?: unknown[];
+ pastes?: unknown[];
+ };
+ }
+ | {
+ ok: true;
+ id?: number;
+ deleted?: "thread";
+ }
+ | {
+ [k: string]: unknown | undefined;
+ }
+ | {
+ mapping: {
+ id: number;
+ space: string;
+ direction?: string;
+ webhook_url_ref: string;
+ webhook_configured: boolean;
+ enabled: boolean;
+ created_at?: string | null;
+ };
+ };
+
+/** [email protected] (owner: community) */
+/**
+ * community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).
+ */
+export type CommunityPostWriteRequest =
+ | {
+ [k: string]: unknown | undefined;
+ }
+ | {
+ /**
+ * A thread vote; 0 takes the vote back.
+ */
+ value: 1 | -1 | 0 | "1" | "-1" | "0";
+ }
+ | NoBody;
+
+/** [email protected] (owner: community) */
+/**
+ * community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.
+ */
+export type CommunityPulseWriteResult =
+ | {
+ created: boolean;
+ item: {
+ id: number;
+ source: {
+ service: string;
+ type: string;
+ id: string;
+ };
+ title: string;
+ url: string;
+ origin: "user" | "ai" | "system";
+ label: "AI" | "System" | null;
+ actor: {
+ subject?: string | null;
+ username?: string | null;
+ display_name?: string | null;
+ avatar_url?: string | null;
+ profile_color?: string | null;
+ } | null;
+ occurred_at: string | null;
+ };
+ }
+ | {
+ removed: number;
+ };
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
"GET /api/v1/balances/:subject",
"GET /api/v1/balances/:subject",
"GET /api/v1/transactions",
"GET /api/v1/transactions",
"GET /api/v1/transactions/:id"
"GET /api/v1/transactions/:id"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "billing.balance-read-result@1"
}
}
@@ -21,5 +21,7 @@
@@ -21,5 +21,7 @@
"GET /api/v1/cashouts",
"GET /api/v1/cashouts",
"POST /api/v1/cashouts/:id/approve",
"POST /api/v1/cashouts/:id/approve",
"POST /api/v1/cashouts/:id/deny"
"POST /api/v1/cashouts/:id/deny"
- ]
+ ],
+ "inputSchema": "billing.cashout-decision@1",
+ "outputSchema": "billing.cashout-result@1"
}
}
@@ -20,5 +20,7 @@
@@ -20,5 +20,7 @@
"POST /api/v1/cashouts",
"POST /api/v1/cashouts",
"POST /api/v1/recycle",
"POST /api/v1/recycle",
"GET /api/v1/cashouts/:id"
"GET /api/v1/cashouts/:id"
- ]
+ ],
+ "inputSchema": "billing.cashout-request@1",
+ "outputSchema": "billing.cashout-result@1"
}
}
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
"GET /api/v1/entitlements/:subject",
"GET /api/v1/entitlements/:subject",
"GET /api/v1/subscriptions",
"GET /api/v1/subscriptions",
"GET /api/v1/subscriptions/:id"
"GET /api/v1/subscriptions/:id"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "billing.entitlement-result@1"
}
}
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
"POST /api/v1/intents",
"POST /api/v1/intents",
"GET /api/v1/intents/:id",
"GET /api/v1/intents/:id",
"POST /api/v1/intents/:id/capture"
"POST /api/v1/intents/:id/capture"
- ]
+ ],
+ "inputSchema": "billing.intent-request@1",
+ "outputSchema": "billing.intent-result@1"
}
}
@@ -24,5 +24,7 @@
@@ -24,5 +24,7 @@
"POST /api/v1/admin/provider-events/:id/reprocess",
"POST /api/v1/admin/provider-events/:id/reprocess",
"POST /api/v1/admin/sweep",
"POST /api/v1/admin/sweep",
"GET /api/v1/admin/import-holds"
"GET /api/v1/admin/import-holds"
- ]
+ ],
+ "inputSchema": "billing.admin-request@1",
+ "outputSchema": "billing.admin-result@1"
}
}
@@ -22,5 +22,7 @@
@@ -22,5 +22,7 @@
"POST /api/v1/subscriptions/:id/cancel",
"POST /api/v1/subscriptions/:id/cancel",
"GET /api/v1/subscriptions",
"GET /api/v1/subscriptions",
"GET /api/v1/subscriptions/:id"
"GET /api/v1/subscriptions/:id"
- ]
+ ],
+ "inputSchema": "billing.subscription-request@1",
+ "outputSchema": "billing.subscription-result@1"
}
}
@@ -19,5 +19,7 @@
@@ -19,5 +19,7 @@
"implementedBy": [
"implementedBy": [
"POST /api/v1/transfers",
"POST /api/v1/transfers",
"POST /api/v1/transfers/:id/refund"
"POST /api/v1/transfers/:id/refund"
- ]
+ ],
+ "inputSchema": "billing.transfer-request@1",
+ "outputSchema": "billing.transfer-result@1"
}
}
@@ -18,5 +18,7 @@
@@ -18,5 +18,7 @@
"implementedBy": [
"implementedBy": [
"WS /ws/chat {type:\"chat\"}",
"WS /ws/chat {type:\"chat\"}",
"POST /internal/live/calls"
"POST /internal/live/calls"
- ]
+ ],
+ "inputSchema": "chat.send-request@1",
+ "outputSchema": "chat.send-result@1"
}
}
@@ -23,5 +23,7 @@
@@ -23,5 +23,7 @@
"DELETE /api/v1/spaces/:space/threads/:slug",
"DELETE /api/v1/spaces/:space/threads/:slug",
"PUT|DELETE /api/v1/posts/:id",
"PUT|DELETE /api/v1/posts/:id",
"GET|POST|PUT /api/v1/relay/*"
"GET|POST|PUT /api/v1/relay/*"
- ]
+ ],
+ "inputSchema": "community.moderation-request@1",
+ "outputSchema": "community.moderation-result@1"
}
}
@@ -18,5 +18,6 @@
@@ -18,5 +18,6 @@
],
],
"implementedBy": [
"implementedBy": [
"POST /api/pastes (Community)"
"POST /api/pastes (Community)"
- ]
+ ],
+ "inputSchema": "community.paste-create-request@1"
}
}
@@ -19,5 +19,7 @@
@@ -19,5 +19,7 @@
"POST /api/pastes/:slug/censor",
"POST /api/pastes/:slug/censor",
"POST /api/pastes/bulk",
"POST /api/pastes/bulk",
"DELETE /api/pastes/:slug/comments/:id"
"DELETE /api/pastes/:slug/comments/:id"
- ]
+ ],
+ "inputSchema": "community.paste-moderate-request@1",
+ "outputSchema": "community.paste-moderate-result@1"
}
}
@@ -19,5 +19,7 @@
@@ -19,5 +19,7 @@
"implementedBy": [
"implementedBy": [
"PUT|DELETE /api/pastes/:slug",
"PUT|DELETE /api/pastes/:slug",
"POST /api/pastes/:slug/{fork,like,copy,comments}"
"POST /api/pastes/:slug/{fork,like,copy,comments}"
- ]
+ ],
+ "inputSchema": "community.paste-write-request@1",
+ "outputSchema": "community.paste-write-result@1"
}
}
@@ -23,5 +23,6 @@
@@ -23,5 +23,6 @@
"POST /api/v1/spaces/:space/threads/:slug/votes",
"POST /api/v1/spaces/:space/threads/:slug/votes",
"PUT|DELETE /api/v1/posts/:id",
"PUT|DELETE /api/v1/posts/:id",
"DELETE /api/v1/spaces/:space/threads/:slug"
"DELETE /api/v1/spaces/:space/threads/:slug"
- ]
+ ],
+ "inputSchema": "community.post-write-request@1"
}
}
@@ -17,5 +17,6 @@
@@ -17,5 +17,6 @@
"implementedBy": [
"implementedBy": [
"GET /api/v1/pulse",
"GET /api/v1/pulse",
"GET /pulse"
"GET /pulse"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
@@ -19,5 +19,6 @@
@@ -19,5 +19,6 @@
"implementedBy": [
"implementedBy": [
"POST /api/v1/pulse/items",
"POST /api/v1/pulse/items",
"DELETE /api/v1/pulse/items/:service/:type/:id"
"DELETE /api/v1/pulse/items/:service/:type/:id"
- ]
+ ],
+ "outputSchema": "community.pulse-write-result@1"
}
}
@@ -18,5 +18,6 @@
@@ -18,5 +18,6 @@
"GET /api/v1/spaces",
"GET /api/v1/spaces",
"GET /api/v1/spaces/:space",
"GET /api/v1/spaces/:space",
"GET /s"
"GET /s"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
@@ -21,5 +21,6 @@
@@ -21,5 +21,6 @@
"GET /s/:space/t/:slug",
"GET /s/:space/t/:slug",
"GET /s/feed.xml",
"GET /s/feed.xml",
"GET /s/:space/feed.xml"
"GET /s/:space/feed.xml"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}