openapi: 3.1.0 info: title: goat-zenoh-portal API version: "0.1.3" description: | Vendor self-service portal + topic registry + message inspector for the Zenoh trial fabric. **Authentication.** All `/api/*` endpoints (except `/api/health` and one-time vendor-bundle downloads) require a `Authorization: Bearer ` header. The operator key is provisioned at deploy time; per- vendor keys are minted by `POST /api/vendors`. **Scope.** Vendor-scoped routes (`/api/vendors/{vendor}/topics/...`) accept either the operator key or the matching vendor's key. The server rejects cross-vendor access with `403`. **Wire format.** All request + response bodies are UTF-8 JSON unless explicitly noted (the topic export/import endpoints also speak YAML). contact: name: jailbreak operator url: https://portal.jb.netbird.datalandingzone.net/docs.html license: name: Apache-2.0 servers: - url: https://portal.jb.netbird.datalandingzone.net description: Jailbreak trial portal - url: http://127.0.0.1:8090 description: Local dev security: - bearerAuth: [] components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: portal-api-key description: | Operator key (`PORTAL_OPERATOR_API_KEY`) for full admin, OR a per-vendor key (`vp__…`) for vendor-scoped routes. schemas: Error: type: object properties: error: type: object properties: code: { type: string } message: { type: string } Health: type: object properties: status: { type: string, example: ok } version: { type: string, example: "0.1.3" } uptime: { type: string, example: "7m38.4s" } sunset: { type: string, description: Trial sunset date if set; empty otherwise } Vendor: type: object required: [prefix, display_name, approved, example, created_at] properties: prefix: type: string pattern: "^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]?$" description: Lowercase identifier; becomes the top-level Zenoh namespace. example: homemade-sensors display_name: { type: string } approved: { type: boolean } example: { type: boolean, description: Seeded example vendor (cannot authenticate). } created_at: { type: integer, format: int64, description: Unix epoch seconds } CreateVendorRequest: type: object required: [prefix] properties: prefix: type: string pattern: "^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]?$" display_name: { type: string } auto_approve: type: boolean default: false description: Skip the operator-approval gate. CreateVendorResponse: type: object properties: prefix: { type: string } api_key: type: string description: Bearer token for the vendor. Returned ONCE — only here. Treat like a password. example: vp_homemade-sensors_5f2a… bundle_url: type: string description: Single-use URL for the cert + key + root + onboarding-packet tarball. bundle_token: { type: string } cert_uri_san: type: string example: "urn:goat-jb:vendor=homemade-sensors" approved: { type: boolean } note_to_share: { type: string, description: Operator note (e.g. ACL render warning). Empty when nothing to report. } Topic: type: object required: [id, vendor_prefix, key_pattern, format, created_at] properties: id: { type: integer, format: int64 } vendor_prefix: { type: string } key_pattern: type: string description: Exact Zenoh key expression the producer publishes to. Must start with `//...`. example: homemade-sensors/garage/temperature/v1 format: type: string description: | One of `json`, `cbor`, `msgpack`, `text`, `raw`, or `protobuf:`. The inspector dispatches decode by this string. example: json schema_ref: type: string description: | Pointer into `attest/schema///.../v`. Optional but encouraged for protobuf topics. description: { type: string } publish_to_storage: type: boolean description: Whether zenohd's storage_manager retains this topic for replay (today informational). publish_count: { type: integer, format: int64 } last_seen_at: { type: integer, format: int64, nullable: true } created_at: { type: integer, format: int64 } retired_at: { type: integer, format: int64, nullable: true } CreateTopicRequest: type: object required: [key_pattern, format] properties: key_pattern: { $ref: "#/components/schemas/Topic/properties/key_pattern" } format: { $ref: "#/components/schemas/Topic/properties/format" } schema_ref: { type: string } description: { type: string } publish_to_storage: { type: boolean, default: false } UpdateTopicRequest: type: object description: All fields optional; only present fields are applied (PATCH semantics). properties: format: { type: string, nullable: true } schema_ref: { type: string, nullable: true } description: { type: string, nullable: true } publish_to_storage: { type: boolean, nullable: true } BulkFile: type: object required: [topics] properties: vendor: type: string description: Vendor prefix. When present must match the URL vendor. topics: type: array items: $ref: "#/components/schemas/BulkTopic" BulkTopic: type: object required: [key_pattern, format] properties: key_pattern: { type: string } format: { type: string } schema_ref: { type: string } description: { type: string } publish_to_storage: { type: boolean, default: false } ImportDiff: type: object properties: vendor: { type: string } dry_run: { type: boolean } mode: { type: string, enum: [additive, replace] } entries: type: array items: type: object properties: key_pattern: { type: string } action: { type: string, enum: [create, update, unchanged, retire] } summary: type: object additionalProperties: { type: integer } paths: /api/health: get: summary: Liveness probe + version security: [] tags: [meta] responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Health" } /api/vendors: get: summary: List vendors tags: [vendors] responses: "200": description: OK content: application/json: schema: type: object properties: vendors: type: array items: { $ref: "#/components/schemas/Vendor" } post: summary: Register a new vendor (operator-only) tags: [vendors] description: | Mints a per-vendor X.509 leaf, issues a portal API key, writes the vendor row, and appends an allow rule to the rendered Zenoh ACL. **The `api_key` field in the response is the only place this key appears** — capture it now. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateVendorRequest" } responses: "201": description: Created content: application/json: schema: { $ref: "#/components/schemas/CreateVendorResponse" } "403": description: Operator key required content: application/json: schema: { $ref: "#/components/schemas/Error" } "409": description: Prefix already taken content: application/json: schema: { $ref: "#/components/schemas/Error" } /api/vendors/{vendor}/topics: parameters: - name: vendor in: path required: true schema: { type: string } get: summary: List active topics for a vendor tags: [topics] responses: "200": description: OK content: application/json: schema: type: object properties: topics: type: array items: { $ref: "#/components/schemas/Topic" } post: summary: Register a new topic under a vendor tags: [topics] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CreateTopicRequest" } responses: "201": description: Created content: application/json: schema: { $ref: "#/components/schemas/Topic" } "400": { description: Validation error } "409": { description: Topic with this key_pattern already exists } /api/vendors/{vendor}/topics/{id}: parameters: - name: vendor in: path required: true schema: { type: string } - name: id in: path required: true schema: { type: integer, format: int64 } get: summary: Get a topic by id tags: [topics] responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Topic" } "404": { description: Not found } patch: summary: Update a topic (format / schema_ref / description / publish_to_storage) tags: [topics] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateTopicRequest" } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Topic" } delete: summary: Retire a topic (soft-delete; row preserved for audit) tags: [topics] responses: "204": { description: Retired } /api/vendors/{vendor}/topics/export: parameters: - name: vendor in: path required: true schema: { type: string } - name: format in: query required: false schema: { type: string, enum: [yaml, json], default: yaml } get: summary: Export this vendor's topic set as YAML or JSON tags: [bulk] description: | Returns the full active topic set under `` as a single file. `Content-Disposition: attachment` so browsers download it. Round-trips through `POST /import` cleanly. responses: "200": description: OK content: application/yaml: { schema: { $ref: "#/components/schemas/BulkFile" } } application/json: { schema: { $ref: "#/components/schemas/BulkFile" } } /api/vendors/{vendor}/topics/import: parameters: - name: vendor in: path required: true schema: { type: string } - name: dry_run in: query required: false schema: { type: boolean, default: false } description: Compute the diff without applying it. - name: mode in: query required: false schema: { type: string, enum: [additive, replace], default: additive } description: | `additive` (default): create new + update existing; leave topics absent from the file alone. `replace`: also retire topics that are in the registry but not in the file. post: summary: Bulk-import this vendor's topic set tags: [bulk] description: | Upsert by `(vendor, key_pattern)`. Accepts YAML or JSON based on `Content-Type`; falls back to a YAML-then-JSON sniff when the header is missing. requestBody: required: true content: application/yaml: { schema: { $ref: "#/components/schemas/BulkFile" } } application/json: { schema: { $ref: "#/components/schemas/BulkFile" } } responses: "200": description: Applied (or computed, when `dry_run=true`) content: application/json: schema: { $ref: "#/components/schemas/ImportDiff" } /api/topics/latest: get: summary: Last value for a key expression (single-shot read via Zenoh REST) tags: [inspector] parameters: - name: key in: query required: true schema: { type: string } responses: "200": { description: OK } /api/topics/recent: get: summary: Recent samples for a key expression (Zenoh REST storage-backed read) tags: [inspector] parameters: - name: key in: query required: true schema: { type: string } - name: limit in: query required: false schema: { type: integer, default: 20 } responses: "200": { description: OK } /api/topics/stream: get: summary: Server-Sent Events stream of new publishes for a key expression tags: [inspector] parameters: - name: key in: query required: true schema: { type: string } responses: "200": description: text/event-stream content: text/event-stream: { schema: { type: string } } /api/vendor-bundle/{vendor}/{token}: get: summary: One-time vendor bundle download (no bearer auth; token-gated) tags: [vendors] security: [] parameters: - name: vendor in: path required: true schema: { type: string } - name: token in: path required: true schema: { type: string } responses: "200": description: tarball content: application/gzip: { schema: { type: string, format: binary } } "404": description: Not found / token already consumed tags: - { name: meta, description: Liveness, version } - { name: vendors, description: Vendor registration + listing } - { name: topics, description: Topic CRUD per vendor } - { name: bulk, description: Bulk import / export per vendor } - { name: inspector, description: Live message read-through }