From d6d01f6b50b09a0f67d1ebf693a6b93313a3f4c7 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Sat, 10 May 2025 22:08:35 +1000 Subject: [PATCH 01/17] updated publisher spec Signed-off-by: Chris Langton --- contributors.md | 2 +- spec/publisher/openapi.json | 1266 ++++++++++++++++++++--------------- spec/publisher/openapi.yaml | 1121 +++++++++++++++++++++++++++++++ 3 files changed, 1842 insertions(+), 547 deletions(-) create mode 100644 spec/publisher/openapi.yaml diff --git a/contributors.md b/contributors.md index 5327a8b..4b6e8e2 100644 --- a/contributors.md +++ b/contributors.md @@ -9,6 +9,7 @@ work during our meetings and workshops. They are noted here in alphabetic order: * Anthony Harrison, APH10, United Kingdom * Olle E. Johansson, Project lead, Edvina AB, Sweden, oej@edvina.net +* Christopher Langton * Mark Symons * Paul Horton * Pavel Shukhman, Reliza, Canada, pavel@reliza.io @@ -16,4 +17,3 @@ work during our meetings and workshops. They are noted here in alphabetic order: * Steve Springett * Valerio Mulas * Viktor Peterson - diff --git a/spec/publisher/openapi.json b/spec/publisher/openapi.json index 261986d..3a06822 100644 --- a/spec/publisher/openapi.json +++ b/spec/publisher/openapi.json @@ -4,7 +4,7 @@ "info": { "title": "Transparency Exchange API", "summary": "Transparency Exchange API specification for consumers and publishers", - "description": "TBC", + "description": "The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations.", "contact": { "name": "TEA Working Group", "email": "TBC", @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE" }, - "version": "0.0.2" + "version": "0.0.3" }, "servers": [ { @@ -25,7 +25,7 @@ "paths": { "/product/{tei_urn}": { "get": { - "description": "Returns the corresponding leafs for a given TEI URN", + "description": "Returns the corresponding releases for a given TEI URN", "operationId": "getTeaProduct", "parameters": [ { @@ -43,11 +43,9 @@ } } }, - "404": { "$ref": "#/components/responses/404-object-by-id-not-found" } - }, "tags": [ "TEA Product" @@ -158,7 +156,22 @@ "operationId": "listTeaProducts", "parameters": [ { - "$ref": "#/components/parameters/standard_pagination" + "$ref": "#/components/parameters/page-offset" + }, + { + "$ref": "#/components/parameters/page-size" + }, + { + "$ref": "#/components/parameters/purl" + }, + { + "$ref": "#/components/parameters/barcode" + }, + { + "$ref": "#/components/parameters/sku" + }, + { + "$ref": "#/components/parameters/vendor_uuid" } ], "responses": { @@ -172,8 +185,20 @@ } } }, - "$ref": "#/components/responses/standard_errors" - } + "400": { + "description": "Invalid request body", + "content": { + "application/json": {} + } + }, + "401": { "$ref": "#/components/responses/401-unauthorized" }, + "404": { + "$ref": "#/components/responses/404-object-by-id-not-found" + } + }, + "tags": [ + "TEA Product" + ] }, "post": { "description": "Create TEA Product entry for the supplied product identifier", @@ -203,6 +228,15 @@ "purl": { "type": "string", "description": "Package URL (PURL)" + }, + "tei_urns": { + "type": "array", + "items": { + "type": "string", + "description": "Transparency Exchange Identifier URN", + "pattern": "^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\\.-]+:.+$" + }, + "description": "List of TEI URNs associated with this product" } }, "required": [ @@ -223,7 +257,10 @@ "barcode": "123456789012", "sku": "PROD-001", "vendor_uuid": "123e4567-e89b-12d3-a456-426614174000", - "purl": "pkg:generic/example@1.0.0" + "purl": "pkg:generic/example@1.0.0", + "tei_urns": [ + "urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1" + ] } } } @@ -255,6 +292,12 @@ }, "purl": { "type": "string" + }, + "tei_urns": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -289,10 +332,10 @@ ] } }, - "/leaf": { + "/component": { "post": { - "description": "Create a new TEA Leaf entry", - "operationId": "createTeaLeaf", + "description": "Create a new TEA Component entry", + "operationId": "createTeaComponent", "requestBody": { "required": true, "content": { @@ -301,35 +344,37 @@ "type": "object", "properties": { "tea_product_identifier": { - "type": "string" - }, - "product_version": { - "type": "string" + "$ref": "#/components/schemas/type_uuid" }, - "release_date": { + "name": { "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" + "description": "Component name" }, - "pre_release": { - "type": "boolean" + "identifiers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/identifier" + }, + "description": "List of identifiers for the component" } }, "required": [ "tea_product_identifier", - "product_version", - "release_date", - "pre_release" + "name" ] }, "examples": { "basic": { - "summary": "Basic leaf creation", + "summary": "Basic component creation", "value": { "tea_product_identifier": "123e4567-e89b-12d3-a456-426614174000", - "product_version": "1.0.0", - "release_date": "2024-03-20T15:30:00Z", - "pre_release": false + "name": "Example Component", + "identifiers": [ + { + "idType": "purl", + "idValue": "pkg:maven/org.example/component@1.0.0" + } + ] } } } @@ -338,11 +383,11 @@ }, "responses": { "201": { - "description": "TEA Leaf created successfully", + "description": "TEA Component created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/tea_leaf" + "$ref": "#/components/schemas/tea_component" } } } @@ -359,7 +404,7 @@ } }, "tags": [ - "TEA Leaf" + "TEA Component" ], "security": [ { @@ -368,25 +413,32 @@ { "basicAuth": [] } - ] +] } }, - "/leaf/{tea_leaf_identifier}": { + "/component/{component_identifier}": { "get": { - "description": "Get the TEA Leaf that describes the Version of a Product", - "operationId": "getTeaLeaf", + "description": "Get information about a TEA Component", + "operationId": "getTeaComponent", "parameters": [ { - "$ref": "#/components/parameters/tea_leaf_identifier" + "name": "component_identifier", + "in": "path", + "required": true, + "description": "UUID of TEA Component in the TEA server", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { "200": { - "description": "Requested TEA Leaf found and returned", + "description": "Requested TEA Component found and returned", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/tea_leaf" + "$ref": "#/components/schemas/tea_component" } } } @@ -396,15 +448,22 @@ } }, "tags": [ - "TEA Leaf" + "TEA Component" ] }, "patch": { - "description": "Update an existing TEA Leaf entry", - "operationId": "updateTeaLeaf", + "description": "Update an existing TEA Component entry", + "operationId": "updateTeaComponent", "parameters": [ { - "$ref": "#/components/parameters/tea_leaf_identifier" + "name": "component_identifier", + "in": "path", + "required": true, + "description": "UUID of TEA Component in the TEA server", + "schema": { + "type": "string", + "format": "uuid" + } } ], "requestBody": { @@ -414,19 +473,16 @@ "schema": { "type": "object", "properties": { - "product_name": { - "type": "string" - }, - "product_version": { - "type": "string" - }, - "release_date": { + "name": { "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" + "description": "Component name" }, - "pre_release": { - "type": "boolean" + "identifiers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/identifier" + }, + "description": "List of identifiers for the component" } } } @@ -435,11 +491,11 @@ }, "responses": { "200": { - "description": "TEA Leaf updated successfully", + "description": "TEA Component updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/tea_leaf" + "$ref": "#/components/schemas/tea_component" } } } @@ -456,7 +512,7 @@ } }, "tags": [ - "TEA Leaf" + "TEA Component" ], "security": [ { @@ -468,23 +524,30 @@ ] }, "delete": { - "description": "Delete a TEA Leaf entry", - "operationId": "deleteTeaLeaf", + "description": "Delete a TEA Component entry", + "operationId": "deleteTeaComponent", "parameters": [ { - "$ref": "#/components/parameters/tea_leaf_identifier" + "name": "component_identifier", + "in": "path", + "required": true, + "description": "UUID of TEA Component in the TEA server", + "schema": { + "type": "string", + "format": "uuid" + } } ], "$ref": "#/components/operations/standard_delete", "tags": [ - "TEA Leaf" + "TEA Component" ] } }, - "/collection": { + "/release": { "post": { - "description": "Create a new TEA Collection", - "operationId": "createTeaCollection", + "description": "Create a new TEA Release", + "operationId": "createTeaRelease", "requestBody": { "required": true, "content": { @@ -492,146 +555,50 @@ "schema": { "type": "object", "properties": { - "tea_leaf_identifier": { - "$ref": "#/components/schemas/type_uuid" - }, - "product_name": { - "type": "string" + "component_identifier": { + "$ref": "#/components/schemas/type_uuid", + "description": "UUID of the TEA Component" }, - "product_version": { - "type": "string" + "version": { + "type": "string", + "description": "Version number" }, "release_date": { "type": "string", "format": "date-time", - "example": "2024-03-20T15:30:00Z" - }, - "author": { - "$ref": "#/components/schemas/type_author" + "example": "2024-03-20T15:30:00Z", + "description": "Timestamp of the release" }, - "reason": { - "$ref": "#/components/schemas/type_collection_reason" + "pre_release": { + "type": "boolean", + "description": "Flag indicating pre-release status" }, - "artifacts": { + "identifiers": { "type": "array", "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/type_tea_collection_artifact_type" - }, - "author": { - "$ref": "#/components/schemas/type_author" - }, - "objects": { - "type": "array", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "mime_type": { - "type": "string" - }, - "artifact_url": { - "type": "string", - "format": "uri" - }, - "artifact_size_in_bytes": { - "type": "integer", - "format": "int64" - }, - "artifact_checksum": { - "type": "string", - "description": "The checksum value" - }, - "artifact_checksum_type": { - "type": "string", - "description": "The algorithm used to generate the checksum", - "enum": [ - "SHA512", - "SHA384", - "SHA256", - "SHA224", - "SHA1", - "MD5", - "BLAKE2b-512", - "BLAKE2s-256", - "BLAKE3" - ], - "default": "SHA256" - }, - "signature_url": { - "type": "string", - "format": "uri" - } - }, - "required": [ - "description", - "mime_type", - "artifact_url", - "artifact_size_in_bytes", - "artifact_checksum", - "artifact_checksum_type" - ] - } - } - }, - "required": [ - "name", - "type", - "author", - "objects" - ] - } + "$ref": "#/components/schemas/identifier" + }, + "description": "List of identifiers for the release" } }, "required": [ - "tea_leaf_identifier", - "product_name", - "product_version", - "release_date", - "author", - "reason" + "component_identifier", + "version", + "release_date" ] }, "examples": { "basic": { - "summary": "Basic collection creation", + "summary": "Basic release creation", "value": { - "tea_leaf_identifier": "123e4567-e89b-12d3-a456-426614174000", - "product_name": "Example Product", - "product_version": "1.0.0", + "component_identifier": "123e4567-e89b-12d3-a456-426614174000", + "version": "1.0.0", "release_date": "2024-03-20T15:30:00Z", - "author": { - "name": "John Doe", - "email": "john.doe@example.com", - "organization": "Example Org" - }, - "reason": "New Product Release", - "artifacts": [ + "pre_release": false, + "identifiers": [ { - "name": "Example Artifact", - "type": "bom", - "author": { - "name": "John Doe", - "email": "john.doe@example.com", - "organization": "Example Org" - }, - "objects": [ - { - "description": "Example SBOM", - "mime_type": "application/json", - "artifact_url": "https://example.com/sbom.json", - "artifact_size_in_bytes": 1024, - "artifact_checksum": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", - "artifact_checksum_type": "SHA256" - } - ] + "idType": "purl", + "idValue": "pkg:maven/org.example/component@1.0.0" } ] } @@ -642,17 +609,17 @@ }, "responses": { "201": { - "description": "TEA Collection created successfully", + "description": "TEA Release created successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/tea_collection" + "$ref": "#/components/schemas/tea_release" } } } }, "400": { - "description": "Invalid request body", + "description": "Invalid parameters", "content": { "application/json": {} } @@ -663,7 +630,7 @@ } }, "tags": [ - "TEA Collection" + "TEA Release" ], "security": [ { @@ -675,22 +642,29 @@ ] } }, - "/collection/{tea_collection_identifier}": { + "/release/{release_identifier}": { "get": { - "description": "Get a TEA Collection by it's Identifier", - "operationId": "getTeaCollection", + "description": "Get information about a TEA Release", + "operationId": "getTeaRelease", "parameters": [ { - "$ref": "#/components/parameters/tea_collection_identifier" + "name": "release_identifier", + "in": "path", + "required": true, + "description": "UUID of TEA Release in the TEA server", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { "200": { - "description": "Requested TEA Collection found and returned", + "description": "Requested TEA Release found and returned", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/tea_collection" + "$ref": "#/components/schemas/tea_release" } } } @@ -700,15 +674,22 @@ } }, "tags": [ - "TEA Collection" + "TEA Release" ] }, "patch": { - "description": "Update an existing TEA Collection", - "operationId": "updateTeaCollection", + "description": "Update an existing TEA Release entry", + "operationId": "updateTeaRelease", "parameters": [ { - "$ref": "#/components/parameters/tea_collection_identifier" + "name": "release_identifier", + "in": "path", + "required": true, + "description": "UUID of TEA Release in the TEA server", + "schema": { + "type": "string", + "format": "uuid" + } } ], "requestBody": { @@ -718,22 +699,103 @@ "schema": { "type": "object", "properties": { - "product_name": { - "type": "string" - }, - "product_version": { - "type": "string" + "version": { + "type": "string", + "description": "Version number" }, "release_date": { "type": "string", "format": "date-time", "example": "2024-03-20T15:30:00Z" }, - "author": { - "$ref": "#/components/schemas/type_author" + "pre_release": { + "type": "boolean", + "description": "May only be set to false if previously true" + }, + "identifiers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/identifier" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "TEA Release updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tea_release" + } + } + } + }, + "400": { + "description": "Invalid request body", + "content": { + "application/json": {} + } + }, + "401": { "$ref": "#/components/responses/401-unauthorized" }, + "404": { + "$ref": "#/components/responses/404-object-by-id-not-found" + } + }, + "tags": [ + "TEA Release" + ], + "security": [ + { + "bearerAuth": [] + }, + { + "basicAuth": [] + } + ] + }, + "delete": { + "description": "Delete a TEA Release entry", + "operationId": "deleteTeaRelease", + "parameters": [ + { + "name": "release_identifier", + "in": "path", + "required": true, + "description": "UUID of TEA Release in the TEA server", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "$ref": "#/components/operations/standard_delete", + "tags": [ + "TEA Release" + ] + } + }, + "/collection": { + "post": { + "description": "Create a new TEA Collection", + "operationId": "createTeaCollection", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "release_identifier": { + "$ref": "#/components/schemas/type_uuid", + "description": "UUID of the TEA Release" }, - "reason": { - "$ref": "#/components/schemas/type_collection_reason" + "update_reason": { + "$ref": "#/components/schemas/collection_update_reason", + "description": "Reason for collection creation or update" }, "artifacts": { "type": "array", @@ -741,56 +803,51 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "Artifact name" }, "type": { - "$ref": "#/components/schemas/type_tea_collection_artifact_type" + "$ref": "#/components/schemas/type_tea_collection_artifact_type", + "description": "Type of artifact" }, "author": { - "$ref": "#/components/schemas/type_author" + "$ref": "#/components/schemas/type_author", + "description": "Author of the artifact" }, - "objects": { + "formats": { "type": "array", "items": { "type": "object", "properties": { "description": { - "type": "string" + "type": "string", + "description": "A free text describing the artifact" }, "mime_type": { - "type": "string" + "type": "string", + "description": "The MIME type of the document" }, "artifact_url": { "type": "string", - "format": "uri" + "format": "uri", + "description": "Direct download URL for the artifact" }, "artifact_size_in_bytes": { "type": "integer", - "format": "int64" - }, - "artifact_checksum": { - "type": "string", - "description": "The checksum value" + "format": "int64", + "description": "Size of the artifact in bytes" }, - "artifact_checksum_type": { - "type": "string", - "description": "The algorithm used to generate the checksum", - "enum": [ - "SHA512", - "SHA384", - "SHA256", - "SHA224", - "SHA1", - "MD5", - "BLAKE2b-512", - "BLAKE2s-256", - "BLAKE3" - ], - "default": "SHA256" + "artifact_checksums": { + "type": "array", + "items": { + "$ref": "#/components/schemas/artifact_checksum" + }, + "description": "List of checksums for the artifact" }, "signature_url": { "type": "string", - "format": "uri" + "format": "uri", + "description": "URL for an external signature of the artifact" } }, "required": [ @@ -798,8 +855,7 @@ "mime_type", "artifact_url", "artifact_size_in_bytes", - "artifact_checksum", - "artifact_checksum_type" + "artifact_checksums" ] } } @@ -808,25 +864,60 @@ "name", "type", "author", - "objects" + "formats" ] } } }, "required": [ - "product_name", - "product_version", - "release_date", - "author", - "reason" + "release_identifier", + "update_reason" ] + }, + "examples": { + "basic": { + "summary": "Basic collection creation", + "value": { + "release_identifier": "123e4567-e89b-12d3-a456-426614174000", + "update_reason": { + "type": "INITIAL_RELEASE", + "comment": "Initial release of collection" + }, + "artifacts": [ + { + "name": "Example SBOM", + "type": "bom", + "author": { + "name": "John Doe", + "email": "john.doe@example.com", + "organization": "Example Org" + }, + "formats": [ + { + "description": "CycloneDX SBOM (XML)", + "mime_type": "application/vnd.cyclonedx+xml", + "artifact_url": "https://example.com/sbom.xml", + "artifact_size_in_bytes": 1024, + "artifact_checksums": [ + { + "algType": "SHA-256", + "algValue": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" + } + ], + "signature_url": "https://example.com/sbom.xml.sig" + } + ] + } + ] + } + } } } } }, "responses": { - "200": { - "description": "TEA Collection updated successfully", + "201": { + "description": "TEA Collection created successfully", "content": { "application/json": { "schema": { @@ -857,11 +948,40 @@ "basicAuth": [] } ] - }, - "delete": { - "description": "Delete a TEA Collection entry", - "operationId": "deleteTeaCollection", - "parameters": [ + } + }, + "/collection/{collection_identifier}": { + "get": { + "description": "Get a TEA Collection by its Identifier", + "operationId": "getTeaCollection", + "parameters": [ + { + "$ref": "#/components/parameters/tea_collection_identifier" + } + ], + "responses": { + "200": { + "description": "Requested TEA Collection found and returned", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tea_collection" + } + } + } + }, + "404": { + "$ref": "#/components/responses/404-object-by-id-not-found" + } + }, + "tags": [ + "TEA Collection" + ] + }, + "delete": { + "description": "Delete a TEA Collection entry", + "operationId": "deleteTeaCollection", + "parameters": [ { "$ref": "#/components/parameters/tea_collection_identifier" } @@ -871,275 +991,380 @@ "TEA Collection" ] } + }, + "/collection/{collection_identifier}/{version}": { + "get": { + "description": "Get a specific version of a TEA Collection", + "operationId": "getTeaCollectionVersion", + "parameters": [ + { + "$ref": "#/components/parameters/tea_collection_identifier" + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "Version of the TEA Collection", + "schema": { + "type": "integer", + "minimum": 1 + } + } + ], + "responses": { + "200": { + "description": "Requested TEA Collection version found and returned", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tea_collection" + } + } + } + }, + "404": { + "$ref": "#/components/responses/404-object-by-id-not-found" + } + }, + "tags": [ + "TEA Collection" + ] + } } }, - "webhooks": {}, "components": { "schemas": { - "base_product_fields": { + "identifier": { "type": "object", + "description": "An identifier with a specified type", "properties": { - "product_name": { - "type": "string" - }, - "barcode": { + "idType": { + "description": "Type of identifier, e.g. `tei`, `purl`, `cpe`", "type": "string", - "description": "Barcode" + "enum": [ + "cpe", + "tei", + "purl" + ] }, - "sku": { + "idValue": { + "description": "Identifier value", + "type": "string" + } + }, + "required": [ + "idType", + "idValue" + ] + }, + "artifact_checksum": { + "type": "object", + "properties": { + "algType": { + "description": "Checksum algorithm", "type": "string", - "description": "Product SKU" + "enum": [ + "MD5", + "SHA-1", + "SHA-256", + "SHA-384", + "SHA-512", + "SHA3-256", + "SHA3-384", + "SHA3-512", + "BLAKE2b-256", + "BLAKE2b-384", + "BLAKE2b-512", + "BLAKE3" + ] }, - "vendor_uuid": { + "algValue": { + "type": "string", + "description": "Checksum value" + } + }, + "required": [ + "algType", + "algValue" + ] + }, + "tea_product": { + "type": "object", + "properties": { + "identifier": { "$ref": "#/components/schemas/type_uuid", - "description": "Vendor UUID" + "description": "A unique identifier for the TEA product" }, - "purl": { + "name": { "type": "string", - "description": "Package URL (PURL)" + "description": "Product name" + }, + "identifiers": { + "type": "array", + "description": "List of identifiers for the product", + "items": { + "$ref": "#/components/schemas/identifier" + } + }, + "components": { + "type": "array", + "description": "List of TEA component UUIDs", + "items": { + "$ref": "#/components/schemas/type_uuid" + } + }, + "tei_urns": { + "type": "array", + "items": { + "type": "string", + "description": "Transparency Exchange Identifier URN", + "pattern": "^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\\.-]+:.+$" + }, + "description": "List of TEI URNs associated with this product" } - } + }, + "required": [ + "identifier", + "name", + "identifiers", + "components" + ] }, - "base_versioned_product": { + "paginated_product_list": { "allOf": [ - { "$ref": "#/components/schemas/base_product_fields" }, + { + "$ref": "#/components/schemas/pagination_details" + }, { "type": "object", "properties": { - "product_version": { - "type": "string" - }, - "release_date": { - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/tea_product" + } } } } ] }, - "base_artifact_object": { + "tea_component": { "type": "object", + "description": "A TEA component", "properties": { - "identifier": { + "uuid": { + "description": "A unique identifier for the TEA component", "$ref": "#/components/schemas/type_uuid" }, - "description": { - "type": "string" - }, - "mime_type": { - "type": "string" - }, - "artifact_url": { + "name": { "type": "string", - "format": "uri" + "description": "Component name" }, - "artifact_size_in_bytes": { - "type": "integer", - "format": "int64" + "identifiers": { + "type": "array", + "description": "List of identifiers for the component", + "items": { + "$ref": "#/components/schemas/identifier" + } + }, + "releases": { + "type": "array", + "description": "List of UUIDs referencing TEA Release entries", + "items": { + "$ref": "#/components/schemas/type_uuid" + } + } + }, + "required": [ + "uuid", + "name", + "identifiers" + ] + }, + "tea_release": { + "type": "object", + "description": "A TEA release", + "properties": { + "uuid": { + "description": "A unique identifier for the TEA Component Release", + "$ref": "#/components/schemas/type_uuid" }, - "artifact_checksum": { + "version": { + "description": "Version number", "type": "string", - "description": "The checksum value" + "example": "1.2.3" }, - "artifact_checksum_type": { + "releaseDate": { + "description": "Timestamp of the release (for sorting purposes)", "type": "string", - "description": "The algorithm used to generate the checksum", - "enum": [ - "SHA512", - "SHA384", - "SHA256", - "SHA224", - "SHA1", - "MD5", - "BLAKE2b-512", - "BLAKE2s-256", - "BLAKE3" - ], - "default": "SHA256" - } - } - }, - "tea_product": { - "allOf": [ - { "$ref": "#/components/schemas/base_product_fields" }, - { - "type": "object", - "properties": { - "identifier": { - "$ref": "#/components/schemas/type_uuid" - }, - "leaf_references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/type_uuid" - }, - "description": "Array of UUIDs referencing TEA Leaf entries" - } + "format": "date-time", + "example": "2024-03-20T15:30:00Z" + }, + "preRelease": { + "type": "boolean", + "description": "A flag indicating pre-release (or beta) status" + }, + "identifiers": { + "type": "array", + "description": "List of identifiers for the component", + "items": { + "$ref": "#/components/schemas/identifier" + } + }, + "collection_references": { + "type": "array", + "items": { + "$ref": "#/components/schemas/type_uuid" }, - "required": ["identifier", "product_name"] + "description": "Array of UUIDs referencing TEA Collection entries", + "default": [] } + }, + "required": [ + "uuid", + "version", + "releaseDate" ] }, - "tea_leaf": { - "allOf": [ - { "$ref": "#/components/schemas/base_versioned_product" }, - { - "type": "object", - "properties": { - "identifier": { - "$ref": "#/components/schemas/type_uuid" - }, - "pre_release": { - "type": "boolean" - }, - "end_of_life": { - "type": "string", - "format": "date-time" - }, - "collection_references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/type_uuid" - }, - "description": "Array of UUIDs referencing TEA Collection entries", - "default": [] - } - }, - "required": [ - "identifier", - "product_name", - "product_version", - "release_date", - "pre_release" + "collection_update_reason": { + "type": "object", + "description": "Reason for the update to the TEA collection", + "properties": { + "type": { + "description": "Type of update reason", + "type": "string", + "enum": [ + "INITIAL_RELEASE", + "VEX_UPDATED", + "ARTIFACT_UPDATED", + "ARTIFACT_ADDED", + "ARTIFACT_REMOVED" ] + }, + "comment": { + "type": "string", + "description": "Free text description" } + }, + "required": [ + "type" ] }, "tea_collection": { "type": "object", + "description": "A collection of security-related documents", "properties": { - "identifier": { + "uuid": { + "description": "UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object.", "$ref": "#/components/schemas/type_uuid" }, - "product_name": { - "type": "string" - }, - "product_version": { - "type": "string" + "version": { + "type": "integer", + "description": "TEA Collection version, incremented each time its content changes. Versions start with 1.", + "minimum": 1 }, - "release_date": { + "releaseDate": { + "description": "The date when the TEA Collection version was created.", "type": "string", "format": "date-time", "example": "2024-03-20T15:30:00Z" }, - "author": { - "$ref": "#/components/schemas/type_author" - }, - "reason": { - "$ref": "#/components/schemas/type_collection_reason" + "updateReason": { + "description": "Reason for the update/release of the TEA Collection object.", + "$ref": "#/components/schemas/collection_update_reason" }, "artifacts": { "type": "array", + "description": "List of TEA artifact objects.", "items": { - "$ref": "#/components/schemas/tea_collection_artifact" - }, - "default": [] + "$ref": "#/components/schemas/tea_artifact" + } } }, "required": [ - "identifier", - "product_name", - "product_version", - "release_date", - "author", - "reason" + "uuid", + "version", + "releaseDate", + "updateReason" ] }, - "tea_collection_artifact": { + "tea_artifact": { "type": "object", + "description": "A security-related document", "properties": { - "identifier": { + "uuid": { + "description": "UUID of the TEA Artifact object.", "$ref": "#/components/schemas/type_uuid" }, "name": { - "type": "string" + "type": "string", + "description": "Artifact name" }, "type": { + "description": "Type of artifact", "$ref": "#/components/schemas/type_tea_collection_artifact_type" }, "author": { - "$ref": "#/components/schemas/type_author" + "$ref": "#/components/schemas/type_author", + "description": "Author of the artifact" }, - "objects": { + "formats": { "type": "array", + "description": "List of objects with the same content, but in different formats.", "items": { - "$ref": "#/components/schemas/tea_collection_artifact_object" - }, - "minItems": 1 + "$ref": "#/components/schemas/artifact_format" + } } }, "required": [ - "identifier", + "uuid", "name", "type", "author", - "objects" + "formats" ] }, - "tea_collection_artifact_object": { + "artifact_format": { "type": "object", + "description": "A security-related document in a specific format", "properties": { - "identifier": { - "$ref": "#/components/schemas/type_uuid" + "mime_type": { + "type": "string", + "description": "The MIME type of the document" }, "description": { - "type": "string" - }, - "mime_type": { - "type": "string" + "type": "string", + "description": "A free text describing the artifact" }, "artifact_url": { "type": "string", - "format": "uri" + "format": "uri", + "description": "Direct download URL for the artifact" }, "artifact_size_in_bytes": { "type": "integer", - "format": "int64" - }, - "artifact_checksum": { - "type": "string", - "description": "The checksum value" + "format": "int64", + "description": "Size of the artifact in bytes" }, - "artifact_checksum_type": { - "type": "string", - "description": "The algorithm used to generate the checksum", - "enum": [ - "SHA512", - "SHA384", - "SHA256", - "SHA224", - "SHA1", - "MD5", - "BLAKE2b-512", - "BLAKE2s-256", - "BLAKE3" - ], - "default": "SHA256" + "artifact_checksums": { + "type": "array", + "items": { + "$ref": "#/components/schemas/artifact_checksum" + }, + "description": "List of checksums for the artifact" }, "signature_url": { "type": "string", - "format": "uri" + "format": "uri", + "description": "Direct download URL for an external signature of the artifact" } }, "required": [ - "identifier", - "description", "mime_type", + "description", "artifact_url", "artifact_size_in_bytes", - "artifact_checksum", - "artifact_checksum_type" + "artifact_checksums" ] }, "type_author": { @@ -1162,46 +1387,6 @@ "organization" ] }, - "type_collection_reason": { - "type": "string", - "description": "Event requiring this TEA Collection was published", - "enum": [ - "New Product Release", - "BOM Updated", - "Attestation Added", - "Attestation Updated" - ] - }, - "type_pagination_details": { - "type": "object", - "properties": { - "timestamp": { - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" - }, - "page_start_index": { - "type": "number", - "format": "int64", - "default": 0 - }, - "page_size": { - "type": "number", - "format": "int64", - "default": 100 - }, - "total_results": { - "type": "number", - "format": "int64" - } - }, - "required": [ - "timestamp", - "page_start_index", - "page_size", - "total_results" - ] - }, "type_tea_collection_artifact_type": { "type": "string", "title": "Type", @@ -1302,28 +1487,47 @@ "type_uuid": { "type": "string", "format": "uuid" - } - }, - "responses": { - "base_error": { + }, + "pagination_details": { "type": "object", "properties": { - "error": { - "type": "string" + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2024-03-20T15:30:00Z" }, - "message": { - "type": "string" + "pageStartIndex": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "pageSize": { + "type": "integer", + "format": "int64", + "default": 100 + }, + "totalResults": { + "type": "integer", + "format": "int64" } - } - }, - "404-object-by-id-not-found": { - "description": "Object requested by identifier not found", + }, + "required": [ + "timestamp", + "pageStartIndex", + "pageSize", + "totalResults" + ] + } + }, + "responses": { + "401-unauthorized": { + "description": "Authentication required", "content": { "application/json": {} } }, - "401-unauthorized": { - "description": "Authentication required", + "404-object-by-id-not-found": { + "description": "Object requested by identifier not found", "content": { "application/json": {} } @@ -1331,23 +1535,23 @@ }, "parameters": { "page-offset": { - "name": "page-offset", + "name": "pageOffset", "description": "Pagination offset", "in": "query", "required": false, "schema": { - "type": "number", + "type": "integer", "format": "int64", "default": 0 } }, "page-size": { - "name": "page-size", - "description": "Pagination offset", + "name": "pageSize", + "description": "Pagination size", "in": "query", "required": false, "schema": { - "type": "number", + "type": "integer", "format": "int64", "default": 100 } @@ -1358,7 +1562,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "pattern": "^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\\.-]+:.+$" } }, "tea_product_identifier": { @@ -1367,34 +1572,18 @@ "in": "path", "required": true, "schema": { - "type": "string" - } - }, - "product_version": { - "name": "product_version", - "description": "Product Version string", - "in": "path", - "required": true, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, "tea_collection_identifier": { - "name": "tea_collection_identifier", + "name": "collection_identifier", "description": "TEA Collection Identifier", "in": "path", "required": true, "schema": { - "type": "string" - } - }, - "tea_leaf_identifier": { - "name": "tea_leaf_identifier", - "description": "TEA Leaf Identifier", - "in": "path", - "required": true, - "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, "purl": { @@ -1444,55 +1633,27 @@ "scheme": "basic" } }, - "security_requirements": { - "standard_auth": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - }, - "responses": { - "standard_errors": { - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { - "$ref": "#/components/responses/401-unauthorized" - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "standard_delete": { - "204": { - "description": "Resource successfully deleted" - } - } - }, - "parameters": { - "standard_pagination": { - "page_offset": { - "$ref": "#/components/parameters/page-offset" - }, - "page_size": { - "$ref": "#/components/parameters/page-size" - } - } - }, "operations": { "standard_delete": { "responses": { - "$ref": "#/components/responses/standard_delete" + "204": { + "description": "Resource successfully deleted" + }, + "401": { + "$ref": "#/components/responses/401-unauthorized" + }, + "404": { + "$ref": "#/components/responses/404-object-by-id-not-found" + } }, - "security": { - "$ref": "#/components/security_requirements/standard_auth" - } + "security": [ + { + "bearerAuth": [] + }, + { + "basicAuth": [] + } + ] } } }, @@ -1505,9 +1666,22 @@ } ], "tags": [ - "TEA Collection", - "TEA Product", - "TEA Leaf" + { + "name": "TEA Product", + "description": "Operations related to TEA Products" + }, + { + "name": "TEA Component", + "description": "Operations related to TEA Components" + }, + { + "name": "TEA Release", + "description": "Operations related to TEA Releases" + }, + { + "name": "TEA Collection", + "description": "Operations related to TEA Collections" + } ], "externalDocs": { "description": "Transparency Exchange API specification", diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml new file mode 100644 index 0000000..f31ffdd --- /dev/null +++ b/spec/publisher/openapi.yaml @@ -0,0 +1,1121 @@ +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base +openapi: 3.1.1 +info: + title: Transparency Exchange API + summary: Transparency Exchange API specification for consumers and publishers + description: The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations. + contact: + name: TEA Working Group + email: TBC + url: https://github.com/CycloneDX/transparency-exchange-api + license: + name: Apache 2.0 + url: https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE + version: 0.0.3 +servers: + - url: http://localhost/tea/v1 + description: Local development +paths: + /product/{tei_urn}: + get: + description: Returns the corresponding releases for a given TEI URN + operationId: getTeaProduct + parameters: + - $ref: '#/components/parameters/tei_urn' + responses: + '200': + description: Requested TEA Product found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_product' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Product + patch: + description: Update an existing TEA Product entry + operationId: updateTeaProduct + parameters: + - $ref: '#/components/parameters/tea_product_identifier' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + product_name: + type: string + barcode: + type: string + description: Barcode + sku: + type: string + description: Product SKU + vendor_uuid: + $ref: '#/components/schemas/type_uuid' + description: Vendor UUID + purl: + type: string + description: Package URL (PURL) + examples: + basic: + summary: Basic product update + value: + product_name: Updated Product Name + barcode: '123456789012' + sku: PROD-001 + vendor_uuid: 123e4567-e89b-12d3-a456-426614174000 + purl: pkg:generic/example@1.0.0 + responses: + '200': + description: TEA Product updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/tea_product' + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Product + security: + - bearerAuth: [] + - basicAuth: [] + delete: + description: Delete a TEA Product entry + operationId: deleteTeaProduct + parameters: + - $ref: '#/components/parameters/tea_product_identifier' + $ref: '#/components/operations/standard_delete' + tags: + - TEA Product + /product: + get: + description: Returns a list of TEA Products + operationId: listTeaProducts + parameters: + - $ref: '#/components/parameters/page-offset' + - $ref: '#/components/parameters/page-size' + - $ref: '#/components/parameters/purl' + - $ref: '#/components/parameters/barcode' + - $ref: '#/components/parameters/sku' + - $ref: '#/components/parameters/vendor_uuid' + responses: + '200': + description: List retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/paginated_product_list' + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Product + post: + description: Create TEA Product entry for the supplied product identifier + operationId: createTeaProduct + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + product_name: + type: string + barcode: + type: string + description: Barcode + sku: + type: string + description: Product SKU + vendor_uuid: + $ref: '#/components/schemas/type_uuid' + description: Vendor UUID + purl: + type: string + description: Package URL (PURL) + tei_urns: + type: array + items: + type: string + description: Transparency Exchange Identifier URN + pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ + description: List of TEI URNs associated with this product + required: + - product_name + examples: + basic: + summary: Basic product creation + value: + product_name: Example Product + full: + summary: Full product creation with all fields + value: + product_name: Complete Product Example + barcode: '123456789012' + sku: PROD-001 + vendor_uuid: 123e4567-e89b-12d3-a456-426614174000 + purl: pkg:generic/example@1.0.0 + tei_urns: + - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + responses: + '201': + description: TEA Product created successfully + content: + application/json: + schema: + type: object + properties: + identifier: + $ref: '#/components/schemas/type_uuid' + product_name: + type: string + barcode: + type: string + sku: + type: string + vendor_uuid: + $ref: '#/components/schemas/type_uuid' + purl: + type: string + tei_urns: + type: array + items: + type: string + required: + - identifier + - product_name + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Product + security: + - bearerAuth: [] + - basicAuth: [] + /component: + post: + description: Create a new TEA Component entry + operationId: createTeaComponent + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tea_product_identifier: + $ref: '#/components/schemas/type_uuid' + name: + type: string + description: Component name + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the component + required: + - tea_product_identifier + - name + examples: + basic: + summary: Basic component creation + value: + tea_product_identifier: 123e4567-e89b-12d3-a456-426614174000 + name: Example Component + identifiers: + - idType: purl + idValue: pkg:maven/org.example/component@1.0.0 + responses: + '201': + description: TEA Component created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/tea_component' + '400': + description: Invalid parameters + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Component + security: + - bearerAuth: [] + - basicAuth: [] + /component/{component_identifier}: + get: + description: Get information about a TEA Component + operationId: getTeaComponent + parameters: + - name: component_identifier + in: path + required: true + description: UUID of TEA Component in the TEA server + schema: + type: string + format: uuid + responses: + '200': + description: Requested TEA Component found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_component' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Component + patch: + description: Update an existing TEA Component entry + operationId: updateTeaComponent + parameters: + - name: component_identifier + in: path + required: true + description: UUID of TEA Component in the TEA server + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Component name + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the component + responses: + '200': + description: TEA Component updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/tea_component' + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Component + security: + - bearerAuth: [] + - basicAuth: [] + delete: + description: Delete a TEA Component entry + operationId: deleteTeaComponent + parameters: + - name: component_identifier + in: path + required: true + description: UUID of TEA Component in the TEA server + schema: + type: string + format: uuid + $ref: '#/components/operations/standard_delete' + tags: + - TEA Component + /release: + post: + description: Create a new TEA Release + operationId: createTeaRelease + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + component_identifier: + $ref: '#/components/schemas/type_uuid' + description: UUID of the TEA Component + version: + type: string + description: Version number + release_date: + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + description: Timestamp of the release + pre_release: + type: boolean + description: Flag indicating pre-release status + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the release + required: + - component_identifier + - version + - release_date + examples: + basic: + summary: Basic release creation + value: + component_identifier: 123e4567-e89b-12d3-a456-426614174000 + version: 1.0.0 + release_date: '2024-03-20T15:30:00Z' + pre_release: false + identifiers: + - idType: purl + idValue: pkg:maven/org.example/component@1.0.0 + responses: + '201': + description: TEA Release created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/tea_release' + '400': + description: Invalid parameters + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Release + security: + - bearerAuth: [] + - basicAuth: [] + /release/{release_identifier}: + get: + description: Get information about a TEA Release + operationId: getTeaRelease + parameters: + - name: release_identifier + in: path + required: true + description: UUID of TEA Release in the TEA server + schema: + type: string + format: uuid + responses: + '200': + description: Requested TEA Release found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_release' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Release + patch: + description: Update an existing TEA Release entry + operationId: updateTeaRelease + parameters: + - name: release_identifier + in: path + required: true + description: UUID of TEA Release in the TEA server + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + version: + type: string + description: Version number + release_date: + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + pre_release: + type: boolean + description: May only be set to false if previously true + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + responses: + '200': + description: TEA Release updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/tea_release' + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Release + security: + - bearerAuth: [] + - basicAuth: [] + delete: + description: Delete a TEA Release entry + operationId: deleteTeaRelease + parameters: + - name: release_identifier + in: path + required: true + description: UUID of TEA Release in the TEA server + schema: + type: string + format: uuid + $ref: '#/components/operations/standard_delete' + tags: + - TEA Release + /collection: + post: + description: Create a new TEA Collection + operationId: createTeaCollection + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + release_identifier: + $ref: '#/components/schemas/type_uuid' + description: UUID of the TEA Release + update_reason: + $ref: '#/components/schemas/collection_update_reason' + description: Reason for collection creation or update + artifacts: + type: array + items: + type: object + properties: + name: + type: string + description: Artifact name + type: + $ref: '#/components/schemas/type_tea_collection_artifact_type' + description: Type of artifact + author: + $ref: '#/components/schemas/type_author' + description: Author of the artifact + formats: + type: array + items: + type: object + properties: + description: + type: string + description: A free text describing the artifact + mime_type: + type: string + description: The MIME type of the document + artifact_url: + type: string + format: uri + description: Direct download URL for the artifact + artifact_size_in_bytes: + type: integer + format: int64 + description: Size of the artifact in bytes + artifact_checksums: + type: array + items: + $ref: '#/components/schemas/artifact_checksum' + description: List of checksums for the artifact + signature_url: + type: string + format: uri + description: URL for an external signature of the artifact + required: + - description + - mime_type + - artifact_url + - artifact_size_in_bytes + - artifact_checksums + required: + - name + - type + - author + - formats + required: + - release_identifier + - update_reason + examples: + basic: + summary: Basic collection creation + value: + release_identifier: 123e4567-e89b-12d3-a456-426614174000 + update_reason: + type: INITIAL_RELEASE + comment: Initial release of collection + artifacts: + - name: Example SBOM + type: bom + author: + name: John Doe + email: john.doe@example.com + organization: Example Org + formats: + - description: CycloneDX SBOM (XML) + mime_type: application/vnd.cyclonedx+xml + artifact_url: https://example.com/sbom.xml + artifact_size_in_bytes: 1024 + artifact_checksums: + - algType: SHA-256 + algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 + signature_url: https://example.com/sbom.xml.sig + responses: + '201': + description: TEA Collection created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/tea_collection' + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Collection + security: + - bearerAuth: [] + - basicAuth: [] + /collection/{collection_identifier}: + get: + description: Get a TEA Collection by its Identifier + operationId: getTeaCollection + parameters: + - $ref: '#/components/parameters/tea_collection_identifier' + responses: + '200': + description: Requested TEA Collection found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_collection' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Collection + delete: + description: Delete a TEA Collection entry + operationId: deleteTeaCollection + parameters: + - $ref: '#/components/parameters/tea_collection_identifier' + $ref: '#/components/operations/standard_delete' + tags: + - TEA Collection + /collection/{collection_identifier}/{version}: + get: + description: Get a specific version of a TEA Collection + operationId: getTeaCollectionVersion + parameters: + - $ref: '#/components/parameters/tea_collection_identifier' + - name: version + in: path + required: true + description: Version of the TEA Collection + schema: + type: integer + minimum: 1 + responses: + '200': + description: Requested TEA Collection version found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_collection' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Collection +components: + schemas: + identifier: + type: object + description: An identifier with a specified type + properties: + idType: + description: Type of identifier, e.g. `tei`, `purl`, `cpe` + type: string + enum: + - cpe + - tei + - purl + idValue: + description: Identifier value + type: string + required: + - idType + - idValue + artifact_checksum: + type: object + properties: + algType: + description: Checksum algorithm + type: string + enum: + - MD5 + - SHA-1 + - SHA-256 + - SHA-384 + - SHA-512 + - SHA3-256 + - SHA3-384 + - SHA3-512 + - BLAKE2b-256 + - BLAKE2b-384 + - BLAKE2b-512 + - BLAKE3 + algValue: + type: string + description: Checksum value + required: + - algType + - algValue + tea_product: + type: object + properties: + identifier: + $ref: '#/components/schemas/type_uuid' + description: A unique identifier for the TEA product + name: + type: string + description: Product name + identifiers: + type: array + description: List of identifiers for the product + items: + $ref: '#/components/schemas/identifier' + components: + type: array + description: List of TEA component UUIDs + items: + $ref: '#/components/schemas/type_uuid' + tei_urns: + type: array + items: + type: string + description: Transparency Exchange Identifier URN + pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ + description: List of TEI URNs associated with this product + required: + - identifier + - name + - identifiers + - components + paginated_product_list: + allOf: + - $ref: '#/components/schemas/pagination_details' + - type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/tea_product' + tea_component: + type: object + description: A TEA component + properties: + uuid: + description: A unique identifier for the TEA component + $ref: '#/components/schemas/type_uuid' + name: + type: string + description: Component name + identifiers: + type: array + description: List of identifiers for the component + items: + $ref: '#/components/schemas/identifier' + releases: + type: array + description: List of UUIDs referencing TEA Release entries + items: + $ref: '#/components/schemas/type_uuid' + required: + - uuid + - name + - identifiers + tea_release: + type: object + description: A TEA release + properties: + uuid: + description: A unique identifier for the TEA Component Release + $ref: '#/components/schemas/type_uuid' + version: + description: Version number + type: string + example: 1.2.3 + releaseDate: + description: Timestamp of the release (for sorting purposes) + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + preRelease: + type: boolean + description: A flag indicating pre-release (or beta) status + identifiers: + type: array + description: List of identifiers for the component + items: + $ref: '#/components/schemas/identifier' + collection_references: + type: array + items: + $ref: '#/components/schemas/type_uuid' + description: Array of UUIDs referencing TEA Collection entries + default: [] + required: + - uuid + - version + - releaseDate + collection_update_reason: + type: object + description: Reason for the update to the TEA collection + properties: + type: + description: Type of update reason + type: string + enum: + - INITIAL_RELEASE + - VEX_UPDATED + - ARTIFACT_UPDATED + - ARTIFACT_ADDED + - ARTIFACT_REMOVED + comment: + type: string + description: Free text description + required: + - type + tea_collection: + type: object + description: A collection of security-related documents + properties: + uuid: + description: UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object. + $ref: '#/components/schemas/type_uuid' + version: + type: integer + description: TEA Collection version, incremented each time its content changes. Versions start with 1. + minimum: 1 + releaseDate: + description: The date when the TEA Collection version was created. + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + updateReason: + description: Reason for the update/release of the TEA Collection object. + $ref: '#/components/schemas/collection_update_reason' + artifacts: + type: array + description: List of TEA artifact objects. + items: + $ref: '#/components/schemas/tea_artifact' + required: + - uuid + - version + - releaseDate + - updateReason + tea_artifact: + type: object + description: A security-related document + properties: + uuid: + description: UUID of the TEA Artifact object. + $ref: '#/components/schemas/type_uuid' + name: + type: string + description: Artifact name + type: + description: Type of artifact + $ref: '#/components/schemas/type_tea_collection_artifact_type' + author: + $ref: '#/components/schemas/type_author' + description: Author of the artifact + formats: + type: array + description: List of objects with the same content, but in different formats. + items: + $ref: '#/components/schemas/artifact_format' + required: + - uuid + - name + - type + - author + - formats + artifact_format: + type: object + description: A security-related document in a specific format + properties: + mime_type: + type: string + description: The MIME type of the document + description: + type: string + description: A free text describing the artifact + artifact_url: + type: string + format: uri + description: Direct download URL for the artifact + artifact_size_in_bytes: + type: integer + format: int64 + description: Size of the artifact in bytes + artifact_checksums: + type: array + items: + $ref: '#/components/schemas/artifact_checksum' + description: List of checksums for the artifact + signature_url: + type: string + format: uri + description: Direct download URL for an external signature of the artifact + required: + - mime_type + - description + - artifact_url + - artifact_size_in_bytes + - artifact_checksums + type_author: + type: object + properties: + name: + type: string + email: + type: string + format: email + organization: + type: string + required: + - name + - email + - organization + type_tea_collection_artifact_type: + type: string + title: Type + description: Specifies the type of external reference. + enum: + - vcs + - issue-tracker + - website + - advisories + - bom + - mailing-list + - social + - chat + - documentation + - support + - source-distribution + - distribution + - distribution-intake + - license + - build-meta + - build-system + - release-notes + - security-contact + - model-card + - log + - configuration + - evidence + - formulation + - attestation + - threat-model + - adversary-model + - risk-assessment + - vulnerability-assertion + - exploitability-statement + - pentest-report + - static-analysis-report + - dynamic-analysis-report + - runtime-analysis-report + - component-analysis-report + - maturity-report + - certification-report + - codified-infrastructure + - quality-metrics + - poam + - electronic-signature + - digital-signature + - rfc-9116 + - other + type_uuid: + type: string + format: uuid + pagination_details: + type: object + properties: + timestamp: + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + pageStartIndex: + type: integer + format: int64 + default: 0 + pageSize: + type: integer + format: int64 + default: 100 + totalResults: + type: integer + format: int64 + required: + - timestamp + - pageStartIndex + - pageSize + - totalResults + responses: + 401-unauthorized: + description: Authentication required + content: + application/json: {} + 404-object-by-id-not-found: + description: Object requested by identifier not found + content: + application/json: {} + parameters: + page-offset: + name: pageOffset + description: Pagination offset + in: query + required: false + schema: + type: integer + format: int64 + default: 0 + page-size: + name: pageSize + description: Pagination size + in: query + required: false + schema: + type: integer + format: int64 + default: 100 + tei_urn: + name: tei_urn + description: Transparency Exchange Identifier (URN) + in: path + required: true + schema: + type: string + pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ + tea_product_identifier: + name: tea_product_identifier + description: TEA Product Identifier + in: path + required: true + schema: + type: string + format: uuid + tea_collection_identifier: + name: collection_identifier + description: TEA Collection Identifier + in: path + required: true + schema: + type: string + format: uuid + purl: + name: purl + description: Package URL (PURL) + in: query + required: false + schema: + type: string + barcode: + name: barcode + description: Barcode + in: query + required: false + schema: + type: string + sku: + name: sku + description: Product SKU + in: query + required: false + schema: + type: string + vendor_uuid: + name: vendor_uuid + description: Vendor UUID + in: query + required: false + schema: + $ref: '#/components/schemas/type_uuid' + securitySchemes: + bearerAuth: + type: http + scheme: bearer + basicAuth: + type: http + scheme: basic + operations: + standard_delete: + responses: + '204': + description: Resource successfully deleted + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + security: + - bearerAuth: [] + - basicAuth: [] +security: + - bearerAuth: [] + - basicAuth: [] +tags: + - name: TEA Product + description: Operations related to TEA Products + - name: TEA Component + description: Operations related to TEA Components + - name: TEA Release + description: Operations related to TEA Releases + - name: TEA Collection + description: Operations related to TEA Collections +externalDocs: + description: Transparency Exchange API specification + url: https://github.com/CycloneDX/transparency-exchange-api From 84781b1ccb8c0568c8e202a916d26dee1d37d81e Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Sat, 10 May 2025 22:31:33 +1000 Subject: [PATCH 02/17] more inconsistencies from consumer that needed updates Signed-off-by: Chris Langton --- spec/publisher/openapi.json | 1690 ----------------------------------- spec/publisher/openapi.yaml | 126 ++- 2 files changed, 112 insertions(+), 1704 deletions(-) delete mode 100644 spec/publisher/openapi.json diff --git a/spec/publisher/openapi.json b/spec/publisher/openapi.json deleted file mode 100644 index 3a06822..0000000 --- a/spec/publisher/openapi.json +++ /dev/null @@ -1,1690 +0,0 @@ -{ - "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base", - "openapi": "3.1.1", - "info": { - "title": "Transparency Exchange API", - "summary": "Transparency Exchange API specification for consumers and publishers", - "description": "The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations.", - "contact": { - "name": "TEA Working Group", - "email": "TBC", - "url": "https://github.com/CycloneDX/transparency-exchange-api" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE" - }, - "version": "0.0.3" - }, - "servers": [ - { - "url": "http://localhost/tea/v1", - "description": "Local development" - } - ], - "paths": { - "/product/{tei_urn}": { - "get": { - "description": "Returns the corresponding releases for a given TEI URN", - "operationId": "getTeaProduct", - "parameters": [ - { - "$ref": "#/components/parameters/tei_urn" - } - ], - "responses": { - "200": { - "description": "Requested TEA Product found and returned", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_product" - } - } - } - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Product" - ] - }, - "patch": { - "description": "Update an existing TEA Product entry", - "operationId": "updateTeaProduct", - "parameters": [ - { - "$ref": "#/components/parameters/tea_product_identifier" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "product_name": { - "type": "string" - }, - "barcode": { - "type": "string", - "description": "Barcode" - }, - "sku": { - "type": "string", - "description": "Product SKU" - }, - "vendor_uuid": { - "$ref": "#/components/schemas/type_uuid", - "description": "Vendor UUID" - }, - "purl": { - "type": "string", - "description": "Package URL (PURL)" - } - } - }, - "examples": { - "basic": { - "summary": "Basic product update", - "value": { - "product_name": "Updated Product Name", - "barcode": "123456789012", - "sku": "PROD-001", - "vendor_uuid": "123e4567-e89b-12d3-a456-426614174000", - "purl": "pkg:generic/example@1.0.0" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "TEA Product updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_product" - } - } - } - }, - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Product" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - }, - "delete": { - "description": "Delete a TEA Product entry", - "operationId": "deleteTeaProduct", - "parameters": [ - { - "$ref": "#/components/parameters/tea_product_identifier" - } - ], - "$ref": "#/components/operations/standard_delete", - "tags": [ - "TEA Product" - ] - } - }, - "/product": { - "get": { - "description": "Returns a list of TEA Products", - "operationId": "listTeaProducts", - "parameters": [ - { - "$ref": "#/components/parameters/page-offset" - }, - { - "$ref": "#/components/parameters/page-size" - }, - { - "$ref": "#/components/parameters/purl" - }, - { - "$ref": "#/components/parameters/barcode" - }, - { - "$ref": "#/components/parameters/sku" - }, - { - "$ref": "#/components/parameters/vendor_uuid" - } - ], - "responses": { - "200": { - "description": "List retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/paginated_product_list" - } - } - } - }, - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Product" - ] - }, - "post": { - "description": "Create TEA Product entry for the supplied product identifier", - "operationId": "createTeaProduct", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "product_name": { - "type": "string" - }, - "barcode": { - "type": "string", - "description": "Barcode" - }, - "sku": { - "type": "string", - "description": "Product SKU" - }, - "vendor_uuid": { - "$ref": "#/components/schemas/type_uuid", - "description": "Vendor UUID" - }, - "purl": { - "type": "string", - "description": "Package URL (PURL)" - }, - "tei_urns": { - "type": "array", - "items": { - "type": "string", - "description": "Transparency Exchange Identifier URN", - "pattern": "^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\\.-]+:.+$" - }, - "description": "List of TEI URNs associated with this product" - } - }, - "required": [ - "product_name" - ] - }, - "examples": { - "basic": { - "summary": "Basic product creation", - "value": { - "product_name": "Example Product" - } - }, - "full": { - "summary": "Full product creation with all fields", - "value": { - "product_name": "Complete Product Example", - "barcode": "123456789012", - "sku": "PROD-001", - "vendor_uuid": "123e4567-e89b-12d3-a456-426614174000", - "purl": "pkg:generic/example@1.0.0", - "tei_urns": [ - "urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1" - ] - } - } - } - } - } - }, - "responses": { - "201": { - "description": "TEA Product created successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "identifier": { - "$ref": "#/components/schemas/type_uuid" - }, - "product_name": { - "type": "string" - }, - "barcode": { - "type": "string" - }, - "sku": { - "type": "string" - }, - "vendor_uuid": { - "$ref": "#/components/schemas/type_uuid" - }, - "purl": { - "type": "string" - }, - "tei_urns": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "identifier", - "product_name" - ] - } - } - } - }, - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Product" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - } - }, - "/component": { - "post": { - "description": "Create a new TEA Component entry", - "operationId": "createTeaComponent", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tea_product_identifier": { - "$ref": "#/components/schemas/type_uuid" - }, - "name": { - "type": "string", - "description": "Component name" - }, - "identifiers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/identifier" - }, - "description": "List of identifiers for the component" - } - }, - "required": [ - "tea_product_identifier", - "name" - ] - }, - "examples": { - "basic": { - "summary": "Basic component creation", - "value": { - "tea_product_identifier": "123e4567-e89b-12d3-a456-426614174000", - "name": "Example Component", - "identifiers": [ - { - "idType": "purl", - "idValue": "pkg:maven/org.example/component@1.0.0" - } - ] - } - } - } - } - } - }, - "responses": { - "201": { - "description": "TEA Component created successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_component" - } - } - } - }, - "400": { - "description": "Invalid parameters", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Component" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } -] - } - }, - "/component/{component_identifier}": { - "get": { - "description": "Get information about a TEA Component", - "operationId": "getTeaComponent", - "parameters": [ - { - "name": "component_identifier", - "in": "path", - "required": true, - "description": "UUID of TEA Component in the TEA server", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Requested TEA Component found and returned", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_component" - } - } - } - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Component" - ] - }, - "patch": { - "description": "Update an existing TEA Component entry", - "operationId": "updateTeaComponent", - "parameters": [ - { - "name": "component_identifier", - "in": "path", - "required": true, - "description": "UUID of TEA Component in the TEA server", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Component name" - }, - "identifiers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/identifier" - }, - "description": "List of identifiers for the component" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "TEA Component updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_component" - } - } - } - }, - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Component" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - }, - "delete": { - "description": "Delete a TEA Component entry", - "operationId": "deleteTeaComponent", - "parameters": [ - { - "name": "component_identifier", - "in": "path", - "required": true, - "description": "UUID of TEA Component in the TEA server", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "$ref": "#/components/operations/standard_delete", - "tags": [ - "TEA Component" - ] - } - }, - "/release": { - "post": { - "description": "Create a new TEA Release", - "operationId": "createTeaRelease", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "component_identifier": { - "$ref": "#/components/schemas/type_uuid", - "description": "UUID of the TEA Component" - }, - "version": { - "type": "string", - "description": "Version number" - }, - "release_date": { - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z", - "description": "Timestamp of the release" - }, - "pre_release": { - "type": "boolean", - "description": "Flag indicating pre-release status" - }, - "identifiers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/identifier" - }, - "description": "List of identifiers for the release" - } - }, - "required": [ - "component_identifier", - "version", - "release_date" - ] - }, - "examples": { - "basic": { - "summary": "Basic release creation", - "value": { - "component_identifier": "123e4567-e89b-12d3-a456-426614174000", - "version": "1.0.0", - "release_date": "2024-03-20T15:30:00Z", - "pre_release": false, - "identifiers": [ - { - "idType": "purl", - "idValue": "pkg:maven/org.example/component@1.0.0" - } - ] - } - } - } - } - } - }, - "responses": { - "201": { - "description": "TEA Release created successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_release" - } - } - } - }, - "400": { - "description": "Invalid parameters", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Release" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - } - }, - "/release/{release_identifier}": { - "get": { - "description": "Get information about a TEA Release", - "operationId": "getTeaRelease", - "parameters": [ - { - "name": "release_identifier", - "in": "path", - "required": true, - "description": "UUID of TEA Release in the TEA server", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Requested TEA Release found and returned", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_release" - } - } - } - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Release" - ] - }, - "patch": { - "description": "Update an existing TEA Release entry", - "operationId": "updateTeaRelease", - "parameters": [ - { - "name": "release_identifier", - "in": "path", - "required": true, - "description": "UUID of TEA Release in the TEA server", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "version": { - "type": "string", - "description": "Version number" - }, - "release_date": { - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" - }, - "pre_release": { - "type": "boolean", - "description": "May only be set to false if previously true" - }, - "identifiers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/identifier" - } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "TEA Release updated successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_release" - } - } - } - }, - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Release" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - }, - "delete": { - "description": "Delete a TEA Release entry", - "operationId": "deleteTeaRelease", - "parameters": [ - { - "name": "release_identifier", - "in": "path", - "required": true, - "description": "UUID of TEA Release in the TEA server", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "$ref": "#/components/operations/standard_delete", - "tags": [ - "TEA Release" - ] - } - }, - "/collection": { - "post": { - "description": "Create a new TEA Collection", - "operationId": "createTeaCollection", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "release_identifier": { - "$ref": "#/components/schemas/type_uuid", - "description": "UUID of the TEA Release" - }, - "update_reason": { - "$ref": "#/components/schemas/collection_update_reason", - "description": "Reason for collection creation or update" - }, - "artifacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Artifact name" - }, - "type": { - "$ref": "#/components/schemas/type_tea_collection_artifact_type", - "description": "Type of artifact" - }, - "author": { - "$ref": "#/components/schemas/type_author", - "description": "Author of the artifact" - }, - "formats": { - "type": "array", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "A free text describing the artifact" - }, - "mime_type": { - "type": "string", - "description": "The MIME type of the document" - }, - "artifact_url": { - "type": "string", - "format": "uri", - "description": "Direct download URL for the artifact" - }, - "artifact_size_in_bytes": { - "type": "integer", - "format": "int64", - "description": "Size of the artifact in bytes" - }, - "artifact_checksums": { - "type": "array", - "items": { - "$ref": "#/components/schemas/artifact_checksum" - }, - "description": "List of checksums for the artifact" - }, - "signature_url": { - "type": "string", - "format": "uri", - "description": "URL for an external signature of the artifact" - } - }, - "required": [ - "description", - "mime_type", - "artifact_url", - "artifact_size_in_bytes", - "artifact_checksums" - ] - } - } - }, - "required": [ - "name", - "type", - "author", - "formats" - ] - } - } - }, - "required": [ - "release_identifier", - "update_reason" - ] - }, - "examples": { - "basic": { - "summary": "Basic collection creation", - "value": { - "release_identifier": "123e4567-e89b-12d3-a456-426614174000", - "update_reason": { - "type": "INITIAL_RELEASE", - "comment": "Initial release of collection" - }, - "artifacts": [ - { - "name": "Example SBOM", - "type": "bom", - "author": { - "name": "John Doe", - "email": "john.doe@example.com", - "organization": "Example Org" - }, - "formats": [ - { - "description": "CycloneDX SBOM (XML)", - "mime_type": "application/vnd.cyclonedx+xml", - "artifact_url": "https://example.com/sbom.xml", - "artifact_size_in_bytes": 1024, - "artifact_checksums": [ - { - "algType": "SHA-256", - "algValue": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" - } - ], - "signature_url": "https://example.com/sbom.xml.sig" - } - ] - } - ] - } - } - } - } - } - }, - "responses": { - "201": { - "description": "TEA Collection created successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_collection" - } - } - } - }, - "400": { - "description": "Invalid request body", - "content": { - "application/json": {} - } - }, - "401": { "$ref": "#/components/responses/401-unauthorized" }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Collection" - ], - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - } - }, - "/collection/{collection_identifier}": { - "get": { - "description": "Get a TEA Collection by its Identifier", - "operationId": "getTeaCollection", - "parameters": [ - { - "$ref": "#/components/parameters/tea_collection_identifier" - } - ], - "responses": { - "200": { - "description": "Requested TEA Collection found and returned", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_collection" - } - } - } - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Collection" - ] - }, - "delete": { - "description": "Delete a TEA Collection entry", - "operationId": "deleteTeaCollection", - "parameters": [ - { - "$ref": "#/components/parameters/tea_collection_identifier" - } - ], - "$ref": "#/components/operations/standard_delete", - "tags": [ - "TEA Collection" - ] - } - }, - "/collection/{collection_identifier}/{version}": { - "get": { - "description": "Get a specific version of a TEA Collection", - "operationId": "getTeaCollectionVersion", - "parameters": [ - { - "$ref": "#/components/parameters/tea_collection_identifier" - }, - { - "name": "version", - "in": "path", - "required": true, - "description": "Version of the TEA Collection", - "schema": { - "type": "integer", - "minimum": 1 - } - } - ], - "responses": { - "200": { - "description": "Requested TEA Collection version found and returned", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tea_collection" - } - } - } - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "tags": [ - "TEA Collection" - ] - } - } - }, - "components": { - "schemas": { - "identifier": { - "type": "object", - "description": "An identifier with a specified type", - "properties": { - "idType": { - "description": "Type of identifier, e.g. `tei`, `purl`, `cpe`", - "type": "string", - "enum": [ - "cpe", - "tei", - "purl" - ] - }, - "idValue": { - "description": "Identifier value", - "type": "string" - } - }, - "required": [ - "idType", - "idValue" - ] - }, - "artifact_checksum": { - "type": "object", - "properties": { - "algType": { - "description": "Checksum algorithm", - "type": "string", - "enum": [ - "MD5", - "SHA-1", - "SHA-256", - "SHA-384", - "SHA-512", - "SHA3-256", - "SHA3-384", - "SHA3-512", - "BLAKE2b-256", - "BLAKE2b-384", - "BLAKE2b-512", - "BLAKE3" - ] - }, - "algValue": { - "type": "string", - "description": "Checksum value" - } - }, - "required": [ - "algType", - "algValue" - ] - }, - "tea_product": { - "type": "object", - "properties": { - "identifier": { - "$ref": "#/components/schemas/type_uuid", - "description": "A unique identifier for the TEA product" - }, - "name": { - "type": "string", - "description": "Product name" - }, - "identifiers": { - "type": "array", - "description": "List of identifiers for the product", - "items": { - "$ref": "#/components/schemas/identifier" - } - }, - "components": { - "type": "array", - "description": "List of TEA component UUIDs", - "items": { - "$ref": "#/components/schemas/type_uuid" - } - }, - "tei_urns": { - "type": "array", - "items": { - "type": "string", - "description": "Transparency Exchange Identifier URN", - "pattern": "^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\\.-]+:.+$" - }, - "description": "List of TEI URNs associated with this product" - } - }, - "required": [ - "identifier", - "name", - "identifiers", - "components" - ] - }, - "paginated_product_list": { - "allOf": [ - { - "$ref": "#/components/schemas/pagination_details" - }, - { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tea_product" - } - } - } - } - ] - }, - "tea_component": { - "type": "object", - "description": "A TEA component", - "properties": { - "uuid": { - "description": "A unique identifier for the TEA component", - "$ref": "#/components/schemas/type_uuid" - }, - "name": { - "type": "string", - "description": "Component name" - }, - "identifiers": { - "type": "array", - "description": "List of identifiers for the component", - "items": { - "$ref": "#/components/schemas/identifier" - } - }, - "releases": { - "type": "array", - "description": "List of UUIDs referencing TEA Release entries", - "items": { - "$ref": "#/components/schemas/type_uuid" - } - } - }, - "required": [ - "uuid", - "name", - "identifiers" - ] - }, - "tea_release": { - "type": "object", - "description": "A TEA release", - "properties": { - "uuid": { - "description": "A unique identifier for the TEA Component Release", - "$ref": "#/components/schemas/type_uuid" - }, - "version": { - "description": "Version number", - "type": "string", - "example": "1.2.3" - }, - "releaseDate": { - "description": "Timestamp of the release (for sorting purposes)", - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" - }, - "preRelease": { - "type": "boolean", - "description": "A flag indicating pre-release (or beta) status" - }, - "identifiers": { - "type": "array", - "description": "List of identifiers for the component", - "items": { - "$ref": "#/components/schemas/identifier" - } - }, - "collection_references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/type_uuid" - }, - "description": "Array of UUIDs referencing TEA Collection entries", - "default": [] - } - }, - "required": [ - "uuid", - "version", - "releaseDate" - ] - }, - "collection_update_reason": { - "type": "object", - "description": "Reason for the update to the TEA collection", - "properties": { - "type": { - "description": "Type of update reason", - "type": "string", - "enum": [ - "INITIAL_RELEASE", - "VEX_UPDATED", - "ARTIFACT_UPDATED", - "ARTIFACT_ADDED", - "ARTIFACT_REMOVED" - ] - }, - "comment": { - "type": "string", - "description": "Free text description" - } - }, - "required": [ - "type" - ] - }, - "tea_collection": { - "type": "object", - "description": "A collection of security-related documents", - "properties": { - "uuid": { - "description": "UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object.", - "$ref": "#/components/schemas/type_uuid" - }, - "version": { - "type": "integer", - "description": "TEA Collection version, incremented each time its content changes. Versions start with 1.", - "minimum": 1 - }, - "releaseDate": { - "description": "The date when the TEA Collection version was created.", - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" - }, - "updateReason": { - "description": "Reason for the update/release of the TEA Collection object.", - "$ref": "#/components/schemas/collection_update_reason" - }, - "artifacts": { - "type": "array", - "description": "List of TEA artifact objects.", - "items": { - "$ref": "#/components/schemas/tea_artifact" - } - } - }, - "required": [ - "uuid", - "version", - "releaseDate", - "updateReason" - ] - }, - "tea_artifact": { - "type": "object", - "description": "A security-related document", - "properties": { - "uuid": { - "description": "UUID of the TEA Artifact object.", - "$ref": "#/components/schemas/type_uuid" - }, - "name": { - "type": "string", - "description": "Artifact name" - }, - "type": { - "description": "Type of artifact", - "$ref": "#/components/schemas/type_tea_collection_artifact_type" - }, - "author": { - "$ref": "#/components/schemas/type_author", - "description": "Author of the artifact" - }, - "formats": { - "type": "array", - "description": "List of objects with the same content, but in different formats.", - "items": { - "$ref": "#/components/schemas/artifact_format" - } - } - }, - "required": [ - "uuid", - "name", - "type", - "author", - "formats" - ] - }, - "artifact_format": { - "type": "object", - "description": "A security-related document in a specific format", - "properties": { - "mime_type": { - "type": "string", - "description": "The MIME type of the document" - }, - "description": { - "type": "string", - "description": "A free text describing the artifact" - }, - "artifact_url": { - "type": "string", - "format": "uri", - "description": "Direct download URL for the artifact" - }, - "artifact_size_in_bytes": { - "type": "integer", - "format": "int64", - "description": "Size of the artifact in bytes" - }, - "artifact_checksums": { - "type": "array", - "items": { - "$ref": "#/components/schemas/artifact_checksum" - }, - "description": "List of checksums for the artifact" - }, - "signature_url": { - "type": "string", - "format": "uri", - "description": "Direct download URL for an external signature of the artifact" - } - }, - "required": [ - "mime_type", - "description", - "artifact_url", - "artifact_size_in_bytes", - "artifact_checksums" - ] - }, - "type_author": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "organization": { - "type": "string" - } - }, - "required": [ - "name", - "email", - "organization" - ] - }, - "type_tea_collection_artifact_type": { - "type": "string", - "title": "Type", - "description": "Specifies the type of external reference.", - "enum": [ - "vcs", - "issue-tracker", - "website", - "advisories", - "bom", - "mailing-list", - "social", - "chat", - "documentation", - "support", - "source-distribution", - "distribution", - "distribution-intake", - "license", - "build-meta", - "build-system", - "release-notes", - "security-contact", - "model-card", - "log", - "configuration", - "evidence", - "formulation", - "attestation", - "threat-model", - "adversary-model", - "risk-assessment", - "vulnerability-assertion", - "exploitability-statement", - "pentest-report", - "static-analysis-report", - "dynamic-analysis-report", - "runtime-analysis-report", - "component-analysis-report", - "maturity-report", - "certification-report", - "codified-infrastructure", - "quality-metrics", - "poam", - "electronic-signature", - "digital-signature", - "rfc-9116", - "other" - ], - "meta:enum": { - "vcs": "Version Control System", - "issue-tracker": "Issue or defect tracking system, or an Application Lifecycle Management (ALM) system", - "website": "Website", - "advisories": "Security advisories", - "bom": "Bill of Materials (SBOM, OBOM, HBOM, SaaSBOM, etc)", - "mailing-list": "Mailing list or discussion group", - "social": "Social media account", - "chat": "Real-time chat platform", - "documentation": "Documentation, guides, or how-to instructions", - "support": "Community or commercial support", - "source-distribution": { - "description": "The location where the source code distributable can be obtained. This is often an archive format such as zip or tgz. The source-distribution type complements use of the version control (vcs) type." - }, - "distribution": "Direct or repository download location", - "distribution-intake": "The location where a component was published to. This is often the same as \"distribution\" but may also include specialized publishing processes that act as an intermediary.", - "license": "The reference to the license file. If a license URL has been defined in the license node, it should also be defined as an external reference for completeness.", - "build-meta": "Build-system specific meta file (i.e. pom.xml, package.json, .nuspec, etc)", - "build-system": "Reference to an automated build system", - "release-notes": "Reference to release notes", - "security-contact": "Specifies a way to contact the maintainer, supplier, or provider in the event of a security incident. Common URIs include links to a disclosure procedure, a mailto (RFC-2368) that specifies an email address, a tel (RFC-3966) that specifies a phone number, or dns (RFC-4501) that specifies the records containing DNS Security TXT.", - "model-card": "A model card describes the intended uses of a machine learning model, potential limitations, biases, ethical considerations, training parameters, datasets used to train the model, performance metrics, and other relevant data useful for ML transparency.", - "log": "A record of events that occurred in a computer system or application, such as problems, errors, or information on current operations.", - "configuration": "Parameters or settings that may be used by other components or services.", - "evidence": "Information used to substantiate a claim.", - "formulation": "Describes how a component or service was manufactured or deployed.", - "attestation": "Human or machine-readable statements containing facts, evidence, or testimony.", - "threat-model": "An enumeration of identified weaknesses, threats, and countermeasures, dataflow diagram (DFD), attack tree, and other supporting documentation in human-readable or machine-readable format.", - "adversary-model": "The defined assumptions, goals, and capabilities of an adversary.", - "risk-assessment": "Identifies and analyzes the potential of future events that may negatively impact individuals, assets, and/or the environment. Risk assessments may also include judgments on the tolerability of each risk.", - "vulnerability-assertion": "A Vulnerability Disclosure Report (VDR) which asserts the known and previously unknown vulnerabilities that affect a component, service, or product including the analysis and findings describing the impact (or lack of impact) that the reported vulnerability has on a component, service, or product.", - "exploitability-statement": "A Vulnerability Exploitability eXchange (VEX) which asserts the known vulnerabilities that do not affect a product, product family, or organization, and optionally the ones that do. The VEX should include the analysis and findings describing the impact (or lack of impact) that the reported vulnerability has on the product, product family, or organization.", - "pentest-report": "Results from an authorized simulated cyberattack on a component or service, otherwise known as a penetration test.", - "static-analysis-report": "SARIF or proprietary machine or human-readable report for which static analysis has identified code quality, security, and other potential issues with the source code.", - "dynamic-analysis-report": "Dynamic analysis report that has identified issues such as vulnerabilities and misconfigurations.", - "runtime-analysis-report": "Report generated by analyzing the call stack of a running application.", - "component-analysis-report": "Report generated by Software Composition Analysis (SCA), container analysis, or other forms of component analysis.", - "maturity-report": "Report containing a formal assessment of an organization, business unit, or team against a maturity model.", - "certification-report": "Industry, regulatory, or other certification from an accredited (if applicable) certification body.", - "codified-infrastructure": "Code or configuration that defines and provisions virtualized infrastructure, commonly referred to as Infrastructure as Code (IaC).", - "quality-metrics": "Report or system in which quality metrics can be obtained.", - "poam": "Plans of Action and Milestones (POAM) complement an \"attestation\" external reference. POAM is defined by NIST as a \"document that identifies tasks needing to be accomplished. It details resources required to accomplish the elements of the plan, any milestones in meeting the tasks and scheduled completion dates for the milestones\".", - "electronic-signature": "An e-signature is commonly a scanned representation of a written signature or a stylized script of the person's name.", - "digital-signature": "A signature that leverages cryptography, typically public/private key pairs, which provides strong authenticity verification.", - "rfc-9116": "Document that complies with RFC-9116 (A File Format to Aid in Security Vulnerability Disclosure)", - "other": "Use this if no other types accurately describe the purpose of the external reference." - } - }, - "type_uuid": { - "type": "string", - "format": "uuid" - }, - "pagination_details": { - "type": "object", - "properties": { - "timestamp": { - "type": "string", - "format": "date-time", - "example": "2024-03-20T15:30:00Z" - }, - "pageStartIndex": { - "type": "integer", - "format": "int64", - "default": 0 - }, - "pageSize": { - "type": "integer", - "format": "int64", - "default": 100 - }, - "totalResults": { - "type": "integer", - "format": "int64" - } - }, - "required": [ - "timestamp", - "pageStartIndex", - "pageSize", - "totalResults" - ] - } - }, - "responses": { - "401-unauthorized": { - "description": "Authentication required", - "content": { - "application/json": {} - } - }, - "404-object-by-id-not-found": { - "description": "Object requested by identifier not found", - "content": { - "application/json": {} - } - } - }, - "parameters": { - "page-offset": { - "name": "pageOffset", - "description": "Pagination offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64", - "default": 0 - } - }, - "page-size": { - "name": "pageSize", - "description": "Pagination size", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int64", - "default": 100 - } - }, - "tei_urn": { - "name": "tei_urn", - "description": "Transparency Exchange Identifier (URN)", - "in": "path", - "required": true, - "schema": { - "type": "string", - "pattern": "^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\\.-]+:.+$" - } - }, - "tea_product_identifier": { - "name": "tea_product_identifier", - "description": "TEA Product Identifier", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - "tea_collection_identifier": { - "name": "collection_identifier", - "description": "TEA Collection Identifier", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - "purl": { - "name": "purl", - "description": "Package URL (PURL)", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - "barcode": { - "name": "barcode", - "description": "Barcode", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - "sku": { - "name": "sku", - "description": "Product SKU", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - "vendor_uuid": { - "name": "vendor_uuid", - "description": "Vendor UUID", - "in": "query", - "required": false, - "schema": { - "$ref": "#/components/schemas/type_uuid" - } - } - }, - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer" - }, - "basicAuth": { - "type": "http", - "scheme": "basic" - } - }, - "operations": { - "standard_delete": { - "responses": { - "204": { - "description": "Resource successfully deleted" - }, - "401": { - "$ref": "#/components/responses/401-unauthorized" - }, - "404": { - "$ref": "#/components/responses/404-object-by-id-not-found" - } - }, - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ] - } - } - }, - "security": [ - { - "bearerAuth": [] - }, - { - "basicAuth": [] - } - ], - "tags": [ - { - "name": "TEA Product", - "description": "Operations related to TEA Products" - }, - { - "name": "TEA Component", - "description": "Operations related to TEA Components" - }, - { - "name": "TEA Release", - "description": "Operations related to TEA Releases" - }, - { - "name": "TEA Collection", - "description": "Operations related to TEA Collections" - } - ], - "externalDocs": { - "description": "Transparency Exchange API specification", - "url": "https://github.com/CycloneDX/transparency-exchange-api" - } -} \ No newline at end of file diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index f31ffdd..31d3aa4 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -6,7 +6,6 @@ info: description: The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations. contact: name: TEA Working Group - email: TBC url: https://github.com/CycloneDX/transparency-exchange-api license: name: Apache 2.0 @@ -619,6 +618,88 @@ paths: security: - bearerAuth: [] - basicAuth: [] + /release/{release_identifier}/collection: + get: + description: Get the latest TEA Collection belonging to the TEA Release + operationId: getLatestCollectionByReleaseId + parameters: + - name: release_identifier + in: path + required: true + description: UUID of TEA Release in the TEA server + schema: + type: string + format: uuid + responses: + '200': + description: Requested TEA Collection found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_collection' + '400': + description: Invalid request + content: + application/json: {} + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Release + /release/{release_identifier}/collections: + get: + description: Get all TEA Collections belonging to the TEA Release + operationId: getCollectionsByReleaseId + parameters: + - name: release_identifier + in: path + required: true + description: UUID of TEA Release in the TEA server + schema: + type: string + format: uuid + responses: + '200': + description: Requested TEA Collections found and returned + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/tea_collection' + '400': + description: Invalid request + content: + application/json: {} + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Release + /artifact/{artifact_identifier}: + get: + description: Get metadata for specific TEA artifact + operationId: getArtifact + parameters: + - name: artifact_identifier + in: path + required: true + description: UUID of TEA Artifact in the TEA server + schema: + $ref: '#/components/schemas/type_uuid' + responses: + '200': + description: Requested TEA Artifact metadata found and returned + content: + application/json: + schema: + $ref: '#/components/schemas/tea_artifact' + '400': + description: Invalid request + content: + application/json: {} + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Artifact /collection/{collection_identifier}: get: description: Get a TEA Collection by its Identifier @@ -767,6 +848,11 @@ components: description: List of identifiers for the component items: $ref: '#/components/schemas/identifier' + versions: # Added to match consumer schema requirements + type: array + description: List of available versions for this component + items: + type: string releases: type: array description: List of UUIDs referencing TEA Release entries @@ -776,6 +862,7 @@ components: - uuid - name - identifiers + - versions tea_release: type: object description: A TEA release @@ -894,29 +981,24 @@ components: description: type: string description: A free text describing the artifact - artifact_url: + url: # Changed from artifact_url to match consumer type: string format: uri description: Direct download URL for the artifact - artifact_size_in_bytes: - type: integer - format: int64 - description: Size of the artifact in bytes - artifact_checksums: + signature_url: # Already matches consumer + type: string + format: uri + description: Direct download URL for an external signature of the artifact + checksums: # Changed from artifact_checksums to match consumer type: array items: $ref: '#/components/schemas/artifact_checksum' description: List of checksums for the artifact - signature_url: - type: string - format: uri - description: Direct download URL for an external signature of the artifact required: - mime_type - description - - artifact_url - - artifact_size_in_bytes - - artifact_checksums + - url + - checksums type_author: type: object properties: @@ -1085,6 +1167,22 @@ components: required: false schema: $ref: '#/components/schemas/type_uuid' + id-type: + name: idType + description: Type of identifier specified in the `idValue` parameter + in: query + schema: + type: string + enum: + - cpe + - tei + - purl + id-value: + name: idValue + description: If present, only the objects with the given identifier value will be returned + in: query + schema: + type: string securitySchemes: bearerAuth: type: http From c0378b3cc75f05f9e24f069d6dc4cfa010ce84d6 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Sat, 10 May 2025 23:14:16 +1000 Subject: [PATCH 03/17] feat(openapi): align consumer/publisher APIs and implement TEI discovery - Add authentication schemes - Create discovery endpoints (/.well-known/tea/{id}) - Standardise property naming between consumer/publisher APIs - Implement advanced filtering for components and artifacts - Add lifecycle status endpoints Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 151 ++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 31d3aa4..8c88f99 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -15,6 +15,15 @@ servers: - url: http://localhost/tea/v1 description: Local development paths: + /.well-known/tea/{tei_identifier}: + get: + description: TEI discovery endpoint that redirects to appropriate API endpoint + parameters: + - name: tei_identifier + in: path + required: true + schema: + type: string /product/{tei_urn}: get: description: Returns the corresponding releases for a given TEI URN @@ -264,6 +273,123 @@ paths: security: - bearerAuth: [] - basicAuth: [] + /components: # Add advanced search capabilities + get: + description: Returns a list of TEA Components + operationId: listTeaComponents + parameters: + - $ref: '#/components/parameters/page-offset' + - $ref: '#/components/parameters/page-size' + - name: tea_product_identifier + in: query + required: false + description: TEA Product UUID + schema: + type: string + format: uuid + - name: purl + in: query + required: false + description: Package URL (PURL) + schema: + type: string + - name: barcode + in: query + required: false + description: Barcode + schema: + type: string + - name: sku + in: query + required: false + description: SKU of the product + schema: + type: string + - name: version + in: query + schema: + type: string + - name: preRelease + in: query + schema: + type: boolean + - name: releaseDate + in: query + schema: + type: string + format: date-time + - name: vendor_uuid + in: query + required: false + description: Vendor UUID + schema: + type: string + format: uuid + - name: component_uuid + in: query + required: false + description: TEA Component UUID + schema: + type: string + format: uuid + - name: component_name + in: query + required: false + description: TEA Component name + schema: + type: string + - name: identifiers + in: query + required: false + description: TEA Component identifiers + schema: + type: array + items: + $ref: '#/components/schemas/identifier' + responses: + '200': + description: List retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/paginated_product_list' + '400': + description: Invalid request body + content: + application/json: {} + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Component + /component/{component_id}/lifecycle: + get: + description: Get the lifecycle of a TEA Component + operationId: getTeaComponentLifecycle + parameters: + - name: component_id + in: path + required: true + description: UUID of TEA Component in the TEA server + schema: + type: string + format: uuid + responses: + '200': + description: Requested TEA Component lifecycle found and returned + content: + application/json: + schema: + type: object + properties: + lifecycle: + type: string + description: Lifecycle status of the component + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Component Lifecycle /component/{component_identifier}: get: description: Get information about a TEA Component @@ -1187,9 +1313,30 @@ components: bearerAuth: type: http scheme: bearer + bearerFormat: JWT + description: Bearer token authentication is a stateless authentication scheme where the client sends a token in the HTTP Authorization header. The server validates the token and grants access to the requested resource to anyone with the token without needing to authenticate again, and stolen tokens can be used by anyone. basicAuth: type: http scheme: basic + description: Basic authentication is a plaintext scheme built into the HTTP protocol and has been deemed insecure because headers are commonly logged in plaintext. + mutualTLS: + type: mutualTLS + description: Client certificate authentication is a secure authentication scheme where the client presents a valid certificate to the server for authentication. The server validates the certificate and grants access to the requested resource, but stolen certificates can be used by anyone. + pasetoAuth: + type: http + scheme: bearer + bearerFormat: PASETO + description: Platform-Agnostic Security Tokens (PASETO) is a secure alternative to JWT that is resistant to many cryptographic vulnerabilities. PASETO tokens use versioned protocols that specify modern cryptographic algorithms. + verifiableCredentials: + type: http + scheme: bearer + bearerFormat: VC-JWT + description: W3C Verifiable Credentials 2.0 with proof field is a standard for expressing verifiable credentials as signed JWT tokens. The integrity and authenticity of the credential can be cryptographically verified. + hmacAuth: + type: apiKey + in: header + name: X-HMAC-Signature + description: HMAC authentication uses a shared secret key to create a signature of the request content using a secure hash algorithm (SHA256, SHA384, SHA512, SHA3-256, SHA3-384, or SHA3-512). The server verifies this signature to authenticate the request. operations: standard_delete: responses: @@ -1205,6 +1352,10 @@ components: security: - bearerAuth: [] - basicAuth: [] + - mutualTLS: [] + - pasetoAuth: [] + - verifiableCredentials: [] + - hmacAuth: [] tags: - name: TEA Product description: Operations related to TEA Products From 637e29f023c6549a8c7f401f37778c18b631fb39 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Wed, 21 May 2025 20:49:59 +1000 Subject: [PATCH 04/17] chore(lint): address long lines Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 376 +++--------------------------------- 1 file changed, 24 insertions(+), 352 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 8c88f99..877a534 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -3,7 +3,8 @@ openapi: 3.1.1 info: title: Transparency Exchange API summary: Transparency Exchange API specification for consumers and publishers - description: The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations. + description: | + The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations. contact: name: TEA Working Group url: https://github.com/CycloneDX/transparency-exchange-api @@ -15,32 +16,7 @@ servers: - url: http://localhost/tea/v1 description: Local development paths: - /.well-known/tea/{tei_identifier}: - get: - description: TEI discovery endpoint that redirects to appropriate API endpoint - parameters: - - name: tei_identifier - in: path - required: true - schema: - type: string /product/{tei_urn}: - get: - description: Returns the corresponding releases for a given TEI URN - operationId: getTeaProduct - parameters: - - $ref: '#/components/parameters/tei_urn' - responses: - '200': - description: Requested TEA Product found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_product' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Product patch: description: Update an existing TEA Product entry operationId: updateTeaProduct @@ -105,33 +81,6 @@ paths: tags: - TEA Product /product: - get: - description: Returns a list of TEA Products - operationId: listTeaProducts - parameters: - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-size' - - $ref: '#/components/parameters/purl' - - $ref: '#/components/parameters/barcode' - - $ref: '#/components/parameters/sku' - - $ref: '#/components/parameters/vendor_uuid' - responses: - '200': - description: List retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/paginated_product_list' - '400': - description: Invalid request body - content: - application/json: {} - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Product post: description: Create TEA Product entry for the supplied product identifier operationId: createTeaProduct @@ -273,146 +222,7 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /components: # Add advanced search capabilities - get: - description: Returns a list of TEA Components - operationId: listTeaComponents - parameters: - - $ref: '#/components/parameters/page-offset' - - $ref: '#/components/parameters/page-size' - - name: tea_product_identifier - in: query - required: false - description: TEA Product UUID - schema: - type: string - format: uuid - - name: purl - in: query - required: false - description: Package URL (PURL) - schema: - type: string - - name: barcode - in: query - required: false - description: Barcode - schema: - type: string - - name: sku - in: query - required: false - description: SKU of the product - schema: - type: string - - name: version - in: query - schema: - type: string - - name: preRelease - in: query - schema: - type: boolean - - name: releaseDate - in: query - schema: - type: string - format: date-time - - name: vendor_uuid - in: query - required: false - description: Vendor UUID - schema: - type: string - format: uuid - - name: component_uuid - in: query - required: false - description: TEA Component UUID - schema: - type: string - format: uuid - - name: component_name - in: query - required: false - description: TEA Component name - schema: - type: string - - name: identifiers - in: query - required: false - description: TEA Component identifiers - schema: - type: array - items: - $ref: '#/components/schemas/identifier' - responses: - '200': - description: List retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/paginated_product_list' - '400': - description: Invalid request body - content: - application/json: {} - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Component - /component/{component_id}/lifecycle: - get: - description: Get the lifecycle of a TEA Component - operationId: getTeaComponentLifecycle - parameters: - - name: component_id - in: path - required: true - description: UUID of TEA Component in the TEA server - schema: - type: string - format: uuid - responses: - '200': - description: Requested TEA Component lifecycle found and returned - content: - application/json: - schema: - type: object - properties: - lifecycle: - type: string - description: Lifecycle status of the component - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Component Lifecycle /component/{component_identifier}: - get: - description: Get information about a TEA Component - operationId: getTeaComponent - parameters: - - name: component_identifier - in: path - required: true - description: UUID of TEA Component in the TEA server - schema: - type: string - format: uuid - responses: - '200': - description: Requested TEA Component found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_component' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Component patch: description: Update an existing TEA Component entry operationId: updateTeaComponent @@ -539,28 +349,6 @@ paths: - bearerAuth: [] - basicAuth: [] /release/{release_identifier}: - get: - description: Get information about a TEA Release - operationId: getTeaRelease - parameters: - - name: release_identifier - in: path - required: true - description: UUID of TEA Release in the TEA server - schema: - type: string - format: uuid - responses: - '200': - description: Requested TEA Release found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_release' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Release patch: description: Update an existing TEA Release entry operationId: updateTeaRelease @@ -744,137 +532,6 @@ paths: security: - bearerAuth: [] - basicAuth: [] - /release/{release_identifier}/collection: - get: - description: Get the latest TEA Collection belonging to the TEA Release - operationId: getLatestCollectionByReleaseId - parameters: - - name: release_identifier - in: path - required: true - description: UUID of TEA Release in the TEA server - schema: - type: string - format: uuid - responses: - '200': - description: Requested TEA Collection found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_collection' - '400': - description: Invalid request - content: - application/json: {} - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Release - /release/{release_identifier}/collections: - get: - description: Get all TEA Collections belonging to the TEA Release - operationId: getCollectionsByReleaseId - parameters: - - name: release_identifier - in: path - required: true - description: UUID of TEA Release in the TEA server - schema: - type: string - format: uuid - responses: - '200': - description: Requested TEA Collections found and returned - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/tea_collection' - '400': - description: Invalid request - content: - application/json: {} - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Release - /artifact/{artifact_identifier}: - get: - description: Get metadata for specific TEA artifact - operationId: getArtifact - parameters: - - name: artifact_identifier - in: path - required: true - description: UUID of TEA Artifact in the TEA server - schema: - $ref: '#/components/schemas/type_uuid' - responses: - '200': - description: Requested TEA Artifact metadata found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_artifact' - '400': - description: Invalid request - content: - application/json: {} - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Artifact - /collection/{collection_identifier}: - get: - description: Get a TEA Collection by its Identifier - operationId: getTeaCollection - parameters: - - $ref: '#/components/parameters/tea_collection_identifier' - responses: - '200': - description: Requested TEA Collection found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_collection' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Collection - delete: - description: Delete a TEA Collection entry - operationId: deleteTeaCollection - parameters: - - $ref: '#/components/parameters/tea_collection_identifier' - $ref: '#/components/operations/standard_delete' - tags: - - TEA Collection - /collection/{collection_identifier}/{version}: - get: - description: Get a specific version of a TEA Collection - operationId: getTeaCollectionVersion - parameters: - - $ref: '#/components/parameters/tea_collection_identifier' - - name: version - in: path - required: true - description: Version of the TEA Collection - schema: - type: integer - minimum: 1 - responses: - '200': - description: Requested TEA Collection version found and returned - content: - application/json: - schema: - $ref: '#/components/schemas/tea_collection' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - tags: - - TEA Collection components: schemas: identifier: @@ -1046,7 +703,8 @@ components: description: A collection of security-related documents properties: uuid: - description: UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object. + description: | + UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object. $ref: '#/components/schemas/type_uuid' version: type: integer @@ -1314,29 +972,43 @@ components: type: http scheme: bearer bearerFormat: JWT - description: Bearer token authentication is a stateless authentication scheme where the client sends a token in the HTTP Authorization header. The server validates the token and grants access to the requested resource to anyone with the token without needing to authenticate again, and stolen tokens can be used by anyone. + description: | + Bearer token authentication is a stateless authentication scheme where the client sends a token in the HTTP Authorization header. + The server validates the token and grants access to the requested resource to anyone with the token without needing to authenticate + again, and stolen tokens can be used by anyone. basicAuth: type: http scheme: basic - description: Basic authentication is a plaintext scheme built into the HTTP protocol and has been deemed insecure because headers are commonly logged in plaintext. + description: | + Basic authentication is a plaintext scheme built into the HTTP protocol and has been deemed insecure because headers are commonly + logged in plaintext. mutualTLS: type: mutualTLS - description: Client certificate authentication is a secure authentication scheme where the client presents a valid certificate to the server for authentication. The server validates the certificate and grants access to the requested resource, but stolen certificates can be used by anyone. + description: | + Client certificate authentication is a secure authentication scheme where the client presents a valid certificate to the server + for authentication. The server validates the certificate and grants access to the requested resource, but stolen certificates can be + used by anyone. pasetoAuth: type: http scheme: bearer bearerFormat: PASETO - description: Platform-Agnostic Security Tokens (PASETO) is a secure alternative to JWT that is resistant to many cryptographic vulnerabilities. PASETO tokens use versioned protocols that specify modern cryptographic algorithms. + description: | + Platform-Agnostic Security Tokens (PASETO) is a secure alternative to JWT that is resistant to many cryptographic vulnerabilities. + PASETO tokens use versioned protocols that specify modern cryptographic algorithms. verifiableCredentials: type: http scheme: bearer bearerFormat: VC-JWT - description: W3C Verifiable Credentials 2.0 with proof field is a standard for expressing verifiable credentials as signed JWT tokens. The integrity and authenticity of the credential can be cryptographically verified. + description: | + W3C Verifiable Credentials 2.0 with proof field is a standard for expressing verifiable credentials as signed JWT tokens. + The integrity and authenticity of the credential can be cryptographically verified. hmacAuth: type: apiKey in: header name: X-HMAC-Signature - description: HMAC authentication uses a shared secret key to create a signature of the request content using a secure hash algorithm (SHA256, SHA384, SHA512, SHA3-256, SHA3-384, or SHA3-512). The server verifies this signature to authenticate the request. + description: | + HMAC authentication uses a shared secret key to create a signature of the request content using a secure hash algorithm (SHA256, + SHA384, SHA512, SHA3-256, SHA3-384, or SHA3-512). The server verifies this signature to authenticate the request. operations: standard_delete: responses: From 059fd752ee3ea85c59d16824fb88c433de516db7 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Wed, 21 May 2025 22:31:17 +1000 Subject: [PATCH 05/17] chore: use camelCase instea of snake_case Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 272 ++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 148 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 877a534..b6277ca 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -16,12 +16,12 @@ servers: - url: http://localhost/tea/v1 description: Local development paths: - /product/{tei_urn}: + /product/{teiUrn}: patch: description: Update an existing TEA Product entry operationId: updateTeaProduct parameters: - - $ref: '#/components/parameters/tea_product_identifier' + - $ref: '#/components/parameters/teaProductIdentifier' requestBody: required: true content: @@ -29,7 +29,7 @@ paths: schema: type: object properties: - product_name: + productName: type: string barcode: type: string @@ -37,8 +37,8 @@ paths: sku: type: string description: Product SKU - vendor_uuid: - $ref: '#/components/schemas/type_uuid' + vendorUuid: + $ref: '#/components/schemas/typeUuid' description: Vendor UUID purl: type: string @@ -47,10 +47,10 @@ paths: basic: summary: Basic product update value: - product_name: Updated Product Name + productName: Updated Product Name barcode: '123456789012' sku: PROD-001 - vendor_uuid: 123e4567-e89b-12d3-a456-426614174000 + vendorUuid: 123e4567-e89b-12d3-a456-426614174000 purl: pkg:generic/example@1.0.0 responses: '200': @@ -58,7 +58,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/tea_product' + $ref: '#/components/schemas/teaProduct' '400': description: Invalid request body content: @@ -69,15 +69,12 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Product - security: - - bearerAuth: [] - - basicAuth: [] delete: description: Delete a TEA Product entry operationId: deleteTeaProduct parameters: - - $ref: '#/components/parameters/tea_product_identifier' - $ref: '#/components/operations/standard_delete' + - $ref: '#/components/parameters/teaProductIdentifier' + $ref: '#/components/operations/standardDelete' tags: - TEA Product /product: @@ -91,7 +88,7 @@ paths: schema: type: object properties: - product_name: + productName: type: string barcode: type: string @@ -99,13 +96,13 @@ paths: sku: type: string description: Product SKU - vendor_uuid: - $ref: '#/components/schemas/type_uuid' + vendorUuid: + $ref: '#/components/schemas/typeUuid' description: Vendor UUID purl: type: string description: Package URL (PURL) - tei_urns: + teiUrns: type: array items: type: string @@ -113,21 +110,21 @@ paths: pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ description: List of TEI URNs associated with this product required: - - product_name + - productName examples: basic: summary: Basic product creation value: - product_name: Example Product + productName: Example Product full: summary: Full product creation with all fields value: - product_name: Complete Product Example + productName: Complete Product Example barcode: '123456789012' sku: PROD-001 - vendor_uuid: 123e4567-e89b-12d3-a456-426614174000 + vendorUuid: 123e4567-e89b-12d3-a456-426614174000 purl: pkg:generic/example@1.0.0 - tei_urns: + teiUrns: - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 responses: '201': @@ -138,24 +135,24 @@ paths: type: object properties: identifier: - $ref: '#/components/schemas/type_uuid' - product_name: + $ref: '#/components/schemas/typeUuid' + productName: type: string barcode: type: string sku: type: string - vendor_uuid: - $ref: '#/components/schemas/type_uuid' + vendorUuid: + $ref: '#/components/schemas/typeUuid' purl: type: string - tei_urns: + teiUrns: type: array items: type: string required: - identifier - - product_name + - productName '400': description: Invalid request body content: @@ -166,9 +163,6 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Product - security: - - bearerAuth: [] - - basicAuth: [] /component: post: description: Create a new TEA Component entry @@ -180,8 +174,8 @@ paths: schema: type: object properties: - tea_product_identifier: - $ref: '#/components/schemas/type_uuid' + teaProductIdentifier: + $ref: '#/components/schemas/typeUuid' name: type: string description: Component name @@ -191,13 +185,13 @@ paths: $ref: '#/components/schemas/identifier' description: List of identifiers for the component required: - - tea_product_identifier + - teaProductIdentifier - name examples: basic: summary: Basic component creation value: - tea_product_identifier: 123e4567-e89b-12d3-a456-426614174000 + teaProductIdentifier: 123e4567-e89b-12d3-a456-426614174000 name: Example Component identifiers: - idType: purl @@ -208,7 +202,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/tea_component' + $ref: '#/components/schemas/teaComponent' '400': description: Invalid parameters content: @@ -219,15 +213,12 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Component - security: - - bearerAuth: [] - - basicAuth: [] - /component/{component_identifier}: + /component/{componentIdentifier}: patch: description: Update an existing TEA Component entry operationId: updateTeaComponent parameters: - - name: component_identifier + - name: componentIdentifier in: path required: true description: UUID of TEA Component in the TEA server @@ -255,7 +246,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/tea_component' + $ref: '#/components/schemas/teaComponent' '400': description: Invalid request body content: @@ -266,21 +257,18 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Component - security: - - bearerAuth: [] - - basicAuth: [] delete: description: Delete a TEA Component entry operationId: deleteTeaComponent parameters: - - name: component_identifier + - name: componentIdentifier in: path required: true description: UUID of TEA Component in the TEA server schema: type: string format: uuid - $ref: '#/components/operations/standard_delete' + $ref: '#/components/operations/standardDelete' tags: - TEA Component /release: @@ -294,18 +282,18 @@ paths: schema: type: object properties: - component_identifier: - $ref: '#/components/schemas/type_uuid' + componentIdentifier: + $ref: '#/components/schemas/typeUuid' description: UUID of the TEA Component version: type: string description: Version number - release_date: + releaseDate: type: string format: date-time example: '2024-03-20T15:30:00Z' description: Timestamp of the release - pre_release: + preRelease: type: boolean description: Flag indicating pre-release status identifiers: @@ -314,17 +302,17 @@ paths: $ref: '#/components/schemas/identifier' description: List of identifiers for the release required: - - component_identifier + - componentIdentifier - version - - release_date + - releaseDate examples: basic: summary: Basic release creation value: - component_identifier: 123e4567-e89b-12d3-a456-426614174000 + componentIdentifier: 123e4567-e89b-12d3-a456-426614174000 version: 1.0.0 - release_date: '2024-03-20T15:30:00Z' - pre_release: false + releaseDate: '2024-03-20T15:30:00Z' + preRelease: false identifiers: - idType: purl idValue: pkg:maven/org.example/component@1.0.0 @@ -334,7 +322,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/tea_release' + $ref: '#/components/schemas/teaRelease' '400': description: Invalid parameters content: @@ -345,15 +333,12 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Release - security: - - bearerAuth: [] - - basicAuth: [] - /release/{release_identifier}: + /release/{releaseIdentifier}: patch: description: Update an existing TEA Release entry operationId: updateTeaRelease parameters: - - name: release_identifier + - name: releaseIdentifier in: path required: true description: UUID of TEA Release in the TEA server @@ -370,11 +355,11 @@ paths: version: type: string description: Version number - release_date: + releaseDate: type: string format: date-time example: '2024-03-20T15:30:00Z' - pre_release: + preRelease: type: boolean description: May only be set to false if previously true identifiers: @@ -387,7 +372,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/tea_release' + $ref: '#/components/schemas/teaRelease' '400': description: Invalid request body content: @@ -398,21 +383,18 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Release - security: - - bearerAuth: [] - - basicAuth: [] delete: description: Delete a TEA Release entry operationId: deleteTeaRelease parameters: - - name: release_identifier + - name: releaseIdentifier in: path required: true description: UUID of TEA Release in the TEA server schema: type: string format: uuid - $ref: '#/components/operations/standard_delete' + $ref: '#/components/operations/standardDelete' tags: - TEA Release /collection: @@ -426,11 +408,11 @@ paths: schema: type: object properties: - release_identifier: - $ref: '#/components/schemas/type_uuid' + releaseIdentifier: + $ref: '#/components/schemas/typeUuid' description: UUID of the TEA Release - update_reason: - $ref: '#/components/schemas/collection_update_reason' + updateReason: + $ref: '#/components/schemas/collectionUpdateReason' description: Reason for collection creation or update artifacts: type: array @@ -441,10 +423,10 @@ paths: type: string description: Artifact name type: - $ref: '#/components/schemas/type_tea_collection_artifact_type' + $ref: '#/components/schemas/typeTeaCollectionArtifactType' description: Type of artifact author: - $ref: '#/components/schemas/type_author' + $ref: '#/components/schemas/typeAuthor' description: Author of the artifact formats: type: array @@ -454,46 +436,46 @@ paths: description: type: string description: A free text describing the artifact - mime_type: + mimeType: type: string description: The MIME type of the document - artifact_url: + artifactUrl: type: string format: uri description: Direct download URL for the artifact - artifact_size_in_bytes: + artifactSizeInBytes: type: integer format: int64 description: Size of the artifact in bytes - artifact_checksums: + artifactChecksums: type: array items: - $ref: '#/components/schemas/artifact_checksum' + $ref: '#/components/schemas/artifactChecksum' description: List of checksums for the artifact - signature_url: + signatureUrl: type: string format: uri description: URL for an external signature of the artifact required: - description - - mime_type - - artifact_url - - artifact_size_in_bytes - - artifact_checksums + - mimeType + - artifactUrl + - artifactSizeInBytes + - artifactChecksums required: - name - type - author - formats required: - - release_identifier - - update_reason + - releaseIdentifier + - updateReason examples: basic: summary: Basic collection creation value: - release_identifier: 123e4567-e89b-12d3-a456-426614174000 - update_reason: + releaseIdentifier: 123e4567-e89b-12d3-a456-426614174000 + updateReason: type: INITIAL_RELEASE comment: Initial release of collection artifacts: @@ -505,20 +487,20 @@ paths: organization: Example Org formats: - description: CycloneDX SBOM (XML) - mime_type: application/vnd.cyclonedx+xml - artifact_url: https://example.com/sbom.xml - artifact_size_in_bytes: 1024 - artifact_checksums: + mimeType: application/vnd.cyclonedx+xml + artifactUrl: https://example.com/sbom.xml + artifactSizeInBytes: 1024 + artifactChecksums: - algType: SHA-256 algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - signature_url: https://example.com/sbom.xml.sig + signatureUrl: https://example.com/sbom.xml.sig responses: '201': description: TEA Collection created successfully content: application/json: schema: - $ref: '#/components/schemas/tea_collection' + $ref: '#/components/schemas/teaCollection' '400': description: Invalid request body content: @@ -529,9 +511,6 @@ paths: $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Collection - security: - - bearerAuth: [] - - basicAuth: [] components: schemas: identifier: @@ -551,7 +530,7 @@ components: required: - idType - idValue - artifact_checksum: + artifactChecksum: type: object properties: algType: @@ -576,11 +555,11 @@ components: required: - algType - algValue - tea_product: + teaProduct: type: object properties: identifier: - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' description: A unique identifier for the TEA product name: type: string @@ -594,8 +573,8 @@ components: type: array description: List of TEA component UUIDs items: - $ref: '#/components/schemas/type_uuid' - tei_urns: + $ref: '#/components/schemas/typeUuid' + teiUrns: type: array items: type: string @@ -607,22 +586,22 @@ components: - name - identifiers - components - paginated_product_list: + paginatedProductList: allOf: - - $ref: '#/components/schemas/pagination_details' + - $ref: '#/components/schemas/paginationDetails' - type: object properties: results: type: array items: - $ref: '#/components/schemas/tea_product' - tea_component: + $ref: '#/components/schemas/teaProduct' + teaComponent: type: object description: A TEA component properties: uuid: description: A unique identifier for the TEA component - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' name: type: string description: Component name @@ -640,19 +619,19 @@ components: type: array description: List of UUIDs referencing TEA Release entries items: - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' required: - uuid - name - identifiers - versions - tea_release: + teaRelease: type: object description: A TEA release properties: uuid: description: A unique identifier for the TEA Component Release - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' version: description: Version number type: string @@ -670,17 +649,17 @@ components: description: List of identifiers for the component items: $ref: '#/components/schemas/identifier' - collection_references: + collectionReferences: type: array items: - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' description: Array of UUIDs referencing TEA Collection entries default: [] required: - uuid - version - releaseDate - collection_update_reason: + collectionUpdateReason: type: object description: Reason for the update to the TEA collection properties: @@ -698,14 +677,14 @@ components: description: Free text description required: - type - tea_collection: + teaCollection: type: object description: A collection of security-related documents properties: uuid: description: | UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object. - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' version: type: integer description: TEA Collection version, incremented each time its content changes. Versions start with 1. @@ -717,73 +696,73 @@ components: example: '2024-03-20T15:30:00Z' updateReason: description: Reason for the update/release of the TEA Collection object. - $ref: '#/components/schemas/collection_update_reason' + $ref: '#/components/schemas/collectionUpdateReason' artifacts: type: array description: List of TEA artifact objects. items: - $ref: '#/components/schemas/tea_artifact' + $ref: '#/components/schemas/teaArtifact' required: - uuid - version - releaseDate - updateReason - tea_artifact: + teaArtifact: type: object description: A security-related document properties: uuid: description: UUID of the TEA Artifact object. - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' name: type: string description: Artifact name type: description: Type of artifact - $ref: '#/components/schemas/type_tea_collection_artifact_type' + $ref: '#/components/schemas/typeTeaCollectionArtifactType' author: - $ref: '#/components/schemas/type_author' + $ref: '#/components/schemas/typeAuthor' description: Author of the artifact formats: type: array description: List of objects with the same content, but in different formats. items: - $ref: '#/components/schemas/artifact_format' + $ref: '#/components/schemas/artifactFormat' required: - uuid - name - type - author - formats - artifact_format: + artifactFormat: type: object description: A security-related document in a specific format properties: - mime_type: + mimeType: type: string description: The MIME type of the document description: type: string description: A free text describing the artifact - url: # Changed from artifact_url to match consumer + url: # Changed from artifactUrl to match consumer type: string format: uri description: Direct download URL for the artifact - signature_url: # Already matches consumer + signatureUrl: # Already matches consumer type: string format: uri description: Direct download URL for an external signature of the artifact - checksums: # Changed from artifact_checksums to match consumer + checksums: # Changed from artifactChecksums to match consumer type: array items: - $ref: '#/components/schemas/artifact_checksum' + $ref: '#/components/schemas/artifactChecksum' description: List of checksums for the artifact required: - - mime_type + - mimeType - description - url - checksums - type_author: + typeAuthor: type: object properties: name: @@ -797,7 +776,7 @@ components: - name - email - organization - type_tea_collection_artifact_type: + typeTeaCollectionArtifactType: type: string title: Type description: Specifies the type of external reference. @@ -845,10 +824,10 @@ components: - digital-signature - rfc-9116 - other - type_uuid: + typeUuid: type: string format: uuid - pagination_details: + paginationDetails: type: object properties: timestamp: @@ -899,24 +878,24 @@ components: type: integer format: int64 default: 100 - tei_urn: - name: tei_urn + teiUrn: + name: teiUrn description: Transparency Exchange Identifier (URN) in: path required: true schema: type: string pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ - tea_product_identifier: - name: tea_product_identifier + teaProductIdentifier: + name: teaProductIdentifier description: TEA Product Identifier in: path required: true schema: type: string format: uuid - tea_collection_identifier: - name: collection_identifier + teaCollectionIdentifier: + name: collectionIdentifier description: TEA Collection Identifier in: path required: true @@ -944,13 +923,13 @@ components: required: false schema: type: string - vendor_uuid: - name: vendor_uuid + vendorUuid: + name: vendorUuid description: Vendor UUID in: query required: false schema: - $ref: '#/components/schemas/type_uuid' + $ref: '#/components/schemas/typeUuid' id-type: name: idType description: Type of identifier specified in the `idValue` parameter @@ -1010,7 +989,7 @@ components: HMAC authentication uses a shared secret key to create a signature of the request content using a secure hash algorithm (SHA256, SHA384, SHA512, SHA3-256, SHA3-384, or SHA3-512). The server verifies this signature to authenticate the request. operations: - standard_delete: + standardDelete: responses: '204': description: Resource successfully deleted @@ -1018,9 +997,6 @@ components: $ref: '#/components/responses/401-unauthorized' '404': $ref: '#/components/responses/404-object-by-id-not-found' - security: - - bearerAuth: [] - - basicAuth: [] security: - bearerAuth: [] - basicAuth: [] From 72746cbe23b68a374437f413bf5cdad269ff10e0 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Wed, 21 May 2025 23:58:09 +1000 Subject: [PATCH 06/17] chore: pointless purl for publisher removed Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 167 ++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 74 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index b6277ca..581465f 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -21,7 +21,7 @@ paths: description: Update an existing TEA Product entry operationId: updateTeaProduct parameters: - - $ref: '#/components/parameters/teaProductIdentifier' + - $ref: '#/components/parameters/productIdentifier' requestBody: required: true content: @@ -40,9 +40,26 @@ paths: vendorUuid: $ref: '#/components/schemas/typeUuid' description: Vendor UUID - purl: + type: type: string - description: Package URL (PURL) + description: Package type (required) + namespace: + type: string + description: Package namespace (optional) + name: + type: string + description: Package name (required) + version: + type: string + description: Package version (optional) + qualifiers: + type: object + additionalProperties: + type: string + description: Qualifiers as key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) examples: basic: summary: Basic product update @@ -51,14 +68,31 @@ paths: barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - purl: pkg:generic/example@1.0.0 + type: generic + name: example + version: 1.0.0 + full: + summary: Full product update with all PURL fields + value: + productName: Updated Product Name + barcode: '123456789012' + sku: PROD-001 + vendorUuid: 123e4567-e89b-12d3-a456-426614174000 + type: maven + namespace: org.example + name: component + version: 2.1.0 + qualifiers: + classifier: sources + repository_url: https://repo.example.com + subpath: src/main responses: '200': description: TEA Product updated successfully content: application/json: schema: - $ref: '#/components/schemas/teaProduct' + $ref: '#/components/schemas/product' '400': description: Invalid request body content: @@ -73,7 +107,7 @@ paths: description: Delete a TEA Product entry operationId: deleteTeaProduct parameters: - - $ref: '#/components/parameters/teaProductIdentifier' + - $ref: '#/components/parameters/productIdentifier' $ref: '#/components/operations/standardDelete' tags: - TEA Product @@ -99,9 +133,6 @@ paths: vendorUuid: $ref: '#/components/schemas/typeUuid' description: Vendor UUID - purl: - type: string - description: Package URL (PURL) teiUrns: type: array items: @@ -109,6 +140,26 @@ paths: description: Transparency Exchange Identifier URN pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ description: List of TEI URNs associated with this product + type: + type: string + description: Package type (required) + namespace: + type: string + description: Package namespace (optional) + name: + type: string + description: Package name (required) + version: + type: string + description: Package version (optional) + qualifiers: + type: object + additionalProperties: + type: string + description: Qualifiers as key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) required: - productName examples: @@ -116,6 +167,8 @@ paths: summary: Basic product creation value: productName: Example Product + type: generic + name: example full: summary: Full product creation with all fields value: @@ -123,12 +176,19 @@ paths: barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - purl: pkg:generic/example@1.0.0 teiUrns: - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + type: maven + namespace: org.example + name: component + version: 2.1.0 + qualifiers: + classifier: sources + repository_url: https://repo.example.com + subpath: src/main responses: '201': - description: TEA Product created successfully + description: Product created successfully content: application/json: schema: @@ -144,8 +204,6 @@ paths: type: string vendorUuid: $ref: '#/components/schemas/typeUuid' - purl: - type: string teiUrns: type: array items: @@ -174,7 +232,7 @@ paths: schema: type: object properties: - teaProductIdentifier: + productIdentifier: $ref: '#/components/schemas/typeUuid' name: type: string @@ -185,13 +243,13 @@ paths: $ref: '#/components/schemas/identifier' description: List of identifiers for the component required: - - teaProductIdentifier + - productIdentifier - name examples: basic: summary: Basic component creation value: - teaProductIdentifier: 123e4567-e89b-12d3-a456-426614174000 + productIdentifier: 123e4567-e89b-12d3-a456-426614174000 name: Example Component identifiers: - idType: purl @@ -202,7 +260,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/teaComponent' + $ref: '#/components/schemas/component' '400': description: Invalid parameters content: @@ -246,7 +304,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/teaComponent' + $ref: '#/components/schemas/component' '400': description: Invalid request body content: @@ -322,7 +380,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/teaRelease' + $ref: '#/components/schemas/release' '400': description: Invalid parameters content: @@ -372,7 +430,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/teaRelease' + $ref: '#/components/schemas/release' '400': description: Invalid request body content: @@ -423,7 +481,7 @@ paths: type: string description: Artifact name type: - $ref: '#/components/schemas/typeTeaCollectionArtifactType' + $ref: '#/components/schemas/typeCollectionArtifactType' description: Type of artifact author: $ref: '#/components/schemas/typeAuthor' @@ -500,7 +558,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/teaCollection' + $ref: '#/components/schemas/collection' '400': description: Invalid request body content: @@ -555,7 +613,7 @@ components: required: - algType - algValue - teaProduct: + product: type: object properties: identifier: @@ -586,16 +644,7 @@ components: - name - identifiers - components - paginatedProductList: - allOf: - - $ref: '#/components/schemas/paginationDetails' - - type: object - properties: - results: - type: array - items: - $ref: '#/components/schemas/teaProduct' - teaComponent: + component: type: object description: A TEA component properties: @@ -625,7 +674,7 @@ components: - name - identifiers - versions - teaRelease: + release: type: object description: A TEA release properties: @@ -677,7 +726,7 @@ components: description: Free text description required: - type - teaCollection: + collection: type: object description: A collection of security-related documents properties: @@ -701,13 +750,13 @@ components: type: array description: List of TEA artifact objects. items: - $ref: '#/components/schemas/teaArtifact' + $ref: '#/components/schemas/artifact' required: - uuid - version - releaseDate - updateReason - teaArtifact: + artifact: type: object description: A security-related document properties: @@ -719,7 +768,7 @@ components: description: Artifact name type: description: Type of artifact - $ref: '#/components/schemas/typeTeaCollectionArtifactType' + $ref: '#/components/schemas/typeCollectionArtifactType' author: $ref: '#/components/schemas/typeAuthor' description: Author of the artifact @@ -776,7 +825,7 @@ components: - name - email - organization - typeTeaCollectionArtifactType: + typeCollectionArtifactType: type: string title: Type description: Specifies the type of external reference. @@ -827,29 +876,6 @@ components: typeUuid: type: string format: uuid - paginationDetails: - type: object - properties: - timestamp: - type: string - format: date-time - example: '2024-03-20T15:30:00Z' - pageStartIndex: - type: integer - format: int64 - default: 0 - pageSize: - type: integer - format: int64 - default: 100 - totalResults: - type: integer - format: int64 - required: - - timestamp - - pageStartIndex - - pageSize - - totalResults responses: 401-unauthorized: description: Authentication required @@ -869,7 +895,7 @@ components: type: integer format: int64 default: 0 - page-size: + pageSize: name: pageSize description: Pagination size in: query @@ -886,15 +912,15 @@ components: schema: type: string pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ - teaProductIdentifier: - name: teaProductIdentifier + productIdentifier: + name: productIdentifier description: TEA Product Identifier in: path required: true schema: type: string format: uuid - teaCollectionIdentifier: + collectionIdentifier: name: collectionIdentifier description: TEA Collection Identifier in: path @@ -902,13 +928,6 @@ components: schema: type: string format: uuid - purl: - name: purl - description: Package URL (PURL) - in: query - required: false - schema: - type: string barcode: name: barcode description: Barcode @@ -930,7 +949,7 @@ components: required: false schema: $ref: '#/components/schemas/typeUuid' - id-type: + idType: name: idType description: Type of identifier specified in the `idValue` parameter in: query From eac17b686313f02cd3051b1f48b1c2832677f902 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Thu, 22 May 2025 00:21:49 +1000 Subject: [PATCH 07/17] chore: sync version and resolve name inconsistencies Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 57 ++++++++++++++----------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 581465f..eabb4b6 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -11,7 +11,7 @@ info: license: name: Apache 2.0 url: https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE - version: 0.0.3 + version: 0.1.0-beta.1 servers: - url: http://localhost/tea/v1 description: Local development @@ -29,8 +29,9 @@ paths: schema: type: object properties: - productName: + name: type: string + description: Product name (required) barcode: type: string description: Barcode @@ -42,16 +43,13 @@ paths: description: Vendor UUID type: type: string - description: Package type (required) + description: Product type (required) namespace: type: string - description: Package namespace (optional) - name: - type: string - description: Package name (required) + description: Product namespace (optional) version: type: string - description: Package version (optional) + description: Product version (optional) qualifiers: type: object additionalProperties: @@ -64,23 +62,21 @@ paths: basic: summary: Basic product update value: - productName: Updated Product Name + name: Updated Product Name barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 type: generic - name: example version: 1.0.0 full: summary: Full product update with all PURL fields value: - productName: Updated Product Name + name: Updated Product Name barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 type: maven namespace: org.example - name: component version: 2.1.0 qualifiers: classifier: sources @@ -122,7 +118,7 @@ paths: schema: type: object properties: - productName: + name: type: string barcode: type: string @@ -142,16 +138,13 @@ paths: description: List of TEI URNs associated with this product type: type: string - description: Package type (required) + description: Product type (required) namespace: type: string - description: Package namespace (optional) - name: - type: string - description: Package name (required) + description: Product namespace (optional) version: type: string - description: Package version (optional) + description: Product version (optional) qualifiers: type: object additionalProperties: @@ -161,18 +154,17 @@ paths: type: string description: Subpath (optional) required: - - productName + - name examples: basic: summary: Basic product creation value: - productName: Example Product + name: Example Product type: generic - name: example full: summary: Full product creation with all fields value: - productName: Complete Product Example + name: Complete Product Example barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 @@ -180,7 +172,6 @@ paths: - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 type: maven namespace: org.example - name: component version: 2.1.0 qualifiers: classifier: sources @@ -497,15 +488,11 @@ paths: mimeType: type: string description: The MIME type of the document - artifactUrl: + url: type: string format: uri description: Direct download URL for the artifact - artifactSizeInBytes: - type: integer - format: int64 - description: Size of the artifact in bytes - artifactChecksums: + checksums: type: array items: $ref: '#/components/schemas/artifactChecksum' @@ -517,9 +504,8 @@ paths: required: - description - mimeType - - artifactUrl - - artifactSizeInBytes - - artifactChecksums + - url + - checksums required: - name - type @@ -546,9 +532,8 @@ paths: formats: - description: CycloneDX SBOM (XML) mimeType: application/vnd.cyclonedx+xml - artifactUrl: https://example.com/sbom.xml - artifactSizeInBytes: 1024 - artifactChecksums: + url: https://example.com/sbom.xml + checksums: - algType: SHA-256 algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 signatureUrl: https://example.com/sbom.xml.sig From fafe58a36cfae285a23203b7808117501c5b6e01 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Thu, 22 May 2025 11:39:07 +1000 Subject: [PATCH 08/17] chore: align to Consumer udpate Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 54 ++++++++----------------------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index eabb4b6..e347640 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -815,49 +815,17 @@ components: title: Type description: Specifies the type of external reference. enum: - - vcs - - issue-tracker - - website - - advisories - - bom - - mailing-list - - social - - chat - - documentation - - support - - source-distribution - - distribution - - distribution-intake - - license - - build-meta - - build-system - - release-notes - - security-contact - - model-card - - log - - configuration - - evidence - - formulation - - attestation - - threat-model - - adversary-model - - risk-assessment - - vulnerability-assertion - - exploitability-statement - - pentest-report - - static-analysis-report - - dynamic-analysis-report - - runtime-analysis-report - - component-analysis-report - - maturity-report - - certification-report - - codified-infrastructure - - quality-metrics - - poam - - electronic-signature - - digital-signature - - rfc-9116 - - other + - ATTESTATION + - BOM + - BUILD_META + - CERTIFICATION + - FORMULATION + - LICENSE + - RELEASE_NOTES + - SECURITY_TXT + - THREAT_MODEL + - VULNERABILITIES + - OTHER typeUuid: type: string format: uuid From 926b98fbc085390699005b26e189d77cc92ba433 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Thu, 22 May 2025 23:05:47 +1000 Subject: [PATCH 09/17] chore: move requestBody into ref components Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 639 ++++++++++++++++++------------------ 1 file changed, 327 insertions(+), 312 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index e347640..8492cea 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -23,65 +23,7 @@ paths: parameters: - $ref: '#/components/parameters/productIdentifier' requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: Product name (required) - barcode: - type: string - description: Barcode - sku: - type: string - description: Product SKU - vendorUuid: - $ref: '#/components/schemas/typeUuid' - description: Vendor UUID - type: - type: string - description: Product type (required) - namespace: - type: string - description: Product namespace (optional) - version: - type: string - description: Product version (optional) - qualifiers: - type: object - additionalProperties: - type: string - description: Qualifiers as key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - examples: - basic: - summary: Basic product update - value: - name: Updated Product Name - barcode: '123456789012' - sku: PROD-001 - vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - type: generic - version: 1.0.0 - full: - summary: Full product update with all PURL fields - value: - name: Updated Product Name - barcode: '123456789012' - sku: PROD-001 - vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - type: maven - namespace: org.example - version: 2.1.0 - qualifiers: - classifier: sources - repository_url: https://repo.example.com - subpath: src/main + $ref: '#/components/requestBodies/UpdateTeaProduct' responses: '200': description: TEA Product updated successfully @@ -112,71 +54,7 @@ paths: description: Create TEA Product entry for the supplied product identifier operationId: createTeaProduct requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - barcode: - type: string - description: Barcode - sku: - type: string - description: Product SKU - vendorUuid: - $ref: '#/components/schemas/typeUuid' - description: Vendor UUID - teiUrns: - type: array - items: - type: string - description: Transparency Exchange Identifier URN - pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ - description: List of TEI URNs associated with this product - type: - type: string - description: Product type (required) - namespace: - type: string - description: Product namespace (optional) - version: - type: string - description: Product version (optional) - qualifiers: - type: object - additionalProperties: - type: string - description: Qualifiers as key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - required: - - name - examples: - basic: - summary: Basic product creation - value: - name: Example Product - type: generic - full: - summary: Full product creation with all fields - value: - name: Complete Product Example - barcode: '123456789012' - sku: PROD-001 - vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - teiUrns: - - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 - type: maven - namespace: org.example - version: 2.1.0 - qualifiers: - classifier: sources - repository_url: https://repo.example.com - subpath: src/main + $ref: '#/components/requestBodies/CreateTeaProduct' responses: '201': description: Product created successfully @@ -217,34 +95,7 @@ paths: description: Create a new TEA Component entry operationId: createTeaComponent requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - productIdentifier: - $ref: '#/components/schemas/typeUuid' - name: - type: string - description: Component name - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the component - required: - - productIdentifier - - name - examples: - basic: - summary: Basic component creation - value: - productIdentifier: 123e4567-e89b-12d3-a456-426614174000 - name: Example Component - identifiers: - - idType: purl - idValue: pkg:maven/org.example/component@1.0.0 + $ref: '#/components/requestBodies/CreateTeaComponent' responses: '201': description: TEA Component created successfully @@ -275,20 +126,7 @@ paths: type: string format: uuid requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - name: - type: string - description: Component name - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the component + $ref: '#/components/requestBodies/UpdateTeaComponent' responses: '200': description: TEA Component updated successfully @@ -325,46 +163,7 @@ paths: description: Create a new TEA Release operationId: createTeaRelease requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - componentIdentifier: - $ref: '#/components/schemas/typeUuid' - description: UUID of the TEA Component - version: - type: string - description: Version number - releaseDate: - type: string - format: date-time - example: '2024-03-20T15:30:00Z' - description: Timestamp of the release - preRelease: - type: boolean - description: Flag indicating pre-release status - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the release - required: - - componentIdentifier - - version - - releaseDate - examples: - basic: - summary: Basic release creation - value: - componentIdentifier: 123e4567-e89b-12d3-a456-426614174000 - version: 1.0.0 - releaseDate: '2024-03-20T15:30:00Z' - preRelease: false - identifiers: - - idType: purl - idValue: pkg:maven/org.example/component@1.0.0 + $ref: '#/components/requestBodies/CreateTeaRelease' responses: '201': description: TEA Release created successfully @@ -395,26 +194,7 @@ paths: type: string format: uuid requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - version: - type: string - description: Version number - releaseDate: - type: string - format: date-time - example: '2024-03-20T15:30:00Z' - preRelease: - type: boolean - description: May only be set to false if previously true - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' + $ref: '#/components/requestBodies/UpdateTeaRelease' responses: '200': description: TEA Release updated successfully @@ -451,92 +231,7 @@ paths: description: Create a new TEA Collection operationId: createTeaCollection requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - releaseIdentifier: - $ref: '#/components/schemas/typeUuid' - description: UUID of the TEA Release - updateReason: - $ref: '#/components/schemas/collectionUpdateReason' - description: Reason for collection creation or update - artifacts: - type: array - items: - type: object - properties: - name: - type: string - description: Artifact name - type: - $ref: '#/components/schemas/typeCollectionArtifactType' - description: Type of artifact - author: - $ref: '#/components/schemas/typeAuthor' - description: Author of the artifact - formats: - type: array - items: - type: object - properties: - description: - type: string - description: A free text describing the artifact - mimeType: - type: string - description: The MIME type of the document - url: - type: string - format: uri - description: Direct download URL for the artifact - checksums: - type: array - items: - $ref: '#/components/schemas/artifactChecksum' - description: List of checksums for the artifact - signatureUrl: - type: string - format: uri - description: URL for an external signature of the artifact - required: - - description - - mimeType - - url - - checksums - required: - - name - - type - - author - - formats - required: - - releaseIdentifier - - updateReason - examples: - basic: - summary: Basic collection creation - value: - releaseIdentifier: 123e4567-e89b-12d3-a456-426614174000 - updateReason: - type: INITIAL_RELEASE - comment: Initial release of collection - artifacts: - - name: Example SBOM - type: bom - author: - name: John Doe - email: john.doe@example.com - organization: Example Org - formats: - - description: CycloneDX SBOM (XML) - mimeType: application/vnd.cyclonedx+xml - url: https://example.com/sbom.xml - checksums: - - algType: SHA-256 - algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - signatureUrl: https://example.com/sbom.xml.sig + $ref: '#/components/requestBodies/CreateTeaCollection' responses: '201': description: TEA Collection created successfully @@ -988,3 +683,323 @@ tags: externalDocs: description: Transparency Exchange API specification url: https://github.com/CycloneDX/transparency-exchange-api + requestBodies: + UpdateTeaProduct: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Product name (required) + barcode: + type: string + description: Barcode + sku: + type: string + description: Product SKU + vendorUuid: + $ref: '#/components/schemas/typeUuid' + description: Vendor UUID + type: + type: string + description: Product type (required) + namespace: + type: string + description: Product namespace (optional) + version: + type: string + description: Product version (optional) + qualifiers: + type: object + additionalProperties: + type: string + description: Qualifiers as key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) + examples: + basic: + summary: Basic product update + value: + name: Updated Product Name + barcode: '123456789012' + sku: PROD-001 + vendorUuid: 123e4567-e89b-12d3-a456-426614174000 + type: generic + version: 1.0.0 + full: + summary: Full product update with all PURL fields + value: + name: Updated Product Name + barcode: '123456789012' + sku: PROD-001 + vendorUuid: 123e4567-e89b-12d3-a456-426614174000 + type: maven + namespace: org.example + version: 2.1.0 + qualifiers: + classifier: sources + repository_url: https://repo.example.com + subpath: src/main + CreateTeaProduct: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + barcode: + type: string + description: Barcode + sku: + type: string + description: Product SKU + vendorUuid: + $ref: '#/components/schemas/typeUuid' + description: Vendor UUID + teiUrns: + type: array + items: + type: string + description: Transparency Exchange Identifier URN + pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ + description: List of TEI URNs associated with this product + type: + type: string + description: Product type (required) + namespace: + type: string + description: Product namespace (optional) + version: + type: string + description: Product version (optional) + qualifiers: + type: object + additionalProperties: + type: string + description: Qualifiers as key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) + required: + - name + examples: + basic: + summary: Basic product creation + value: + name: Example Product + type: generic + full: + summary: Full product creation with all fields + value: + name: Complete Product Example + barcode: '123456789012' + sku: PROD-001 + vendorUuid: 123e4567-e89b-12d3-a456-426614174000 + teiUrns: + - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + type: maven + namespace: org.example + version: 2.1.0 + qualifiers: + classifier: sources + repository_url: https://repo.example.com + subpath: src/main + CreateTeaComponent: + required: true + content: + application/json: + schema: + type: object + properties: + productIdentifier: + $ref: '#/components/schemas/typeUuid' + name: + type: string + description: Component name + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the component + required: + - productIdentifier + - name + examples: + basic: + summary: Basic component creation + value: + productIdentifier: 123e4567-e89b-12d3-a456-426614174000 + name: Example Component + identifiers: + - idType: purl + idValue: pkg:maven/org.example/component@1.0.0 + UpdateTeaComponent: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Component name + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the component + CreateTeaRelease: + required: true + content: + application/json: + schema: + type: object + properties: + componentIdentifier: + $ref: '#/components/schemas/typeUuid' + description: UUID of the TEA Component + version: + type: string + description: Version number + releaseDate: + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + description: Timestamp of the release + preRelease: + type: boolean + description: Flag indicating pre-release status + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the release + required: + - componentIdentifier + - version + - releaseDate + examples: + basic: + summary: Basic release creation + value: + componentIdentifier: 123e4567-e89b-12d3-a456-426614174000 + version: 1.0.0 + releaseDate: '2024-03-20T15:30:00Z' + preRelease: false + identifiers: + - idType: purl + idValue: pkg:maven/org.example/component@1.0.0 + UpdateTeaRelease: + required: true + content: + application/json: + schema: + type: object + properties: + version: + type: string + description: Version number + releaseDate: + type: string + format: date-time + example: '2024-03-20T15:30:00Z' + preRelease: + type: boolean + description: May only be set to false if previously true + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + CreateTeaCollection: + required: true + content: + application/json: + schema: + type: object + properties: + releaseIdentifier: + $ref: '#/components/schemas/typeUuid' + description: UUID of the TEA Release + updateReason: + $ref: '#/components/schemas/collectionUpdateReason' + description: Reason for collection creation or update + artifacts: + type: array + items: + type: object + properties: + name: + type: string + description: Artifact name + type: + $ref: '#/components/schemas/typeCollectionArtifactType' + description: Type of artifact + author: + $ref: '#/components/schemas/typeAuthor' + description: Author of the artifact + formats: + type: array + items: + type: object + properties: + description: + type: string + description: A free text describing the artifact + mimeType: + type: string + description: The MIME type of the document + url: + type: string + format: uri + description: Direct download URL for the artifact + checksums: + type: array + items: + $ref: '#/components/schemas/artifactChecksum' + description: List of checksums for the artifact + signatureUrl: + type: string + format: uri + description: URL for an external signature of the artifact + required: + - description + - mimeType + - url + - checksums + required: + - name + - type + - author + - formats + required: + - releaseIdentifier + - updateReason + examples: + basic: + summary: Basic collection creation + value: + releaseIdentifier: 123e4567-e89b-12d3-a456-426614174000 + updateReason: + type: INITIAL_RELEASE + comment: Initial release of collection + artifacts: + - name: Example SBOM + type: bom + author: + name: John Doe + email: john.doe@example.com + organization: Example Org + formats: + - description: CycloneDX SBOM (XML) + mimeType: application/vnd.cyclonedx+xml + url: https://example.com/sbom.xml + checksums: + - algType: SHA-256 + algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 + signatureUrl: https://example.com/sbom.xml.sig From 59d2ba53d04af8581cd5adcebd1c1b8159c5f445 Mon Sep 17 00:00:00 2001 From: Chris Langton Date: Sun, 25 May 2025 21:23:05 +1000 Subject: [PATCH 10/17] feat: Refactor to use $ref for operations on product, component, and release paths Signed-off-by: Chris Langton --- spec/publisher/openapi.yaml | 508 ++++++++++++++++++++++++++---------- 1 file changed, 366 insertions(+), 142 deletions(-) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 8492cea..2953082 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -16,25 +16,25 @@ servers: - url: http://localhost/tea/v1 description: Local development paths: - /product/{teiUrn}: + /product/{uuid}: patch: description: Update an existing TEA Product entry operationId: updateTeaProduct parameters: - - $ref: '#/components/parameters/productIdentifier' + - name: uuid + in: path + required: true + description: UUID of TEA Product in the TEA server + schema: + type: string + format: uuid requestBody: $ref: '#/components/requestBodies/UpdateTeaProduct' responses: '200': - description: TEA Product updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/product' + $ref: '#/components/responses/200-tea-product-updated' '400': - description: Invalid request body - content: - application/json: {} + $ref: '#/components/responses/400-invalid-request-body' '401': $ref: '#/components/responses/401-unauthorized' '404': @@ -45,7 +45,13 @@ paths: description: Delete a TEA Product entry operationId: deleteTeaProduct parameters: - - $ref: '#/components/parameters/productIdentifier' + - name: uuid + in: path + required: true + description: UUID of TEA Product in the TEA server + schema: + type: string + format: uuid $ref: '#/components/operations/standardDelete' tags: - TEA Product @@ -57,33 +63,9 @@ paths: $ref: '#/components/requestBodies/CreateTeaProduct' responses: '201': - description: Product created successfully - content: - application/json: - schema: - type: object - properties: - identifier: - $ref: '#/components/schemas/typeUuid' - productName: - type: string - barcode: - type: string - sku: - type: string - vendorUuid: - $ref: '#/components/schemas/typeUuid' - teiUrns: - type: array - items: - type: string - required: - - identifier - - productName + $ref: '#/components/responses/201-tea-product-created' '400': - description: Invalid request body - content: - application/json: {} + $ref: '#/components/responses/400-invalid-request-body' '401': $ref: '#/components/responses/401-unauthorized' '404': @@ -98,27 +80,21 @@ paths: $ref: '#/components/requestBodies/CreateTeaComponent' responses: '201': - description: TEA Component created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/component' + $ref: '#/components/responses/201-tea-component-created' '400': - description: Invalid parameters - content: - application/json: {} + $ref: '#/components/responses/400-invalid-parameters' '401': $ref: '#/components/responses/401-unauthorized' '404': $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Component - /component/{componentIdentifier}: + /component/{uuid}: patch: description: Update an existing TEA Component entry operationId: updateTeaComponent parameters: - - name: componentIdentifier + - name: uuid in: path required: true description: UUID of TEA Component in the TEA server @@ -129,15 +105,9 @@ paths: $ref: '#/components/requestBodies/UpdateTeaComponent' responses: '200': - description: TEA Component updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/component' + $ref: '#/components/responses/200-tea-component-updated' '400': - description: Invalid request body - content: - application/json: {} + $ref: '#/components/responses/400-invalid-request-body' '401': $ref: '#/components/responses/401-unauthorized' '404': @@ -148,7 +118,7 @@ paths: description: Delete a TEA Component entry operationId: deleteTeaComponent parameters: - - name: componentIdentifier + - name: uuid in: path required: true description: UUID of TEA Component in the TEA server @@ -166,27 +136,21 @@ paths: $ref: '#/components/requestBodies/CreateTeaRelease' responses: '201': - description: TEA Release created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/release' + $ref: '#/components/responses/201-tea-release-created' '400': - description: Invalid parameters - content: - application/json: {} + $ref: '#/components/responses/400-invalid-parameters' '401': $ref: '#/components/responses/401-unauthorized' '404': $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Release - /release/{releaseIdentifier}: + /release/{uuid}: patch: description: Update an existing TEA Release entry operationId: updateTeaRelease parameters: - - name: releaseIdentifier + - name: uuid in: path required: true description: UUID of TEA Release in the TEA server @@ -197,15 +161,9 @@ paths: $ref: '#/components/requestBodies/UpdateTeaRelease' responses: '200': - description: TEA Release updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/release' + $ref: '#/components/responses/200-tea-release-updated' '400': - description: Invalid request body - content: - application/json: {} + $ref: '#/components/responses/400-invalid-request-body' '401': $ref: '#/components/responses/401-unauthorized' '404': @@ -216,7 +174,7 @@ paths: description: Delete a TEA Release entry operationId: deleteTeaRelease parameters: - - name: releaseIdentifier + - name: uuid in: path required: true description: UUID of TEA Release in the TEA server @@ -234,15 +192,9 @@ paths: $ref: '#/components/requestBodies/CreateTeaCollection' responses: '201': - description: TEA Collection created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/collection' + $ref: '#/components/responses/201-tea-collection-created' '400': - description: Invalid request body - content: - application/json: {} + $ref: '#/components/responses/400-invalid-request-body' '401': $ref: '#/components/responses/401-unauthorized' '404': @@ -256,12 +208,13 @@ components: description: An identifier with a specified type properties: idType: - description: Type of identifier, e.g. `tei`, `purl`, `cpe` + description: Type of identifier, e.g. `tei`, `purl`, `cpe`, `swid` type: string enum: - cpe - tei - purl + - swid idValue: description: Identifier value type: string @@ -275,8 +228,6 @@ components: description: Checksum algorithm type: string enum: - - MD5 - - SHA-1 - SHA-256 - SHA-384 - SHA-512 @@ -302,23 +253,44 @@ components: name: type: string description: Product name + barcode: + type: string + description: Barcode + sku: + type: string + description: Product SKU + vendorUuid: + $ref: '#/components/schemas/typeUuid' + description: Vendor UUID identifiers: type: array description: List of identifiers for the product items: $ref: '#/components/schemas/identifier' + type: + $ref: '#/components/schemas/typePackage' + description: Product type + namespace: + type: string + description: Product namespace (optional) + version: + type: string + description: Product version (optional) + qualifiers: + type: array + items: + type: object + additionalProperties: + type: string + description: Array of qualifier objects containing related key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) components: type: array description: List of TEA component UUIDs items: $ref: '#/components/schemas/typeUuid' - teiUrns: - type: array - items: - type: string - description: Transparency Exchange Identifier URN - pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ - description: List of TEI URNs associated with this product required: - identifier - name @@ -524,7 +496,136 @@ components: typeUuid: type: string format: uuid + typePackage: + type: string + description: Package type following PURL specification + enum: + - alpm + - apk + - bitbucket + - bitnami + - cargo + - cocoapods + - composer + - conan + - conda + - cpan + - cran + - deb + - docker + - gem + - generic + - github + - golang + - hackage + - hex + - huggingface + - luarocks + - maven + - mlflow + - npm + - nuget + - oci + - pub + - pypi + - qpkg + - rpm + - swid + - swift responses: + 200-tea-product-updated: + description: TEA Product updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/product' + 200-tea-component-updated: + description: TEA Component updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/component' + 200-tea-release-updated: + description: TEA Release updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/release' + 201-tea-product-created: + description: Product created successfully + content: + application/json: + schema: + type: object + properties: + identifier: + $ref: '#/components/schemas/typeUuid' + description: Unique identifier for the created product + name: + type: string + description: Product name + barcode: + type: string + description: Barcode + sku: + type: string + description: Product SKU + vendorUuid: + $ref: '#/components/schemas/typeUuid' + description: Vendor UUID + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the product (TEI, CPE, SWID, etc.) + type: + $ref: '#/components/schemas/typePackage' + description: Product type + namespace: + type: string + description: Product namespace (optional) + version: + type: string + description: Product version (optional) + qualifiers: + type: array + items: + type: object + additionalProperties: + type: string + description: Array of qualifier objects containing related key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) + required: + - identifier + - name + 201-tea-component-created: + description: TEA Component created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/component' + 201-tea-release-created: + description: TEA Release created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/release' + 201-tea-collection-created: + description: TEA Collection created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/collection' + 400-invalid-request-body: + description: Invalid request body + content: + application/json: {} + 400-invalid-parameters: + description: Invalid parameters + content: + application/json: {} 401-unauthorized: description: Authentication required content: @@ -562,7 +663,7 @@ components: pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ productIdentifier: name: productIdentifier - description: TEA Product Identifier + description: Customer unique Product Identifier in: path required: true schema: @@ -664,25 +765,6 @@ components: $ref: '#/components/responses/401-unauthorized' '404': $ref: '#/components/responses/404-object-by-id-not-found' -security: - - bearerAuth: [] - - basicAuth: [] - - mutualTLS: [] - - pasetoAuth: [] - - verifiableCredentials: [] - - hmacAuth: [] -tags: - - name: TEA Product - description: Operations related to TEA Products - - name: TEA Component - description: Operations related to TEA Components - - name: TEA Release - description: Operations related to TEA Releases - - name: TEA Collection - description: Operations related to TEA Collections -externalDocs: - description: Transparency Exchange API specification - url: https://github.com/CycloneDX/transparency-exchange-api requestBodies: UpdateTeaProduct: required: true @@ -703,8 +785,13 @@ externalDocs: vendorUuid: $ref: '#/components/schemas/typeUuid' description: Vendor UUID + identifiers: + type: array + items: + $ref: '#/components/schemas/identifier' + description: List of identifiers for the product (TEI, CPE, SWID, etc.) type: - type: string + $ref: '#/components/schemas/typePackage' description: Product type (required) namespace: type: string @@ -713,10 +800,12 @@ externalDocs: type: string description: Product version (optional) qualifiers: - type: object - additionalProperties: - type: string - description: Qualifiers as key-value pairs (optional) + type: array + items: + type: object + additionalProperties: + type: string + description: Array of qualifier objects containing related key-value pairs (optional) subpath: type: string description: Subpath (optional) @@ -730,19 +819,27 @@ externalDocs: vendorUuid: 123e4567-e89b-12d3-a456-426614174000 type: generic version: 1.0.0 + qualifiers: + - os: linux + arch: x86_64 full: - summary: Full product update with all PURL fields + summary: Full product update with all fields value: name: Updated Product Name barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 + identifiers: + - idType: tei + idValue: urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + - idType: cpe + idValue: cpe:2.3:a:example:product:2.1.0:*:*:*:*:*:*:* type: maven namespace: org.example version: 2.1.0 qualifiers: - classifier: sources - repository_url: https://repo.example.com + - classifier: sources + repository_url: https://repo.example.com subpath: src/main CreateTeaProduct: required: true @@ -762,15 +859,13 @@ externalDocs: vendorUuid: $ref: '#/components/schemas/typeUuid' description: Vendor UUID - teiUrns: + identifiers: type: array items: - type: string - description: Transparency Exchange Identifier URN - pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ - description: List of TEI URNs associated with this product + $ref: '#/components/schemas/identifier' + description: List of identifiers for the product (TEI, CPE, SWID, etc.) type: - type: string + $ref: '#/components/schemas/typePackage' description: Product type (required) namespace: type: string @@ -779,10 +874,12 @@ externalDocs: type: string description: Product version (optional) qualifiers: - type: object - additionalProperties: - type: string - description: Qualifiers as key-value pairs (optional) + type: array + items: + type: object + additionalProperties: + type: string + description: Array of qualifier objects containing related key-value pairs (optional) subpath: type: string description: Subpath (optional) @@ -794,6 +891,9 @@ externalDocs: value: name: Example Product type: generic + qualifiers: + - os: linux + arch: x86_64 full: summary: Full product creation with all fields value: @@ -801,14 +901,17 @@ externalDocs: barcode: '123456789012' sku: PROD-001 vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - teiUrns: - - urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + identifiers: + - idType: tei + idValue: urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 + - idType: cpe + idValue: cpe:2.3:a:example:product:2.1.0:*:*:*:*:*:*:* type: maven namespace: org.example version: 2.1.0 qualifiers: - classifier: sources - repository_url: https://repo.example.com + - classifier: sources + repository_url: https://repo.example.com subpath: src/main CreateTeaComponent: required: true @@ -826,7 +929,26 @@ externalDocs: type: array items: $ref: '#/components/schemas/identifier' - description: List of identifiers for the component + description: List of identifiers for the component (TEI, CPE, SWID, etc.) + type: + $ref: '#/components/schemas/typePackage' + description: Component type (required) + namespace: + type: string + description: Component namespace (optional) + version: + type: string + description: Component version (optional) + qualifiers: + type: array + items: + type: object + additionalProperties: + type: string + description: Array of qualifier objects containing related key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) required: - productIdentifier - name @@ -836,9 +958,27 @@ externalDocs: value: productIdentifier: 123e4567-e89b-12d3-a456-426614174000 name: Example Component + type: maven + qualifiers: + - classifier: jar + type: binary + full: + summary: Full component creation with all fields + value: + productIdentifier: 123e4567-e89b-12d3-a456-426614174000 + name: Apache Log4j Core Component identifiers: - - idType: purl - idValue: pkg:maven/org.example/component@1.0.0 + - idType: cpe + idValue: cpe:2.3:a:apache:log4j:2.17.1:*:*:*:*:*:*:* + - idType: tei + idValue: urn:tei:uuid:apache.org:log4j-core-component + type: maven + namespace: org.apache.logging.log4j + version: 2.17.1 + qualifiers: + - classifier: sources + repository_url: https://github.com/apache/logging-log4j2 + subpath: log4j-core UpdateTeaComponent: required: true content: @@ -853,7 +993,54 @@ externalDocs: type: array items: $ref: '#/components/schemas/identifier' - description: List of identifiers for the component + description: List of identifiers for the component (TEI, CPE, SWID, etc.) + type: + $ref: '#/components/schemas/typePackage' + description: Component type + namespace: + type: string + description: Component namespace (optional) + version: + type: string + description: Component version (optional) + qualifiers: + type: array + items: + type: object + additionalProperties: + type: string + description: Array of qualifier objects containing related key-value pairs (optional) + subpath: + type: string + description: Subpath (optional) + examples: + basic: + summary: Basic component update + value: + name: Updated Component Name + type: maven + version: 2.0.0 + qualifiers: + - classifier: jar + type: binary + full: + summary: Full component update with all fields + value: + name: Updated Apache Log4j Core Component + identifiers: + - idType: cpe + idValue: cpe:2.3:a:apache:log4j:2.18.0:*:*:*:*:*:*:* + - idType: tei + idValue: urn:tei:uuid:apache.org:log4j-core-component-updated + type: maven + namespace: org.apache.logging.log4j + version: 2.18.0 + qualifiers: + - classifier: sources + repository_url: https://github.com/apache/logging-log4j2 + - type: library + scope: compile + subpath: log4j-core CreateTeaRelease: required: true content: @@ -863,7 +1050,7 @@ externalDocs: properties: componentIdentifier: $ref: '#/components/schemas/typeUuid' - description: UUID of the TEA Component + description: Customer unique identifier of the TEA Component version: type: string description: Version number @@ -916,6 +1103,24 @@ externalDocs: type: array items: $ref: '#/components/schemas/identifier' + examples: + basic: + summary: Basic release update + value: + version: 1.0.1 + releaseDate: '2024-03-21T10:15:00Z' + preRelease: false + full: + summary: Full release update with identifiers + value: + version: 2.0.0 + releaseDate: '2024-04-15T14:30:00Z' + preRelease: false + identifiers: + - idType: purl + idValue: pkg:maven/org.example/component@2.0.0 + - idType: cpe + idValue: cpe:2.3:a:example:component:2.0.0:*:*:*:*:*:*:* CreateTeaCollection: required: true content: @@ -925,7 +1130,7 @@ externalDocs: properties: releaseIdentifier: $ref: '#/components/schemas/typeUuid' - description: UUID of the TEA Release + description: Customer unique identifier of the TEA Release updateReason: $ref: '#/components/schemas/collectionUpdateReason' description: Reason for collection creation or update @@ -990,7 +1195,7 @@ externalDocs: comment: Initial release of collection artifacts: - name: Example SBOM - type: bom + type: BOM author: name: John Doe email: john.doe@example.com @@ -1003,3 +1208,22 @@ externalDocs: - algType: SHA-256 algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 signatureUrl: https://example.com/sbom.xml.sig +security: + - bearerAuth: [] + - basicAuth: [] + - mutualTLS: [] + - pasetoAuth: [] + - verifiableCredentials: [] + - hmacAuth: [] +tags: + - name: TEA Product + description: Operations related to TEA Products + - name: TEA Component + description: Operations related to TEA Components + - name: TEA Release + description: Operations related to TEA Releases + - name: TEA Collection + description: Operations related to TEA Collections +externalDocs: + description: Transparency Exchange API specification + url: https://github.com/CycloneDX/transparency-exchange-api From 1ab1e850710d86c32a7d1a5cdf418fd9a3bc491c Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 11:12:45 +1000 Subject: [PATCH 11/17] fix(publisher): make the draft valid and give it artifacts, signatures and distribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The document did not validate as OpenAPI 3.1, and the way it failed mattered. `components.operations` is not a field OpenAPI defines, so the three $refs pointing into it never resolved and every delete operation declared no responses at all — a generator reading this produced deletes that returned nothing. Redocly reported 9 errors; it now reports 0. Error responses carried `application/json: {}`, an empty schema that says nothing about what comes back. Publication runs in release pipelines, so failures need to be actionable without a human reading prose: there is now an `error` schema with a machine-readable code. Three things were missing rather than broken. Artifacts. The draft could create products, components, releases and collections but had no way to publish the artifact that carries the SBOM, VEX or attestation the exchange exists to move. Creating an artifact and uploading its bytes are separate operations, because an SBOM is routinely tens of megabytes and a publisher who must resend all of it to fix one metadata field will not fix the field. Content goes up per format in its own media type rather than base64 in JSON, stored bytes are immutable so a recorded checksum cannot start describing different content, and Content-Digest is verified so a truncated retry fails loudly instead of publishing corruption under a checksum that claims otherwise. Signatures. The consumption API exposes only a signatureUrl, leaving a consumer to infer the scheme from the bytes. The publisher knows it, so artifactSignature records the scheme, key id, certificate chain and transparency-log entry — which is what makes a signature checkable rather than merely present. Distribution. TEA describes how data is fetched, not who may fetch it. That boundary works for a consumption API and cannot hold for a publication one, where the same server holds public material, material shared with named counterparties, and internal material. Three visibilities, plus mirroring to other TEA servers. Inheritance narrows and never widens — a child may restrict what it inherits and a server rejects any attempt to loosen it, because otherwise marking a product private would guarantee nothing when any artifact beneath it could be made public. GET /distribution/{uuid} reports declared against effective and names the ancestor responsible, since the gap between those two is where accidental disclosure hides. Idempotency-Key on creates, because pipelines retry and a timeout that actually succeeded otherwise yields a duplicate found by a consumer rather than by the publisher. The README records what changed and three open questions: releases are not split into product and component as the consumption API splits them, there is no publisher-side read surface, and publishTo does not say whether a failed mirror blocks local publication. Signed-off-by: Christopher Langton --- spec/publisher/README.md | 104 +++++ spec/publisher/openapi.yaml | 729 +++++++++++++++++++++++++++++++++++- 2 files changed, 819 insertions(+), 14 deletions(-) diff --git a/spec/publisher/README.md b/spec/publisher/README.md index c1c7c4f..76c0971 100644 --- a/spec/publisher/README.md +++ b/spec/publisher/README.md @@ -4,3 +4,107 @@ The TEA specification is focused on the consumption API, which is the base of conformance with the specification. NOTE: This is a copy of the OpenAPI specification including both consumption and publication APIs. + +## Status of this draft + +The document now validates as OpenAPI 3.1. It previously did not, and the way it +failed mattered: `components.operations` is not a field OpenAPI defines, so the +three `$ref`s pointing into it did not resolve and every delete operation +declared **no responses at all**. A generator reading it produced deletes that +returned nothing, which is why the draft could not be exercised end to end. + +Validated with `npx @redocly/cli lint spec/publisher/openapi.yaml` — 9 errors +before, 0 after. The remaining warnings are query parameters carried over from +the consumption API that no publication path uses; they are left in place +because this file is meant to grow into a combined document. + +## What was fixed + +- **Deletes have responses.** The invalid `components.operations` indirection is + replaced by an explicit shared `204` alongside the existing 401 and 404. +- **Errors have a shape.** Error responses previously carried + `application/json: {}` — an empty schema, saying nothing about what comes + back. There is now an `error` schema with a machine-readable `code`, because + publication happens in release pipelines and a failure has to be actionable + without a human reading prose. +- **Every operation has a summary**, and the server entry is a templated + `https://{host}/tea/v{version}` matching the address a consumer reaches after + reading `/.well-known/tea`, rather than a bare localhost URL. + +## What was added, and why + +### Artifacts + +The draft could create products, components, releases and collections but had no +way to publish an **artifact** — the only object that carries the SBOM, VEX or +attestation the whole exchange exists to move. + +Creating an artifact and uploading its bytes are deliberately separate. An SBOM +is routinely tens of megabytes, and a publisher who must resend all of it to +correct one metadata field will in practice not correct the field. Content is +`PUT` per format in its own media type rather than base64 inside JSON, which +would inflate it by a third for no benefit. + +Stored bytes are immutable. A checksum a consumer has already recorded must not +start describing different content, so replacing content means a new format or a +new artifact, never an overwrite in place. + +Uploads accept `Content-Digest` (RFC 9530) and the server must reject a +mismatch. That is what makes a retry safe: a truncated transfer fails loudly +instead of publishing corrupt content under a checksum claiming otherwise. + +### Signatures + +The consumption API exposes only `signatureUrl`, so a consumer can find a +signature but must infer the scheme from the bytes before verifying it. The +publisher knows the scheme, and `artifactSignature` records it — with the key +identifier, certificate chain and transparency-log entry where the scheme has +them. Stating it turns a signature from something that exists into something +that can be checked. + +### Distribution + +TEA describes how transparency data is fetched, not who is entitled to fetch it. +That is a reasonable boundary for a consumption API and an impossible one for a +publication API: the same server holds material that is deliberately public, +material shared with named counterparties under agreement, and material that is +purely internal. Every publisher otherwise invents this, incompatibly. + +`distribution` gives three visibilities — `private`, `shared` (to named +organisation UUIDs, with optional expiry) and `public` — plus `publishTo` for +mirroring to other TEA servers. + +Two rules carry the weight: + +- **Inheritance narrows, never widens.** An artifact takes its collection's + distribution, a collection its release's, a release its product's. A child may + restrict what it inherits and must not loosen it; a server rejects the attempt + with `DISTRIBUTION_WIDENS_PARENT`. Without this, marking a product private + would guarantee nothing, because any artifact beneath it could quietly be made + public. +- **`public` is one-way in practice.** No later request recalls what has already + been fetched, so a server should require a distinct confirmation for that + transition rather than treating it as an ordinary field update. + +`GET /distribution/{uuid}` reports what an object *declares* alongside what it +*effectively* has, and which ancestor that came from. The gap between declared +and effective is exactly where accidental disclosure hides, so it is reported +rather than left to be reconstructed. + +### Idempotency + +`Idempotency-Key` on creates. Publication runs in pipelines, and pipelines +retry. Without it a timeout that actually succeeded yields a second product on +the next attempt — a duplicate found by a consumer rather than by the publisher. + +## Open questions + +- **Releases are not split.** The consumption API distinguishes product releases + from component releases; this draft has a single `/release`. Left alone here + because changing it rewrites existing request bodies, but they are different + object types and a publisher has to say which one it is creating. +- **No publisher-side read surface.** A publisher reconciling state has to go + through the consumption API, which by design will not show it anything + private — so there is currently no way to list what you have published. +- **`publishTo` describes mirroring but not its failure semantics.** Whether a + failed mirror should block the local publication is unspecified. diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 2953082..ecb2b75 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -13,11 +13,23 @@ info: url: https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE version: 0.1.0-beta.1 servers: - - url: http://localhost/tea/v1 - description: Local development + - url: https://{host}/tea/v{version} + description: | + A TEA server. `host` is the name a consumer reaches after resolving the + domain component of a TEI and reading `/.well-known/tea`; `version` is one + of the versions that document advertises for the chosen endpoint, so the + publication and consumption APIs are addressed identically. + variables: + host: + default: localhost + description: Host serving the API. + version: + default: '1' + description: TEA API version, without a leading `v`. paths: /product/{uuid}: patch: + summary: Update a TEA Product description: Update an existing TEA Product entry operationId: updateTeaProduct parameters: @@ -42,6 +54,7 @@ paths: tags: - TEA Product delete: + summary: Delete a TEA Product description: Delete a TEA Product entry operationId: deleteTeaProduct parameters: @@ -52,11 +65,18 @@ paths: schema: type: string format: uuid - $ref: '#/components/operations/standardDelete' + responses: + '204': + $ref: '#/components/responses/204-object-deleted' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Product /product: post: + summary: Create a TEA Product description: Create TEA Product entry for the supplied product identifier operationId: createTeaProduct requestBody: @@ -74,6 +94,7 @@ paths: - TEA Product /component: post: + summary: Create a TEA Component description: Create a new TEA Component entry operationId: createTeaComponent requestBody: @@ -91,6 +112,7 @@ paths: - TEA Component /component/{uuid}: patch: + summary: Update a TEA Component description: Update an existing TEA Component entry operationId: updateTeaComponent parameters: @@ -115,6 +137,7 @@ paths: tags: - TEA Component delete: + summary: Delete a TEA Component description: Delete a TEA Component entry operationId: deleteTeaComponent parameters: @@ -125,11 +148,18 @@ paths: schema: type: string format: uuid - $ref: '#/components/operations/standardDelete' + responses: + '204': + $ref: '#/components/responses/204-object-deleted' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Component /release: post: + summary: Create a TEA Release description: Create a new TEA Release operationId: createTeaRelease requestBody: @@ -147,6 +177,7 @@ paths: - TEA Release /release/{uuid}: patch: + summary: Update a TEA Release description: Update an existing TEA Release entry operationId: updateTeaRelease parameters: @@ -171,6 +202,7 @@ paths: tags: - TEA Release delete: + summary: Delete a TEA Release description: Delete a TEA Release entry operationId: deleteTeaRelease parameters: @@ -181,13 +213,22 @@ paths: schema: type: string format: uuid - $ref: '#/components/operations/standardDelete' + responses: + '204': + $ref: '#/components/responses/204-object-deleted' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' tags: - TEA Release /collection: post: + summary: Create a TEA Collection description: Create a new TEA Collection operationId: createTeaCollection + parameters: + - $ref: '#/components/parameters/idempotencyKey' requestBody: $ref: '#/components/requestBodies/CreateTeaCollection' responses: @@ -199,10 +240,472 @@ paths: $ref: '#/components/responses/401-unauthorized' '404': $ref: '#/components/responses/404-object-by-id-not-found' + '409': + $ref: '#/components/responses/409-conflict' + tags: + - TEA Collection + /collection/{uuid}: + patch: + summary: Update a TEA Collection + description: | + Update a collection. + + A collection is the unit TEA versions, and its `updateReason` is how a + consumer distinguishes a correction from a re-issue. A server MUST + require an update reason on any change that alters the artifacts a + collection publishes, because a collection that changes without saying + why is indistinguishable from one that was tampered with. + operationId: updateTeaCollection + parameters: + - $ref: '#/components/parameters/objectUuid' + requestBody: + $ref: '#/components/requestBodies/UpdateTeaCollection' + responses: + '200': + $ref: '#/components/responses/200-tea-collection-updated' + '400': + $ref: '#/components/responses/400-invalid-request-body' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + '409': + $ref: '#/components/responses/409-conflict' tags: - TEA Collection + delete: + summary: Delete a TEA Collection + description: Delete a collection and the artifact entries it holds. + operationId: deleteTeaCollection + parameters: + - $ref: '#/components/parameters/objectUuid' + responses: + '204': + $ref: '#/components/responses/204-object-deleted' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Collection + + /artifact: + post: + summary: Create a TEA Artifact + description: | + Create an artifact entry within a collection. + + This registers the artifact and its formats; it does not carry the + bytes. Content is uploaded per format through + `/artifact/{uuid}/format/{formatIndex}/content`, or referenced by URL + when the publisher hosts it elsewhere. Separating the two matters + because an SBOM is routinely tens of megabytes, and a publisher that + must resend all of it to correct one field will eventually not correct + the field. + operationId: createTeaArtifact + parameters: + - $ref: '#/components/parameters/idempotencyKey' + requestBody: + $ref: '#/components/requestBodies/CreateTeaArtifact' + responses: + '201': + $ref: '#/components/responses/201-tea-artifact-created' + '400': + $ref: '#/components/responses/400-invalid-request-body' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + '409': + $ref: '#/components/responses/409-conflict' + tags: + - TEA Artifact + /artifact/{uuid}: + patch: + summary: Update a TEA Artifact + description: | + Update an artifact's metadata. + + Stored bytes are immutable: a checksum a consumer has already recorded + must not start describing different content. Replacing content means + adding a new format or a new artifact version, never rewriting one in + place. + operationId: updateTeaArtifact + parameters: + - $ref: '#/components/parameters/objectUuid' + requestBody: + $ref: '#/components/requestBodies/UpdateTeaArtifact' + responses: + '200': + $ref: '#/components/responses/200-tea-artifact-updated' + '400': + $ref: '#/components/responses/400-invalid-request-body' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + '409': + $ref: '#/components/responses/409-conflict' + tags: + - TEA Artifact + delete: + summary: Delete a TEA Artifact + description: Delete an artifact entry and any content this server holds for it. + operationId: deleteTeaArtifact + parameters: + - $ref: '#/components/parameters/objectUuid' + responses: + '204': + $ref: '#/components/responses/204-object-deleted' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Artifact + /artifact/{uuid}/format/{formatIndex}/content: + put: + summary: Upload artifact content + description: | + Upload the bytes of one artifact format. + + The body is the artifact itself, sent with its own media type rather + than wrapped in JSON or base64 — an SBOM is already a document, and + re-encoding it inflates it by a third for no benefit. + + A client SHOULD send `Content-Digest`. Where it does, the server MUST + verify the digest against the received bytes and reject a mismatch with + `CHECKSUM_MISMATCH`. This is what makes an upload safe to retry: a + truncated transfer fails loudly instead of publishing a corrupt artifact + under a checksum that says otherwise. + operationId: uploadTeaArtifactContent + parameters: + - $ref: '#/components/parameters/objectUuid' + - $ref: '#/components/parameters/formatIndex' + - $ref: '#/components/parameters/contentDigest' + requestBody: + required: true + description: The artifact bytes, in the media type declared by the format. + content: + application/octet-stream: + schema: + type: string + format: binary + application/vnd.cyclonedx+json: + schema: + type: string + format: binary + application/spdx+json: + schema: + type: string + format: binary + application/sarif+json: + schema: + type: string + format: binary + responses: + '200': + $ref: '#/components/responses/200-tea-artifact-updated' + '400': + $ref: '#/components/responses/400-invalid-request-body' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + '413': + $ref: '#/components/responses/413-payload-too-large' + '415': + $ref: '#/components/responses/415-unsupported-media-type' + tags: + - TEA Artifact + /artifact/{uuid}/format/{formatIndex}/signature: + put: + summary: Attach a detached signature + description: | + Attach a detached signature to one artifact format, declaring the scheme + it was produced with. + + The consumption API exposes a `signatureUrl` and nothing else, so a + consumer is left to infer the scheme from the bytes. Recording it here + is what turns a signature from something that exists into something that + can be checked. + operationId: uploadTeaArtifactSignature + parameters: + - $ref: '#/components/parameters/objectUuid' + - $ref: '#/components/parameters/formatIndex' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/artifactSignature' + application/octet-stream: + schema: + type: string + format: binary + description: | + The raw detached signature. The scheme MUST then be supplied in + the `X-TEA-Signature-Type` header, since bytes alone do not say + how to verify them. + responses: + '200': + $ref: '#/components/responses/200-tea-artifact-updated' + '400': + $ref: '#/components/responses/400-invalid-request-body' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + '415': + $ref: '#/components/responses/415-unsupported-media-type' + tags: + - TEA Artifact + + /distribution/{uuid}: + get: + summary: Read effective distribution + description: | + The distribution actually in force for an object, after inheritance. + + A publisher needs to be able to answer "who can see this?" without + replaying the object tree in their head. The response reports both what + the object declares and what it inherits, because the difference between + the two is exactly where accidental disclosure hides. + operationId: getTeaDistribution + parameters: + - $ref: '#/components/parameters/objectUuid' + responses: + '200': + $ref: '#/components/responses/200-tea-distribution' + '401': + $ref: '#/components/responses/401-unauthorized' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + tags: + - TEA Distribution + put: + summary: Set distribution + description: | + Set an object's own distribution, replacing any it previously declared. + + A server MUST reject a distribution that is wider than the effective + distribution of the object's parent, with `DISTRIBUTION_WIDENS_PARENT`. + A server SHOULD require a distinct confirmation for a change to + `public`, because that transition cannot be undone by any later request: + whatever was fetched while it was public stays fetched. + operationId: setTeaDistribution + parameters: + - $ref: '#/components/parameters/objectUuid' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/distribution' + responses: + '200': + $ref: '#/components/responses/200-tea-distribution' + '400': + $ref: '#/components/responses/400-invalid-request-body' + '401': + $ref: '#/components/responses/401-unauthorized' + '403': + $ref: '#/components/responses/403-forbidden' + '404': + $ref: '#/components/responses/404-object-by-id-not-found' + '409': + $ref: '#/components/responses/409-conflict' + tags: + - TEA Distribution components: schemas: + error: + type: object + description: | + A machine-readable error. Publication is an automated activity — a + release pipeline is usually the caller — so a failure has to be + actionable without a human reading prose. + properties: + code: + type: string + description: Stable, machine-readable reason. Clients branch on this, never on `message`. + enum: + - INVALID_REQUEST + - UNAUTHORIZED + - FORBIDDEN + - OBJECT_NOT_FOUND + - DUPLICATE_OBJECT + - VERSION_CONFLICT + - DISTRIBUTION_WIDENS_PARENT + - UNSUPPORTED_MEDIA_TYPE + - PAYLOAD_TOO_LARGE + - CHECKSUM_MISMATCH + - SIGNATURE_INVALID + message: + type: string + description: Human-readable explanation. Not a stable interface. + field: + type: string + description: JSON Pointer to the offending member of the request body, where one applies. + required: + - code + - message + + distribution: + type: object + description: | + Who may read an object through the consumption API. + + TEA describes how transparency data is *fetched*, not who is entitled to + fetch it, which leaves every publisher to invent an answer. A publisher + API cannot avoid the question: the same server holds material that is + deliberately public, material shared with named counterparties under an + agreement, and material that is purely internal. + + Distribution is resolved by inheritance. An object with no distribution + of its own takes its parent's — artifact from collection, collection + from release, release from product. An object MAY narrow what it + inherits and MUST NOT widen it: a server MUST reject with + `DISTRIBUTION_WIDENS_PARENT` any request that would make a child more + visible than its parent. Without that rule, publishing a product + privately would be no guarantee at all, because any artifact beneath it + could quietly be made public. + properties: + visibility: + type: string + description: | + `private` — only the publishing organisation may read it. + + `shared` — readable by the publishing organisation and by the + organisations named in `sharedWith`. Authentication is required and + the reader's identity decides the answer. + + `public` — readable without authentication. This is the only value + that makes an object part of the open transparency web, and it is + irreversible in practice: what has been fetched cannot be recalled. + enum: + - private + - shared + - public + default: private + sharedWith: + type: array + description: | + Organisations entitled to read this object when `visibility` is + `shared`. Identified by the consuming organisation's UUID on this + server. MUST be empty for any other visibility, so that a later + change to `private` cannot silently leave a grant in place. + items: + $ref: '#/components/schemas/shareGrant' + publishTo: + type: array + description: | + Remote TEA servers this object is mirrored to. Mirroring is a copy, + not a reference: once an object reaches another server, this + server's distribution no longer governs it. + items: + $ref: '#/components/schemas/publicationTarget' + required: + - visibility + + shareGrant: + type: object + description: An entitlement for one organisation to read one object. + properties: + organizationUuid: + type: string + format: uuid + description: The consuming organisation's UUID on this server. + organizationName: + type: string + description: Display name at the time of granting. Advisory; the UUID is the identity. + grantedAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + description: | + When the grant lapses. A grant for the duration of an evaluation or + a contract is the common case, and a server that cannot express an + expiry pushes publishers towards grants nobody ever revokes. + required: + - organizationUuid + + publicationTarget: + type: object + description: A remote TEA server this publisher mirrors objects to. + properties: + uuid: + type: string + format: uuid + description: Identifier of a target already configured on this server. + domain: + type: string + description: | + The TEI domain-name component of the target, which is what a + consumer resolves. Reported so a publisher can see where a copy + went without holding the target's credentials. + state: + type: string + description: State of the most recent mirroring attempt for this object. + enum: + - pending + - published + - failed + lastAttemptAt: + type: string + format: date-time + detail: + type: string + description: Why the last attempt failed, when it did. + required: + - uuid + + artifactSignature: + type: object + description: | + A detached signature over an artifact format's bytes. + + The consumption API exposes only a `signatureUrl`, which tells a + consumer where a signature lives but not what it is, so verifying one + means guessing the scheme. A publisher knows, and stating it here is + what makes the signature checkable rather than merely present. + properties: + signatureType: + type: string + description: The signature scheme, so a consumer knows how to verify rather than having to guess. + enum: + - jws + - cose + - pgp + - x509-cms + - sigstore-bundle + - ssh + url: + type: string + format: url + description: Direct download URL for the detached signature. + keyId: + type: string + description: Key identifier the signature was produced with, where the scheme has one. + certificateUrl: + type: string + format: url + description: Signing certificate or chain, where verification needs one. + transparencyLogUrl: + type: string + format: url + description: | + Entry in a signature transparency log, such as Rekor. A signature + that is itself publicly logged is far harder to backdate. + signedAt: + type: string + format: date-time + required: + - signatureType + - url + identifier: type: object description: An identifier with a specified type @@ -533,6 +1036,93 @@ components: - swid - swift responses: + 204-object-deleted: + description: | + The object was deleted and is no longer resolvable through the + consumption API. + + Deletion does not retract what consumers already hold. An object that + was published outside this server SHOULD also be withdrawn through a CLE + `withdrawn` event, so a consumer who acted on it can learn that it was + retracted rather than simply finding it gone. + 409-conflict: + description: | + The request conflicts with the current state of the object: a create + that would duplicate an existing natural key, an update against a + version that has since been superseded, or a distribution change that + would make an object more visible than the object it belongs to. + content: + application/json: + schema: + $ref: '#/components/schemas/error' + 413-payload-too-large: + description: The uploaded artifact or signature exceeds the size this server accepts. + content: + application/json: + schema: + $ref: '#/components/schemas/error' + 415-unsupported-media-type: + description: The supplied media type is not one this server accepts for the object. + content: + application/json: + schema: + $ref: '#/components/schemas/error' + 403-forbidden: + description: | + Authenticated, but not entitled to perform this operation on this + object. Distinct from 401 on purpose: a publisher debugging a pipeline + needs to know whether the credential was rejected or the permission was. + content: + application/json: + schema: + $ref: '#/components/schemas/error' + 200-tea-collection-updated: + description: TEA Collection updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/collection' + 201-tea-artifact-created: + description: TEA Artifact created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/artifact' + 200-tea-artifact-updated: + description: TEA Artifact updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/artifact' + 200-tea-distribution: + description: The distribution in force for the object + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + format: uuid + description: The object the distribution applies to. + declared: + description: | + What this object sets for itself. Absent when the object + declares nothing and relies entirely on inheritance. + $ref: '#/components/schemas/distribution' + effective: + description: What is actually enforced, after inheritance. + $ref: '#/components/schemas/distribution' + inheritedFrom: + type: string + format: uuid + description: | + The ancestor the effective distribution came from, when it was + not declared on this object. Naming it is what lets a + publisher fix the cause rather than the symptom. + required: + - uuid + - effective 200-tea-product-updated: description: TEA Product updated successfully content: @@ -635,6 +1225,52 @@ components: content: application/json: {} parameters: + objectUuid: + name: uuid + in: path + required: true + description: UUID of the object in the TEA server. + schema: + type: string + format: uuid + formatIndex: + name: formatIndex + in: path + required: true + description: | + Zero-based index of the format within the artifact's `formats` array. + Formats are addressed positionally because a media type is not unique + within an artifact — the same document is often published both + compressed and not. + schema: + type: integer + minimum: 0 + idempotencyKey: + name: Idempotency-Key + in: header + required: false + description: | + A client-chosen key making a create safe to retry. + + Publication runs in release pipelines, which retry. Without this, a + timeout that actually succeeded produces a second product on the next + attempt, and the duplicate is discovered by a consumer rather than by + the publisher. A server that supports the key MUST return the original + result for a repeat of the same key rather than creating again. + schema: + type: string + maxLength: 255 + contentDigest: + name: Content-Digest + in: header + required: false + description: | + Digest of the request body, per RFC 9530, e.g. `sha-256=::`. + Where supplied, the server MUST verify it against the bytes received and + reject a mismatch rather than storing content that does not match the + checksum it will be published under. + schema: + type: string page-offset: name: pageOffset description: Pagination offset @@ -756,16 +1392,73 @@ components: description: | HMAC authentication uses a shared secret key to create a signature of the request content using a secure hash algorithm (SHA256, SHA384, SHA512, SHA3-256, SHA3-384, or SHA3-512). The server verifies this signature to authenticate the request. - operations: - standardDelete: - responses: - '204': - description: Resource successfully deleted - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' requestBodies: + UpdateTeaCollection: + required: true + content: + application/json: + schema: + type: object + properties: + updateReason: + $ref: '#/components/schemas/collectionUpdateReason' + distribution: + $ref: '#/components/schemas/distribution' + required: + - updateReason + CreateTeaArtifact: + required: true + content: + application/json: + schema: + type: object + properties: + collectionUuid: + type: string + format: uuid + description: The collection this artifact belongs to. + name: + type: string + description: Human-readable name of the artifact. + type: + type: string + description: Type of artifact, as defined by the consumption API. + author: + type: string + description: Author of the artifact. + formats: + type: array + description: | + The representations this artifact is published in. A format + may carry a `url` when the publisher hosts the bytes itself; + otherwise the bytes are uploaded to this server afterwards. + items: + $ref: '#/components/schemas/artifactFormat' + distribution: + $ref: '#/components/schemas/distribution' + required: + - collectionUuid + - name + - formats + UpdateTeaArtifact: + required: true + content: + application/json: + schema: + type: object + description: | + Metadata only. Stored bytes are immutable — see the operation + description for why replacing content means a new format or a new + artifact rather than an overwrite. + properties: + name: + type: string + type: + type: string + author: + type: string + distribution: + $ref: '#/components/schemas/distribution' UpdateTeaProduct: required: true content: @@ -1224,6 +1917,14 @@ tags: description: Operations related to TEA Releases - name: TEA Collection description: Operations related to TEA Collections + - name: TEA Artifact + description: | + Operations on the artifacts a collection publishes, including uploading + their content and attaching detached signatures. + - name: TEA Distribution + description: | + Operations controlling who may read an object through the consumption + API: the publishing organisation alone, named counterparties, or anyone. externalDocs: description: Transparency Exchange API specification url: https://github.com/CycloneDX/transparency-exchange-api From 621fe16cd4a8b50ec23afbfd22e6b15f7bb2086c Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 11:25:04 +1000 Subject: [PATCH 12/17] refactor(publisher): generate the spec from the consumption API to end the drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publisher document was a hand-maintained copy of the consumption specification and had fallen four minor versions behind it. The consumption API is at 0.4.0; the copy still carried schemas from 0.0.3, renamed along the way — artifactFormat beside the real artifact-format, typeUuid beside uuid, a private artifactChecksum beside checksum. A combined document therefore held two definitions of the same concept, each free to move independently, with nothing to say which one a publisher should believe. It is now generated. build.mjs merges spec/openapi.yaml with a publication overlay, so everything shared is a $ref into the consumption specification's own definitions and the publisher version is copied from the consumer rather than chosen. The build refuses to run if the overlay declares a schema, parameter, response, request body or path method the consumption specification already owns — the overlay may only add. Alignment corrections this surfaced: - Releases are split into /productRelease and /componentRelease. The consumption API has distinguished the two since 0.1.0 and a single /release could not say which it was creating. - Collections are addressed through their release. The consumption specification states a collection's UUID matches its release and that an update only changes the version, so it is not an independently created object: PUT /productRelease/{uuid}/collection, not POST /collection. - Errors reuse the consumption envelope. publisher-error-response has the same { error } shape as error-response over an enumeration that repeats OBJECT_UNKNOWN and OBJECT_NOT_SHAREABLE and adds the failures only a writer can hit, so clients branch on one enumeration rather than two. - distribution became access-policy, because the consumption specification already uses release-distribution for a downloadable build with its own URL and checksums. Reusing the word for access control would have been actively misleading. Enforcement of the access policy needs no new consumer behaviour: a denied consumer gets OBJECT_NOT_SHAREABLE, which already exists in unknown-error-type. The generated document now adds no lint errors of its own. It reports 24, and linting spec/openapi.yaml alone reports the same 24 — 23 operations missing summaries and one OpenAPI 3.0 `nullable` on pagination-details.nextPageToken. Those belong upstream rather than patched here, where they would put the fork at odds with a file upstream owns. CI compares the two counts and fails only if the overlay makes things worse. Signed-off-by: Christopher Langton --- .github/workflows/publisher-spec-sync.yaml | 61 + .gitignore | 1 + spec/publisher/README.md | 181 +- spec/publisher/build.mjs | 151 + spec/publisher/openapi.yaml | 3801 ++++++++++++-------- spec/publisher/overlay.yaml | 1114 ++++++ spec/publisher/package-lock.json | 3225 +++++++++++++++++ spec/publisher/package.json | 15 + 8 files changed, 7013 insertions(+), 1536 deletions(-) create mode 100644 .github/workflows/publisher-spec-sync.yaml create mode 100644 spec/publisher/build.mjs create mode 100644 spec/publisher/overlay.yaml create mode 100644 spec/publisher/package-lock.json create mode 100644 spec/publisher/package.json diff --git a/.github/workflows/publisher-spec-sync.yaml b/.github/workflows/publisher-spec-sync.yaml new file mode 100644 index 0000000..0493594 --- /dev/null +++ b/.github/workflows/publisher-spec-sync.yaml @@ -0,0 +1,61 @@ +name: Publisher Spec Sync + +# The publisher document is generated from the consumption specification plus +# spec/publisher/overlay.yaml. It was previously a hand-maintained copy and +# drifted four minor versions behind, carrying duplicate definitions of shared +# objects under different names. This job fails if it drifts again. + +on: + push: + branches: ['main'] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + check: + name: Publisher spec is in sync + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install + working-directory: spec/publisher + run: npm ci || npm install + + - name: Rebuild and compare + working-directory: spec/publisher + run: npm run check + + - name: Lint the generated document + working-directory: spec/publisher + # The consumption specification currently carries 24 lint errors + # of its own (23 missing operation summaries, one OpenAPI 3.0 + # `nullable`). The overlay adds none, so this compares the two + # counts rather than requiring zero, and fails only if the + # publisher document is worse than the spec it is built from. + run: | + set -o pipefail + count() { + npx redocly lint "$1" --format=json 2>/dev/null \ + | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);console.log(j.problems.filter(p=>p.severity==="error").length)})' + } + base=$(count ../openapi.yaml) + pub=$(count openapi.yaml) + echo "consumption errors: $base, publisher errors: $pub" + if [ "$pub" -gt "$base" ]; then + echo "The publication overlay introduced $((pub - base)) new error(s)." + npx redocly lint openapi.yaml + exit 1 + fi diff --git a/.gitignore b/.gitignore index 981765e..1b408c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ out/* .DS_Store +node_modules/ diff --git a/spec/publisher/README.md b/spec/publisher/README.md index 76c0971..04fe921 100644 --- a/spec/publisher/README.md +++ b/spec/publisher/README.md @@ -3,108 +3,151 @@ This specification will be a recommended TEA publisher API. The TEA specification is focused on the consumption API, which is the base of conformance with the specification. -NOTE: This is a copy of the OpenAPI specification including both consumption and publication APIs. - -## Status of this draft - -The document now validates as OpenAPI 3.1. It previously did not, and the way it -failed mattered: `components.operations` is not a field OpenAPI defines, so the -three `$ref`s pointing into it did not resolve and every delete operation -declared **no responses at all**. A generator reading it produced deletes that -returned nothing, which is why the draft could not be exercised end to end. - -Validated with `npx @redocly/cli lint spec/publisher/openapi.yaml` — 9 errors -before, 0 after. The remaining warnings are query parameters carried over from -the consumption API that no publication path uses; they are left in place -because this file is meant to grow into a combined document. - -## What was fixed - -- **Deletes have responses.** The invalid `components.operations` indirection is - replaced by an explicit shared `204` alongside the existing 401 and 404. -- **Errors have a shape.** Error responses previously carried - `application/json: {}` — an empty schema, saying nothing about what comes - back. There is now an `error` schema with a machine-readable `code`, because - publication happens in release pipelines and a failure has to be actionable - without a human reading prose. -- **Every operation has a summary**, and the server entry is a templated - `https://{host}/tea/v{version}` matching the address a consumer reaches after - reading `/.well-known/tea`, rather than a bare localhost URL. - -## What was added, and why +`openapi.yaml` here is **generated**. It is the consumption specification plus +the publication operations in `overlay.yaml`, merged by `build.mjs`. + +```sh +cd spec/publisher +npm install +npm run build # regenerate openapi.yaml +npm run check # fail if it has drifted from spec/openapi.yaml +npm run lint # redocly +``` + +## Why it is generated + +It used to be a hand-maintained copy, and it drifted, as copies do. While the +consumption API moved to 0.4.0 the copy still carried schemas from 0.0.3, under +different names — `artifactFormat` beside the real `artifact-format`, `typeUuid` +beside `uuid`, a private `artifactChecksum` beside `checksum`. A combined +document then holds two definitions of the same concept, each free to move +independently, with nothing to say which one a publisher should believe. + +Generating it removes the possibility rather than the symptom. Everything shared +is a `$ref` into the consumption specification's own definitions, so there is +exactly one definition of a product, a checksum or an artifact format, and the +publisher version *is* the consumer version because `build.mjs` copies it rather +than letting anyone choose one. + +`build.mjs` refuses to build if `overlay.yaml` declares a schema, parameter, +response, request body or path method that the consumption specification already +owns. That is the drift check with teeth: the overlay may only add. + +## Alignment corrections made when this was regenerated + +- **Releases are split.** The draft had a single `/release`, but the consumption + API distinguishes a product release from a component release. There are now + `/productRelease` and `/componentRelease`, matching `productRelease` and + `release` in the consumption schemas. +- **Collections are addressed through their release.** The consumption + specification states that a collection's UUID matches the release it belongs + to, and that an update only changes the version — so a collection is not an + independently created object. Publishing one is now + `PUT /productRelease/{uuid}/collection` (and the component equivalent), which + publishes the next version, rather than `POST /collection`. +- **Errors reuse the consumption envelope.** The draft returned + `application/json: {}` — an empty schema saying nothing about failures. + `publisher-error-response` uses the same `{ error: }` shape as + `error-response`, over an enumeration that repeats the consumption values + (`OBJECT_UNKNOWN`, `OBJECT_NOT_SHAREABLE`) and adds the ones only a writer can + hit, so a client branches on one enumeration rather than two. +- **`distribution` was renamed `access-policy`.** The consumption specification + already uses `release-distribution` for something else entirely — a + downloadable build with its own URL and checksums. Reusing the word for access + control in a combined document would have been actively misleading. +- **The invalid delete indirection is gone.** `components.operations` is not a + field OpenAPI defines, so the three `$ref`s pointing into it never resolved + and every delete declared no responses at all. A generator reading the old + draft produced deletes that returned nothing. + +## What the publication overlay adds ### Artifacts The draft could create products, components, releases and collections but had no -way to publish an **artifact** — the only object that carries the SBOM, VEX or -attestation the whole exchange exists to move. +way to publish the artifact carrying the SBOM, VEX or attestation the exchange +exists to move. -Creating an artifact and uploading its bytes are deliberately separate. An SBOM +Registering an artifact and uploading its bytes are separate operations. An SBOM is routinely tens of megabytes, and a publisher who must resend all of it to correct one metadata field will in practice not correct the field. Content is `PUT` per format in its own media type rather than base64 inside JSON, which would inflate it by a third for no benefit. -Stored bytes are immutable. A checksum a consumer has already recorded must not -start describing different content, so replacing content means a new format or a -new artifact, never an overwrite in place. - -Uploads accept `Content-Digest` (RFC 9530) and the server must reject a -mismatch. That is what makes a retry safe: a truncated transfer fails loudly -instead of publishing corrupt content under a checksum claiming otherwise. +Stored bytes are immutable — a checksum a consumer already recorded must not +begin describing different content, so replacing content means a new format or a +new artifact. Uploads accept `Content-Digest` (RFC 9530) and a mismatch is +rejected, which is what makes a retry safe: a truncated transfer fails loudly +instead of publishing corruption under a checksum claiming otherwise. ### Signatures -The consumption API exposes only `signatureUrl`, so a consumer can find a -signature but must infer the scheme from the bytes before verifying it. The -publisher knows the scheme, and `artifactSignature` records it — with the key -identifier, certificate chain and transparency-log entry where the scheme has -them. Stating it turns a signature from something that exists into something -that can be checked. +`signatures/signature.md` asks for an indication of the hash algorithm, an +indicator of the certificate used, and the intermediate and signing +certificates. The consumption API exposes only a `signatureUrl`, so a consumer +must infer the scheme from the bytes before it can verify anything. +`artifact-signature` records the scheme, algorithm, key identifier, certificate +and chain, and the transparency-log entry where the scheme has one — which is +what turns a signature from something that exists into something checkable. -### Distribution +### Access policy TEA describes how transparency data is fetched, not who is entitled to fetch it. That is a reasonable boundary for a consumption API and an impossible one for a -publication API: the same server holds material that is deliberately public, -material shared with named counterparties under agreement, and material that is -purely internal. Every publisher otherwise invents this, incompatibly. +publication API, where the same server holds material that is deliberately +public, material shared with named counterparties under agreement, and material +that is purely internal. Absent an answer, every publisher invents one. -`distribution` gives three visibilities — `private`, `shared` (to named -organisation UUIDs, with optional expiry) and `public` — plus `publishTo` for -mirroring to other TEA servers. - -Two rules carry the weight: +Three visibilities — `private`, `shared` (to named organisation UUIDs, with +optional expiry) and `public` — plus `publishTo` for mirroring to other TEA +servers. Two rules carry the weight: - **Inheritance narrows, never widens.** An artifact takes its collection's - distribution, a collection its release's, a release its product's. A child may + policy, a collection its release's, a release its product's. A child may restrict what it inherits and must not loosen it; a server rejects the attempt - with `DISTRIBUTION_WIDENS_PARENT`. Without this, marking a product private - would guarantee nothing, because any artifact beneath it could quietly be made + with `ACCESS_WIDENS_PARENT`. Without this, marking a product private would + guarantee nothing, because any artifact beneath it could quietly be made public. - **`public` is one-way in practice.** No later request recalls what has already - been fetched, so a server should require a distinct confirmation for that + been fetched, so a server should require a separate confirmation for that transition rather than treating it as an ordinary field update. -`GET /distribution/{uuid}` reports what an object *declares* alongside what it -*effectively* has, and which ancestor that came from. The gap between declared -and effective is exactly where accidental disclosure hides, so it is reported -rather than left to be reconstructed. +This needs no new consumer-side behaviour: a consumer denied by the policy gets +the consumption specification's existing `OBJECT_NOT_SHAREABLE`, which is +already in `unknown-error-type`. + +`GET /accessPolicy/{uuid}` reports what an object *declares* alongside what it +*effectively* has and which ancestor that came from. The gap between declared +and effective is where accidental disclosure hides, so it is reported rather +than left to be reconstructed. ### Idempotency `Idempotency-Key` on creates. Publication runs in pipelines, and pipelines -retry. Without it a timeout that actually succeeded yields a second product on +retry. Without it a timeout that actually succeeded yields a second object on the next attempt — a duplicate found by a consumer rather than by the publisher. +## Known lint baseline + +`npm run lint` reports 24 errors. All 24 are inherited: linting +`spec/openapi.yaml` on its own reports exactly the same 24, so the publication +overlay contributes none. + +- 23 × `operation-summary` — consumption operations have no `summary`. +- 1 × `struct` — `pagination-details.nextPageToken` uses `nullable: false`, + which OpenAPI 3.1 removed in favour of a type union. + +These are worth fixing in the consumption specification rather than papering +over here, since patching them in this fork would put it at odds with upstream +on a file upstream owns. + ## Open questions -- **Releases are not split.** The consumption API distinguishes product releases - from component releases; this draft has a single `/release`. Left alone here - because changing it rewrites existing request bodies, but they are different - object types and a publisher has to say which one it is creating. - **No publisher-side read surface.** A publisher reconciling state has to go through the consumption API, which by design will not show it anything private — so there is currently no way to list what you have published. -- **`publishTo` describes mirroring but not its failure semantics.** Whether a - failed mirror should block the local publication is unspecified. +- **`publishTo` does not define failure semantics.** Whether a failed mirror + should block the local publication is unspecified. +- **Artifact versions.** The consumption API addresses artifacts as + `/artifact/{uuid}/{artifactVersion}`, but the overlay's create and update do + not yet say how a publisher advances that version. diff --git a/spec/publisher/build.mjs b/spec/publisher/build.mjs new file mode 100644 index 0000000..7acddc3 --- /dev/null +++ b/spec/publisher/build.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +// Build spec/publisher/openapi.yaml from the consumption specification plus a +// publication overlay. +// +// The publisher document used to be a hand-maintained copy of the consumption +// spec. It drifted, as copies do: while the consumption API reached 0.4.0 the +// copy was still carrying schemas from 0.0.3, under different names — +// `artifactFormat` beside the real `artifact-format`, `typeUuid` beside `uuid`, +// a private `artifactChecksum` beside `checksum`. A combined document then has +// two definitions of the same concept, each free to move independently, and +// nothing in it says which one a publisher should believe. +// +// Generating the document removes the possibility. Everything shared is a +// `$ref` into the consumption spec's own definitions, so there is exactly one +// definition of a product, a checksum or an artifact format, and the publisher +// version is the consumer version by construction. +// +// node spec/publisher/build.mjs # regenerate openapi.yaml +// node spec/publisher/build.mjs --check # fail (exit 1) if it has drifted +// +// Run --check in CI after any change to spec/openapi.yaml. + +import { readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import yaml from 'js-yaml' + +const here = dirname(fileURLToPath(import.meta.url)) +const consumerPath = resolve(here, '..', 'openapi.yaml') +const overlayPath = resolve(here, 'overlay.yaml') +const outPath = resolve(here, 'openapi.yaml') + +const consumer = yaml.load(readFileSync(consumerPath, 'utf8')) +const overlay = yaml.load(readFileSync(overlayPath, 'utf8')) + +/** + * Merge the overlay onto the consumption document. + * + * Deliberately shallow-but-structured rather than a generic deep merge: the + * overlay may only ADD. If it defines a key the consumption spec already + * defines, that is drift reappearing — the publisher redefining something the + * consumer owns — so it is a build failure rather than a silent overwrite. + */ +const collisions = [] + +function mergeSection(target, source, path) { + for (const [key, value] of Object.entries(source ?? {})) { + if (target[key] === undefined) { + target[key] = value + continue + } + // Path items are the one place both documents legitimately contribute: + // the consumer defines GET /product/{uuid}, the publisher adds PATCH + // and DELETE to the same path. Merge per method, and still refuse to + // redefine a method the consumer already declared. + if (path === 'paths') { + for (const [method, op] of Object.entries(value)) { + if (target[key][method] !== undefined) { + collisions.push(`${path}.${key}.${method}`) + continue + } + target[key][method] = op + } + continue + } + collisions.push(`${path}.${key}`) + } +} + +// info: the publisher document describes the same API version as the consumer. +// The version is copied, never chosen, so the two cannot report different +// numbers for the same object model. +consumer.info = { + ...consumer.info, + title: 'Transparency Exchange API', + summary: 'The OWASP Transparency Exchange API specification for consumers and publishers', + description: [ + consumer.info.description?.trim(), + '', + 'This document is the consumption specification plus the publication', + 'operations. Everything the two share — products, components, releases,', + 'collections, artifacts, checksums, identifiers — has a single definition,', + 'taken from the consumption specification, so a publisher and a consumer', + 'cannot hold different ideas of the same object.', + '', + 'Generated by spec/publisher/build.mjs. Do not edit by hand: edit', + 'spec/publisher/overlay.yaml, or the consumption specification, and rebuild.', + ].filter(v => v !== undefined).join('\n'), +} + +mergeSection(consumer.paths, overlay.paths, 'paths') + +for (const section of ['schemas', 'parameters', 'responses', 'requestBodies', 'securitySchemes']) { + consumer.components[section] ??= {} + mergeSection(consumer.components[section], overlay.components?.[section], `components.${section}`) +} + +// Tags are a list, not a map. +const existingTags = new Set((consumer.tags ?? []).map(t => t.name)) +consumer.tags = [ + ...(consumer.tags ?? []), + ...(overlay.tags ?? []).filter(t => !existingTags.has(t.name)), +] + +if (collisions.length) { + console.error('The overlay redefines objects the consumption specification already owns:') + for (const c of collisions) console.error(` ${c}`) + console.error('\nRemove them from the overlay and $ref the consumption definition instead.') + process.exit(1) +} + +const banner = `# GENERATED FILE - do not edit by hand. +# +# Built by spec/publisher/build.mjs from: +# spec/openapi.yaml (the consumption specification, version ${consumer.info.version}) +# spec/publisher/overlay.yaml (the publication operations) +# +# Regenerate with: node spec/publisher/build.mjs +` + +const body = yaml.dump(consumer, { + lineWidth: 100, + noRefs: true, + quotingType: '"', +}) + +const out = `${banner}\n${body}` + +if (process.argv.includes('--check')) { + let current = '' + try { + current = readFileSync(outPath, 'utf8') + } catch { + current = '' + } + if (current !== out) { + console.error('spec/publisher/openapi.yaml has drifted from the consumption specification.') + console.error('Run: node spec/publisher/build.mjs') + process.exit(1) + } + console.log(`publisher spec is in sync with consumption specification ${consumer.info.version}.`) + process.exit(0) +} + +writeFileSync(outPath, out) +console.log( + `Wrote ${outPath}\n` + + ` version ${consumer.info.version}, ` + + `${Object.keys(consumer.paths).length} paths, ` + + `${Object.keys(consumer.components.schemas).length} schemas.`, +) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index ecb2b75..8b91747 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -1,388 +1,982 @@ -jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base +# GENERATED FILE - do not edit by hand. +# +# Built by spec/publisher/build.mjs from: +# spec/openapi.yaml (the consumption specification, version 0.4.0) +# spec/publisher/overlay.yaml (the publication operations) +# +# Regenerate with: node spec/publisher/build.mjs + openapi: 3.1.1 +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base info: title: Transparency Exchange API - summary: Transparency Exchange API specification for consumers and publishers - description: | - The Transparency Exchange API (TEA) provides standardized access to software transparency artifacts such as SBOMs, VEX, VDR, and other attestations. + summary: The OWASP Transparency Exchange API specification for consumers and publishers + description: |- + TBC + + This document is the consumption specification plus the publication + operations. Everything the two share — products, components, releases, + collections, artifacts, checksums, identifiers — has a single definition, + taken from the consumption specification, so a publisher and a consumer + cannot hold different ideas of the same object. + + Generated by spec/publisher/build.mjs. Do not edit by hand: edit + spec/publisher/overlay.yaml, or the consumption specification, and rebuild. contact: name: TEA Working Group + email: tbc@somewhere.tld url: https://github.com/CycloneDX/transparency-exchange-api license: name: Apache 2.0 url: https://github.com/CycloneDX/transparency-exchange-api/blob/main/LICENSE - version: 0.1.0-beta.1 + version: 0.4.0 servers: - - url: https://{host}/tea/v{version} - description: | - A TEA server. `host` is the name a consumer reaches after resolving the - domain component of a TEI and reading `/.well-known/tea`; `version` is one - of the versions that document advertises for the chosen endpoint, so the - publication and consumption APIs are addressed identically. - variables: - host: - default: localhost - description: Host serving the API. - version: - default: '1' - description: TEA API version, without a leading `v`. + - url: http://localhost/tea/v1 + description: Local development paths: /product/{uuid}: + get: + description: Get a TEA Product by UUID + operationId: getTeaProductByUuid + parameters: + - name: uuid + in: path + required: true + description: UUID of the TEA product in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: Requested TEA Product found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/product" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Product patch: summary: Update a TEA Product - description: Update an existing TEA Product entry + description: Update a product's mutable fields. Omitted fields are left unchanged. operationId: updateTeaProduct + parameters: + - $ref: "#/components/parameters/object-uuid" + requestBody: + $ref: "#/components/requestBodies/write-product" + responses: + "200": + $ref: "#/components/responses/product-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + delete: + summary: Delete a TEA Product + description: | + Delete a product and everything beneath it. + + Deletion does not retract what consumers already hold. A product that + has been public SHOULD also be withdrawn through a CLE `withdrawn` + event, so a consumer who acted on it learns that it was retracted + rather than simply finding it gone. + operationId: deleteTeaProduct + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /product/{uuid}/releases: + get: + description: Get releases of the product + operationId: getReleasesByProductId parameters: - name: uuid in: path required: true description: UUID of TEA Product in the TEA server schema: - type: string - format: uuid + $ref: "#/components/schemas/uuid" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product-release" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-product-release" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Product Release + /productRelease/{uuid}: + get: + description: Get a TEA Product Release + operationId: getTeaProductReleaseByUuid + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Product Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: Requested TEA Product Release found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/productRelease" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Product Release + patch: + summary: Update a TEA Product Release + operationId: updateTeaProductRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Update a product release's mutable fields. requestBody: - $ref: '#/components/requestBodies/UpdateTeaProduct' + $ref: "#/components/requestBodies/write-product-release" responses: - '200': - $ref: '#/components/responses/200-tea-product-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/product-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" tags: - - TEA Product + - TEA Publication delete: - summary: Delete a TEA Product - description: Delete a TEA Product entry - operationId: deleteTeaProduct + summary: Delete a TEA Product Release + operationId: deleteTeaProductRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Delete a product release and the collection it holds. + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /productRelease/{uuid}/cle: + get: + description: Get the CLE (Common Lifecycle Enumeration) data for a TEA Product Release + operationId: getCleByProductReleaseId + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Product Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: CLE data for the requested TEA Product Release found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/cle" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - CLE + /productReleases: + get: + description: Returns a list of TEA product releases. Note that multiple product releases may match. + operationId: queryTeaProductReleases + parameters: + - $ref: "#/components/parameters/id-type" + - $ref: "#/components/parameters/id-value" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product-release" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-product-release" + "400": + $ref: "#/components/responses/400-invalid-request" + tags: + - TEA Product Release + /product/{uuid}/cle: + get: + description: Get the CLE (Common Lifecycle Enumeration) data for a TEA Product + operationId: getCleByProductId parameters: - name: uuid in: path required: true description: UUID of TEA Product in the TEA server schema: - type: string - format: uuid + $ref: "#/components/schemas/uuid" responses: - '204': - $ref: '#/components/responses/204-object-deleted' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + description: CLE data for the requested TEA Product found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/cle" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" tags: - - TEA Product - /product: - post: - summary: Create a TEA Product - description: Create TEA Product entry for the supplied product identifier - operationId: createTeaProduct - requestBody: - $ref: '#/components/requestBodies/CreateTeaProduct' + - CLE + /products: + get: + description: Returns a list of TEA products. Note that multiple products may match. + operationId: queryTeaProducts + parameters: + - $ref: "#/components/parameters/id-type" + - $ref: "#/components/parameters/id-value" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product" + - $ref: "#/components/parameters/sort-order" responses: - '201': - $ref: '#/components/responses/201-tea-product-created' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/paginated-product" + "400": + $ref: "#/components/responses/400-invalid-request" tags: - TEA Product - /component: - post: - summary: Create a TEA Component - description: Create a new TEA Component entry - operationId: createTeaComponent - requestBody: - $ref: '#/components/requestBodies/CreateTeaComponent' + /component/{uuid}: + get: + description: Get a TEA Component + operationId: getTeaComponentById + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Component in the TEA server + schema: + $ref: "#/components/schemas/uuid" responses: - '201': - $ref: '#/components/responses/201-tea-component-created' - '400': - $ref: '#/components/responses/400-invalid-parameters' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + description: Requested TEA Component found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/component" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" tags: - TEA Component - /component/{uuid}: patch: summary: Update a TEA Component - description: Update an existing TEA Component entry + description: Update a component's mutable fields. Omitted fields are left unchanged. operationId: updateTeaComponent + parameters: + - $ref: "#/components/parameters/object-uuid" + requestBody: + $ref: "#/components/requestBodies/write-component" + responses: + "200": + $ref: "#/components/responses/component-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + delete: + summary: Delete a TEA Component + description: Delete a component and its releases. + operationId: deleteTeaComponent + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /component/{uuid}/releases: + get: + description: Get releases of the component + operationId: getReleasesByComponentId parameters: - name: uuid in: path required: true description: UUID of TEA Component in the TEA server schema: - type: string - format: uuid - requestBody: - $ref: '#/components/requestBodies/UpdateTeaComponent' + $ref: "#/components/schemas/uuid" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-component-release" + - $ref: "#/components/parameters/sort-order" responses: - '200': - $ref: '#/components/responses/200-tea-component-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/paginated-component-release" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" tags: - TEA Component - delete: - summary: Delete a TEA Component - description: Delete a TEA Component entry - operationId: deleteTeaComponent + /component/{uuid}/cle: + get: + description: Get the CLE (Common Lifecycle Enumeration) data for a TEA Component + operationId: getCleByComponentId parameters: - name: uuid in: path required: true description: UUID of TEA Component in the TEA server schema: - type: string - format: uuid + $ref: "#/components/schemas/uuid" + responses: + "200": + description: CLE data for the requested TEA Component found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/cle" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - CLE + /components: + get: + description: Returns a list of TEA components. Note that multiple components may match. + operationId: queryTeaComponents + parameters: + - $ref: "#/components/parameters/id-type" + - $ref: "#/components/parameters/id-value" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-component" + - $ref: "#/components/parameters/sort-order" responses: - '204': - $ref: '#/components/responses/204-object-deleted' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/paginated-component" + "400": + $ref: "#/components/responses/400-invalid-request" tags: - TEA Component - /release: - post: - summary: Create a TEA Release - description: Create a new TEA Release - operationId: createTeaRelease - requestBody: - $ref: '#/components/requestBodies/CreateTeaRelease' + /componentReleases: + get: + description: Returns a list of TEA component releases. Note that multiple component releases may match. + operationId: queryTeaComponentReleases + parameters: + - $ref: "#/components/parameters/id-type" + - $ref: "#/components/parameters/id-value" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-component" + - $ref: "#/components/parameters/sort-order" responses: - '201': - $ref: '#/components/responses/201-tea-release-created' - '400': - $ref: '#/components/responses/400-invalid-parameters' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/paginated-component-release" + "400": + $ref: "#/components/responses/400-invalid-request" tags: - - TEA Release - /release/{uuid}: - patch: - summary: Update a TEA Release - description: Update an existing TEA Release entry - operationId: updateTeaRelease + - TEA Component Release + /componentRelease/{uuid}: + get: + description: Get the TEA Component Release with its latest collection + operationId: getComponentReleaseById parameters: - name: uuid in: path required: true - description: UUID of TEA Release in the TEA server + description: UUID of TEA Component Release in the TEA server schema: - type: string - format: uuid + $ref: "#/components/schemas/uuid" + responses: + "200": + description: Requested TEA Component Release and its latest Collection found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/component-release-with-collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Component Release + patch: + summary: Update a TEA Component Release + operationId: updateTeaComponentRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Update a component release's mutable fields. requestBody: - $ref: '#/components/requestBodies/UpdateTeaRelease' + $ref: "#/components/requestBodies/write-component-release" responses: - '200': - $ref: '#/components/responses/200-tea-release-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/component-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" tags: - - TEA Release + - TEA Publication delete: - summary: Delete a TEA Release - description: Delete a TEA Release entry - operationId: deleteTeaRelease + summary: Delete a TEA Component Release + operationId: deleteTeaComponentRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Delete a component release and the collection it holds. + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /componentRelease/{uuid}/cle: + get: + description: Get the CLE (Common Lifecycle Enumeration) data for a TEA Component Release + operationId: getCleByComponentReleaseId + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Component Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: CLE data for the requested TEA Component Release found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/cle" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - CLE + /componentRelease/{uuid}/collection/latest: + get: + description: Get the latest TEA Collection belonging to the TEA Component Release + operationId: getLatestCollection parameters: - name: uuid in: path required: true - description: UUID of TEA Release in the TEA server + description: UUID of TEA Component Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: Requested TEA Collection found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Component Release + /productRelease/{uuid}/collection/latest: + get: + description: Get the latest TEA Collection belonging to the TEA Product Release + operationId: getLatestCollectionForProductRelease + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Product Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: Requested TEA Collection found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Product Release + /componentRelease/{uuid}/collections: + get: + description: Get the TEA Collections belonging to the TEA Component Release + operationId: getCollectionsByReleaseId + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Component Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-collection" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Component Release + /productRelease/{uuid}/collections: + get: + description: Get the TEA Collections belonging to the TEA Product Release + operationId: getCollectionsByProductReleaseId + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Product Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-collection" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Product Release + /productRelease/{uuid}/collection/{collectionVersion}: + get: + description: Get a specific Collection (by version) for a TEA Product Release by its UUID + operationId: getCollectionForProductRelease + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Product Release in the TEA server + schema: + $ref: "#/components/schemas/uuid" + - name: collectionVersion + in: path + required: true + description: Version of TEA Collection + schema: + type: integer + responses: + "200": + description: Requested TEA Collection Version found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Product Release + /componentRelease/{uuid}/collection/{collectionVersion}: + get: + description: Get a specific Collection (by version) for a TEA Component Release by its UUID + operationId: getCollection + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Collection in the TEA server + schema: + $ref: "#/components/schemas/uuid" + - name: collectionVersion + in: path + required: true + description: Version of TEA Collection + schema: + type: integer + responses: + "200": + description: Requested TEA Collection Version found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/collection" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Component Release + /artifact/{uuid}/latest: + get: + description: Get metadata for latest revision of a specific TEA Artifact + operationId: getLatestArtifact + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Artifact in the TEA server + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: Requested TEA Artifact metadata found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/artifact" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Artifact + /artifact/{uuid}/{artifactVersion}: + get: + description: Get metadata for a specific revision of a specific TEA Artifact + operationId: getArtifactByVersion + parameters: + - name: uuid + in: path + required: true + description: UUID of TEA Artifact in the TEA server + schema: + $ref: "#/components/schemas/uuid" + - name: artifactVersion + in: path + required: true + description: Version of TEA Artifact + schema: + type: integer + responses: + "200": + description: Requested TEA Artifact metadata found and returned + content: + application/json: + schema: + $ref: "#/components/schemas/artifact" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" + tags: + - TEA Artifact + /discovery: + get: + description: Discovery endpoint which resolves TEI into product release UUID. + operationId: discoveryByTei + parameters: + - name: tei + in: query + required: true + description: >- + Transparency Exchange Identifier (TEI) for the product being discovered. Provide the TEI + as a URL-encoded string per RFC 3986, RFC 3987. schema: type: string - format: uuid + example: urn%3Atei%3Auuid%3Aproducts.example.com%3Ad4d9f54a-abcf-11ee-ac79-1a52914d44b responses: - '204': - $ref: '#/components/responses/204-object-deleted' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/discovery-response" + "400": + $ref: "#/components/responses/400-invalid-request" + "404": + $ref: "#/components/responses/404-object-by-id-not-found" tags: - - TEA Release - /collection: + - TEA Discovery + /product: post: - summary: Create a TEA Collection - description: Create a new TEA Collection - operationId: createTeaCollection + summary: Create a TEA Product + description: | + Create a product and return the object the consumption API will serve, + including the UUID and identifiers the server assigned. + operationId: createTeaProduct parameters: - - $ref: '#/components/parameters/idempotencyKey' + - $ref: "#/components/parameters/idempotency-key" requestBody: - $ref: '#/components/requestBodies/CreateTeaCollection' + $ref: "#/components/requestBodies/write-product" responses: - '201': - $ref: '#/components/responses/201-tea-collection-created' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '409': - $ref: '#/components/responses/409-conflict' + "201": + $ref: "#/components/responses/product-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" tags: - - TEA Collection - /collection/{uuid}: - patch: - summary: Update a TEA Collection - description: | - Update a collection. - - A collection is the unit TEA versions, and its `updateReason` is how a - consumer distinguishes a correction from a re-issue. A server MUST - require an update reason on any change that alters the artifacts a - collection publishes, because a collection that changes without saying - why is indistinguishable from one that was tampered with. - operationId: updateTeaCollection + - TEA Publication + /component: + post: + summary: Create a TEA Component + operationId: createTeaComponent + description: Create a component and return the object the consumption API will serve. parameters: - - $ref: '#/components/parameters/objectUuid' + - $ref: "#/components/parameters/idempotency-key" requestBody: - $ref: '#/components/requestBodies/UpdateTeaCollection' + $ref: "#/components/requestBodies/write-component" responses: - '200': - $ref: '#/components/responses/200-tea-collection-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '409': - $ref: '#/components/responses/409-conflict' + "201": + $ref: "#/components/responses/component-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" tags: - - TEA Collection - delete: - summary: Delete a TEA Collection - description: Delete a collection and the artifact entries it holds. - operationId: deleteTeaCollection + - TEA Publication + /productRelease: + post: + summary: Create a TEA Product Release + operationId: createTeaProductRelease + description: Create a release of a product. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-product-release" + responses: + "201": + $ref: "#/components/responses/product-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /componentRelease: + post: + summary: Create a TEA Component Release + operationId: createTeaComponentRelease + description: Create a release of a component. parameters: - - $ref: '#/components/parameters/objectUuid' + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-component-release" responses: - '204': - $ref: '#/components/responses/204-object-deleted' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "201": + $ref: "#/components/responses/component-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" tags: - - TEA Collection + - TEA Publication + /productRelease/{uuid}/collection: + put: + summary: Publish a new collection version for a product release + operationId: publishTeaProductReleaseCollection + parameters: + - $ref: "#/components/parameters/object-uuid" + description: | + Publish the next version of this release's collection. + The server assigns the version, incrementing from the current one. + `updateReason` is required: a collection whose contents change without + saying why is indistinguishable, to a consumer, from one that was + tampered with. + requestBody: + $ref: "#/components/requestBodies/write-collection" + responses: + "200": + $ref: "#/components/responses/collection-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /componentRelease/{uuid}/collection: + put: + summary: Publish a new collection version for a component release + operationId: publishTeaComponentReleaseCollection + parameters: + - $ref: "#/components/parameters/object-uuid" + description: | + Publish the next version of this release's collection. The server + assigns the version, and `updateReason` is required. + requestBody: + $ref: "#/components/requestBodies/write-collection" + responses: + "200": + $ref: "#/components/responses/collection-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication /artifact: post: summary: Create a TEA Artifact + operationId: createTeaArtifact description: | - Create an artifact entry within a collection. + Register an artifact within a release's collection. - This registers the artifact and its formats; it does not carry the - bytes. Content is uploaded per format through - `/artifact/{uuid}/format/{formatIndex}/content`, or referenced by URL - when the publisher hosts it elsewhere. Separating the two matters - because an SBOM is routinely tens of megabytes, and a publisher that - must resend all of it to correct one field will eventually not correct - the field. - operationId: createTeaArtifact + This creates the entry and its formats; it does not carry the bytes. + Content goes up per format afterwards. Keeping the two apart matters + because an SBOM is routinely tens of megabytes, and a publisher who + must resend all of it to correct one metadata field will in practice + not correct the field. parameters: - - $ref: '#/components/parameters/idempotencyKey' + - $ref: "#/components/parameters/idempotency-key" requestBody: - $ref: '#/components/requestBodies/CreateTeaArtifact' + $ref: "#/components/requestBodies/write-artifact" responses: - '201': - $ref: '#/components/responses/201-tea-artifact-created' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '409': - $ref: '#/components/responses/409-conflict' + "201": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" tags: - - TEA Artifact + - TEA Publication /artifact/{uuid}: patch: summary: Update a TEA Artifact + operationId: updateTeaArtifact description: | Update an artifact's metadata. Stored bytes are immutable: a checksum a consumer has already recorded - must not start describing different content. Replacing content means - adding a new format or a new artifact version, never rewriting one in - place. - operationId: updateTeaArtifact + must not begin describing different content. Replacing content means a + new format or a new artifact version, never an overwrite in place. parameters: - - $ref: '#/components/parameters/objectUuid' + - $ref: "#/components/parameters/artifact-uuid" requestBody: - $ref: '#/components/requestBodies/UpdateTeaArtifact' + $ref: "#/components/requestBodies/update-artifact" responses: - '200': - $ref: '#/components/responses/200-tea-artifact-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '409': - $ref: '#/components/responses/409-conflict' + "200": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" tags: - - TEA Artifact + - TEA Publication delete: summary: Delete a TEA Artifact - description: Delete an artifact entry and any content this server holds for it. operationId: deleteTeaArtifact + description: Delete an artifact and any content this server holds for it. parameters: - - $ref: '#/components/parameters/objectUuid' + - $ref: "#/components/parameters/artifact-uuid" responses: - '204': - $ref: '#/components/responses/204-object-deleted' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" tags: - - TEA Artifact + - TEA Publication /artifact/{uuid}/format/{formatIndex}/content: put: summary: Upload artifact content + operationId: uploadTeaArtifactContent description: | Upload the bytes of one artifact format. - The body is the artifact itself, sent with its own media type rather - than wrapped in JSON or base64 — an SBOM is already a document, and - re-encoding it inflates it by a third for no benefit. + The body is the artifact itself in its own media type, rather than + base64 inside JSON, which would inflate it by a third for no benefit. - A client SHOULD send `Content-Digest`. Where it does, the server MUST - verify the digest against the received bytes and reject a mismatch with - `CHECKSUM_MISMATCH`. This is what makes an upload safe to retry: a - truncated transfer fails loudly instead of publishing a corrupt artifact - under a checksum that says otherwise. - operationId: uploadTeaArtifactContent + A client SHOULD send `Content-Digest` (RFC 9530). Where it does, the + server MUST verify it against the received bytes and reject a mismatch. + That is what makes an upload safe to retry: a truncated transfer fails + loudly instead of publishing corrupt content under a checksum that says + otherwise. parameters: - - $ref: '#/components/parameters/objectUuid' - - $ref: '#/components/parameters/formatIndex' - - $ref: '#/components/parameters/contentDigest' + - $ref: "#/components/parameters/artifact-uuid" + - $ref: "#/components/parameters/format-index" + - $ref: "#/components/parameters/content-digest" requestBody: required: true description: The artifact bytes, in the media type declared by the format. @@ -404,185 +998,1171 @@ paths: type: string format: binary responses: - '200': - $ref: '#/components/responses/200-tea-artifact-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '413': - $ref: '#/components/responses/413-payload-too-large' - '415': - $ref: '#/components/responses/415-unsupported-media-type' + "200": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "413": + $ref: "#/components/responses/publisher-payload-too-large" + "415": + $ref: "#/components/responses/publisher-unsupported-media-type" tags: - - TEA Artifact + - TEA Publication /artifact/{uuid}/format/{formatIndex}/signature: put: - summary: Attach a detached signature + summary: Attach a detached signature to an artifact format + operationId: uploadTeaArtifactSignature description: | - Attach a detached signature to one artifact format, declaring the scheme - it was produced with. + Attach a detached signature, declaring the scheme it was produced with. - The consumption API exposes a `signatureUrl` and nothing else, so a - consumer is left to infer the scheme from the bytes. Recording it here - is what turns a signature from something that exists into something that - can be checked. - operationId: uploadTeaArtifactSignature + The consumption API exposes a `signatureUrl` and nothing more, so a + consumer must infer the scheme from the bytes before it can verify + anything. The publisher knows, and recording it is what turns a + signature from something that exists into something that can be + checked. See signatures/signature.md. parameters: - - $ref: '#/components/parameters/objectUuid' - - $ref: '#/components/parameters/formatIndex' + - $ref: "#/components/parameters/artifact-uuid" + - $ref: "#/components/parameters/format-index" requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/artifactSignature' - application/octet-stream: - schema: - type: string - format: binary - description: | - The raw detached signature. The scheme MUST then be supplied in - the `X-TEA-Signature-Type` header, since bytes alone do not say - how to verify them. + $ref: "#/components/schemas/artifact-signature" responses: - '200': - $ref: '#/components/responses/200-tea-artifact-updated' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '415': - $ref: '#/components/responses/415-unsupported-media-type' + "200": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "415": + $ref: "#/components/responses/publisher-unsupported-media-type" tags: - - TEA Artifact - - /distribution/{uuid}: + - TEA Publication + /accessPolicy/{uuid}: get: - summary: Read effective distribution + summary: Read the access policy in force for an object + operationId: getTeaAccessPolicy description: | - The distribution actually in force for an object, after inheritance. + The access policy actually in force, after inheritance. - A publisher needs to be able to answer "who can see this?" without - replaying the object tree in their head. The response reports both what - the object declares and what it inherits, because the difference between - the two is exactly where accidental disclosure hides. - operationId: getTeaDistribution + Reported as both what the object declares and what it effectively has, + because the difference between the two is exactly where accidental + disclosure hides. parameters: - - $ref: '#/components/parameters/objectUuid' + - $ref: "#/components/parameters/object-uuid" responses: - '200': - $ref: '#/components/responses/200-tea-distribution' - '401': - $ref: '#/components/responses/401-unauthorized' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' + "200": + $ref: "#/components/responses/access-policy-read" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" tags: - - TEA Distribution + - TEA Access Policy put: - summary: Set distribution + summary: Set the access policy for an object + operationId: setTeaAccessPolicy description: | - Set an object's own distribution, replacing any it previously declared. + Replace the object's own access policy. - A server MUST reject a distribution that is wider than the effective - distribution of the object's parent, with `DISTRIBUTION_WIDENS_PARENT`. - A server SHOULD require a distinct confirmation for a change to - `public`, because that transition cannot be undone by any later request: - whatever was fetched while it was public stays fetched. - operationId: setTeaDistribution + A server MUST reject a policy wider than the effective policy of the + object's parent, with `ACCESS_WIDENS_PARENT`. A server SHOULD require + a separate confirmation for a change to `public`, because no later + request undoes it: whatever was fetched while it was public stays + fetched. parameters: - - $ref: '#/components/parameters/objectUuid' + - $ref: "#/components/parameters/object-uuid" requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/distribution' + $ref: "#/components/schemas/access-policy" responses: - '200': - $ref: '#/components/responses/200-tea-distribution' - '400': - $ref: '#/components/responses/400-invalid-request-body' - '401': - $ref: '#/components/responses/401-unauthorized' - '403': - $ref: '#/components/responses/403-forbidden' - '404': - $ref: '#/components/responses/404-object-by-id-not-found' - '409': - $ref: '#/components/responses/409-conflict' + "200": + $ref: "#/components/responses/access-policy-read" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" tags: - - TEA Distribution + - TEA Access Policy components: schemas: - error: + date-time: + type: string + description: Timestamp + format: date-time + pattern: ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$ + example: "2024-03-20T15:30:00Z" + identifier: type: object - description: | - A machine-readable error. Publication is an automated activity — a - release pipeline is usually the caller — so a failure has to be - actionable without a human reading prose. + description: An identifier with a specified type properties: - code: - type: string - description: Stable, machine-readable reason. Clients branch on this, never on `message`. - enum: - - INVALID_REQUEST - - UNAUTHORIZED - - FORBIDDEN - - OBJECT_NOT_FOUND - - DUPLICATE_OBJECT - - VERSION_CONFLICT - - DISTRIBUTION_WIDENS_PARENT - - UNSUPPORTED_MEDIA_TYPE - - PAYLOAD_TOO_LARGE - - CHECKSUM_MISMATCH - - SIGNATURE_INVALID + idType: + description: Type of identifier, e.g. `TEI`, `PURL`, `CPE` + $ref: "#/components/schemas/identifier-type" + idValue: + description: Identifier value + type: string + identifier-type: + type: string + description: Enumeration of identifiers types + enum: + - CPE + - TEI + - PURL + - COMPLIANCE_DOCUMENT + compliance-document-type: + type: string + description: > + Well-known compliance document types. When idType is COMPLIANCE_DOCUMENT, the idValue SHOULD + be one of these values. + enum: + - SOC_2_TYPE_I + - SOC_2_TYPE_II + - SOC_3 + - ISO_27001 + - ISO_27017 + - ISO_27018 + - ISO_27701 + - ISO_42001 + - PCI_DSS + - HIPAA + - FedRAMP + - GDPR + - CSA_STAR + - NIST_800_53 + - NIST_800_171 + - CMMC + - HITRUST + - TISAX + - CYBER_ESSENTIALS + - CYBER_ESSENTIALS_PLUS + uuid: + type: string + description: A UUID + format: uuid + pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + product: + type: object + description: A TEA product + properties: + uuid: + description: A unique identifier for the TEA product + $ref: "#/components/schemas/uuid" + name: + type: string + description: Product name + identifiers: + type: array + description: | + List of identifiers for the product, like TEI, CPE, PURL or other identifiers + items: + $ref: "#/components/schemas/identifier" + required: + - uuid + - name + - identifiers + examples: + - uuid: 09e8c73b-ac45-4475-acac-33e6a7314e6d + name: Apache Log4j 2 + identifiers: + - idType: CPE + idValue: cpe:2.3:a:apache:log4j + - idType: PURL + idValue: pkg:maven/org.apache.logging.log4j/log4j-api + productRelease: + type: object + description: A specific release of a TEA product + properties: + uuid: + description: A unique identifier for the TEA Product Release + $ref: "#/components/schemas/uuid" + product: + description: UUID of the TEA Product this release belongs to + $ref: "#/components/schemas/uuid" + productName: + description: Name of the TEA Product this release belongs to + type: string + example: Apache Log4j 2 + version: + description: Version number of the product release + type: string + example: 2.24.3 + createdDate: + description: Timestamp when this Product Release was created in TEA (for sorting purposes) + $ref: "#/components/schemas/date-time" + releaseDate: + description: Timestamp of the product release + $ref: "#/components/schemas/date-time" + preRelease: + type: boolean + description: > + A flag indicating pre-release (or beta) status. + + May be disabled after the creation of the release object, but can't be enabled after + creation of an object. + identifiers: + type: array + description: List of identifiers for the product release + items: + $ref: "#/components/schemas/identifier" + components: + type: array + description: > + List of component references that compose this product release. A component reference + can optionally include + + the UUID of a specific component release to pin the exact version. + items: + $ref: "#/components/schemas/component-ref" + required: + - uuid + - version + - createdDate + - components + examples: + - uuid: 123e4567-e89b-12d3-a456-426614174000 + version: 2.24.3 + createdDate: 2025-04-01T15:43:00.000Z + releaseDate: 2025-04-01T15:43:00.000Z + identifiers: + - idType: TEI + idValue: tei:vendor:product@2.24.3 + components: + - uuid: 3910e0fd-aff4-48d6-b75f-8bf6b84687f0 + - uuid: b844c9bd-55d6-478c-af59-954a932b6ad3 + release: da89e38e-95e7-44ca-aa7d-f3b6b34c7fab + component: + type: object + description: A TEA component + properties: + uuid: + description: A unique identifier for the TEA component + $ref: "#/components/schemas/uuid" + name: + type: string + description: Component name + identifiers: + type: array + description: List of identifiers for the component + items: + $ref: "#/components/schemas/identifier" + required: + - uuid + - name + - identifiers + examples: + - uuid: 3910e0fd-aff4-48d6-b75f-8bf6b84687f0 + name: Apache Log4j API + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.logging.log4j/log4j-api + - uuid: b844c9bd-55d6-478c-af59-954a932b6ad3 + name: Apache Log4j Core + identifiers: + - idType: CPE + idValue: cpe:2.3:a:apache:log4j + - idType: PURL + idValue: pkg:maven/org.apache.logging.log4j/log4j-core + component-ref: + type: object + description: A reference to a TEA component or specific component release + properties: + uuid: + description: A unique identifier for the TEA component + $ref: "#/components/schemas/uuid" + release: + description: > + Optional UUID of a specific release included in the product in the case where the + product + + always include a specific release of a component. The product name should include a + version + + identifier in this case. + $ref: "#/components/schemas/uuid" + required: + - uuid + release: + type: object + description: A TEA Component Release + properties: + uuid: + description: A unique identifier for the TEA Component Release + $ref: "#/components/schemas/uuid" + component: + description: UUID of the TEA Component this release belongs to + $ref: "#/components/schemas/uuid" + componentName: + description: Name of the TEA Component this release belongs to + type: string + example: tomcat + version: + description: Version number + type: string + example: 1.2.3 + createdDate: + description: Timestamp when this Release was created in TEA (for sorting purposes) + $ref: "#/components/schemas/date-time" + releaseDate: + description: Timestamp of the release + $ref: "#/components/schemas/date-time" + preRelease: + type: boolean + description: > + A flag indicating pre-release (or beta) status. + + May be disabled after the creation of the release object, but can't be enabled after + creation of an object. + identifiers: + type: array + description: List of identifiers for the component + items: + $ref: "#/components/schemas/identifier" + distributions: + type: array + description: List of different formats of this component release + items: + $ref: "#/components/schemas/release-distribution" + required: + - uuid + - version + - createdDate + examples: + - uuid: 605d0ecb-1057-40e4-9abf-c400b10f0345 + version: 11.0.7 + createdDate: 2025-05-07T18:08:00.000Z + releaseDate: 2025-05-12T18:08:00.000Z + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.7 + distributions: + - distributionId: 6a0d58e1-4896-4f1e-83f7-5feb6c032537 + description: Core binary distribution, zip archive + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.6?type=zip + checksums: + - algType: SHA_256 + algValue: 9da736a1cdd27231e70187cbc67398d29ca0b714f885e7032da9f1fb247693c1 + url: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.zip + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.zip.asc + - distributionId: dba01c13-dd96-4928-be72-9c87ffa8cab8 + description: Core binary distribution, tar.gz archive + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.6?type=tar.gz + checksums: + - algType: SHA_256 + algValue: 2fcece641c62ba1f28e1d7b257493151fc44f161fb391015ee6a95fa71632fb9 + url: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.tar.gz + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.tar.gz.asc + - distributionId: cfe068c2-fae7-43d0-97ec-5ea092454040 + description: Core binary distribution, Windows x64 zip archive + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.6?classifier=windows-x64&type=zip + checksums: + - algType: SHA_256 + algValue: 62a5c358d87a8ef21d7ec1b3b63c9bbb577453dda9c00cbb522b16cee6c23fc4 + url: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6-windows-x64.zip + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.zip.asc + - distributionId: de45ffaf-e4b5-47b5-be28-444a76df098e + description: Core binary distribution, Windows Service Installer (MSI) + checksums: + - algType: SHA_512 + algValue: >- + 1d3824e7643c8aba455ab0bd9e67b14a60f2aaa6aa7775116bce40eb0579e8ced162a4f828051d3b867e96ee2858ec5da0cc654e83a83ba30823cbea0df4ff96 + url: https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.7/bin/apache-tomcat-11.0.7.exe + signatureUrl: >- + https://downloads.apache.org/tomcat/tomcat-11/v11.0.7/bin/apache-tomcat-11.0.7.exe.asc + - uuid: 95f481df-f760-47f4-b2f2-f8b76d858450 + version: 11.0.0-M26 + createdDate: 2024-09-13T17:49:00.000Z + preRelease: true + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.0-M26 + release-distribution: + type: object + properties: + distributionId: + description: A unique identifier for the TEA Distribution object + $ref: "#/components/schemas/uuid" + description: + type: string + description: Free-text description of the distribution. + identifiers: + type: array + description: List of identifiers specific to this distribution. + items: + $ref: "#/components/schemas/identifier" + url: + type: string + description: Direct download URL for the distribution. + format: url + signatureUrl: + type: string + description: Direct download URL for the distribution's external signature. + format: url + checksums: + type: array + description: List of checksums for the distribution. + items: + $ref: "#/components/schemas/checksum" + required: + - distributionId + examples: + - distributionId: 6a0d58e1-4896-4f1e-83f7-5feb6c032537 + description: Core binary distribution, zip archive + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.6?type=zip + checksums: + - algType: SHA_256 + algValue: 9da736a1cdd27231e70187cbc67398d29ca0b714f885e7032da9f1fb247693c1 + url: https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.zip + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.zip.asc + - distributionId: dba01c13-dd96-4928-be72-9c87ffa8cab8 + description: Core binary distribution, tar.gz archive + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.6?type=tar.gz + checksums: + - algType: SHA_256 + algValue: 2fcece641c62ba1f28e1d7b257493151fc44f161fb391015ee6a95fa71632fb9 + url: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.tar.gz + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.tar.gz.asc + - distributionId: cfe068c2-fae7-43d0-97ec-5ea092454040 + description: Core binary distribution, Windows x64 zip archive + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.6?classifier=windows-x64&type=zip + checksums: + - algType: SHA_256 + algValue: 62a5c358d87a8ef21d7ec1b3b63c9bbb577453dda9c00cbb522b16cee6c23fc4 + url: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6-windows-x64.zip + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.6.zip.asc + - distributionId: de45ffaf-e4b5-47b5-be28-444a76df098e + description: Core binary distribution, Windows Service Installer (MSI) + checksums: + - algType: SHA_512 + algValue: >- + 1d3824e7643c8aba455ab0bd9e67b14a60f2aaa6aa7775116bce40eb0579e8ced162a4f828051d3b867e96ee2858ec5da0cc654e83a83ba30823cbea0df4ff96 + url: https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.7/bin/apache-tomcat-11.0.7.exe + signatureUrl: https://downloads.apache.org/tomcat/tomcat-11/v11.0.7/bin/apache-tomcat-11.0.7.exe.asc + component-release-with-collection: + type: object + description: A TEA Component Release combined with its latest collection + properties: + release: + description: The TEA Component Release information + $ref: "#/components/schemas/release" + latestCollection: + description: The latest TEA Collection for this component release + $ref: "#/components/schemas/collection" + required: + - release + - latestCollection + examples: + - release: + uuid: 605d0ecb-1057-40e4-9abf-c400b10f0345 + version: 11.0.7 + createdDate: 2025-05-07T18:08:00.000Z + releaseDate: 2025-05-12T18:08:00.000Z + identifiers: + - idType: PURL + idValue: pkg:maven/org.apache.tomcat/tomcat@11.0.7 + latestCollection: + uuid: 605d0ecb-1057-40e4-9abf-c400b10f0345 + version: 2 + date: 2025-05-12T18:08:00.000Z + belongsTo: COMPONENT_RELEASE + updateReason: + type: INITIAL_RELEASE + comment: Initial collection for this release + artifacts: + - uuid: 1cb47b95-8bf8-3bad-a5a4-0d54d86e10ce + name: Build SBOM + type: BOM + formats: + - mediaType: application/vnd.cyclonedx+xml + description: CycloneDX SBOM (XML) + url: >- + https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat/11.0.7/tomcat-11.0.7-cyclonedx.xml + checksums: + - algType: SHA-256 + algValue: 9da736a1cdd27231e70187cbc67398d29ca0b714f885e7032da9f1fb247693c1 + - uuid: dfa35519-9734-4259-bba1-3e825cf4be06 + name: Vulnerability Disclosure Report + type: VULNERABILITIES + formats: + - mediaType: application/vnd.cyclonedx+xml + description: CycloneDX VDR (XML) + url: https://tomcat.apache.org/cyclonedx/vdr.xml + checksums: + - algType: SHA-256 + algValue: 75b81020b3917cb682b1a7605ade431e062f7a4c01a412f0b87543b6e995ad2a + collection: + type: object + description: A collection of security-related documents + properties: + uuid: + description: > + UUID of the TEA Collection object. + + This matches the UUID of the associated TEA Component Release or TEA Product Release + object. + + When updating a collection, only the `version` is changed. + $ref: "#/components/schemas/uuid" + version: + type: integer + description: | + TEA Collection version, incremented each time its content changes. + Versions start with 1. + date: + description: The date when the TEA Collection version was created. + $ref: "#/components/schemas/date-time" + belongsTo: + description: Indicates whether this collection belongs to a Component Release or a Product Release + $ref: "#/components/schemas/collection-belongs-to-type" + updateReason: + description: Reason for the update/release of the TEA Collection object. + $ref: "#/components/schemas/collection-update-reason" + artifacts: + type: array + description: List of TEA Artifact objects. + items: + $ref: "#/components/schemas/artifact" + examples: + - uuid: 4c72fe22-9d83-4c2f-8eba-d6db484f32c8 + version: 10 + date: 2024-12-13T00:00:00.000Z + updateReason: + type: ARTIFACT_UPDATED + comment: VDR file updated + artifacts: + - uuid: 1cb47b95-8bf8-3bad-a5a4-0d54d86e10ce + name: Build SBOM + type: BOM + formats: + - mediaType: application/vnd.cyclonedx+xml + description: CycloneDX SBOM (XML) + url: >- + https://repo.maven.apache.org/maven2/org/apache/logging/log4j/log4j-core/2.24.3/log4j-core-2.24.3-cyclonedx.xml + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/logging/log4j/log4j-core/2.24.3/log4j-core-2.24.3-cyclonedx.xml.asc + checksums: + - algType: MD5 + algValue: 2e1a525afc81b0a8ecff114b8b743de9 + - algType: SHA-1 + algValue: 5a7d4caef63c5c5ccdf07c39337323529eb5a770 + - uuid: dfa35519-9734-4259-bba1-3e825cf4be06 + version: 7 + name: Vulnerability Disclosure Report + type: VULNERABILITIES + formats: + - mediaType: application/vnd.cyclonedx+xml + description: CycloneDX VDR (XML) + url: https://logging.apache.org/cyclonedx/vdr.xml + checksums: + - algType: SHA-256 + algValue: 75b81020b3917cb682b1a7605ade431e062f7a4c01a412f0b87543b6e995ad2a + collection-update-reason: + type: object + description: Reason for the update to the TEA collection + properties: + type: + description: Type of update reason. + $ref: "#/components/schemas/collection-update-reason-type" + comment: + type: string + description: Free text description + collection-update-reason-type: + type: string + description: Type of TEA collection update + enum: + - INITIAL_RELEASE + - VEX_UPDATED + - ARTIFACT_UPDATED + - ARTIFACT_ADDED + - ARTIFACT_REMOVED + collection-belongs-to-type: + type: string + description: Indicates whether a collection belongs to a component release or a product release + enum: + - COMPONENT_RELEASE + - PRODUCT_RELEASE + artifact: + type: object + description: A security-related document + properties: + uuid: + description: >- + The UUID of the TEA Artifact object. Together with *version* uniquely identifies the TEA + Artifact. + $ref: "#/components/schemas/uuid" + version: + description: > + An integer with default value 1. + + Together with *uuid* uniquely identifies the TEA Artifact. + + This field can be used to designate successive, immutable revisions of an artefact + content (e.g. an updated VEX file). + type: integer + default: 1 + name: + type: string + description: Name of TEA Artifact + type: + description: Type of TEA Artifact + $ref: "#/components/schemas/artifact-type" + createdDate: + description: The date when the TEA Artifact revision was created. + $ref: "#/components/schemas/date-time" + distributionIds: + type: array + description: | + List of TEA Component Release distributions that this TEA Artifact applies to. + If absent or empty, the TEA Artifact applies to all distributions. + items: + $ref: "#/components/schemas/uuid" + description: > + The distribution IDs of the TEA component release distributions that this TEA Artifact + applies to. + formats: + type: array + description: | + List of objects with the same content, but in different formats. + The order of the list has no significance. + items: + $ref: "#/components/schemas/artifact-format" + required: + - uuid + - type + - formats + examples: + - uuid: 1cb47b95-8bf8-3bad-a5a4-0d54d86e10ce + name: Build SBOM + type: BOM + formats: + - mediaType: application/vnd.cyclonedx+xml + description: CycloneDX SBOM (XML) + url: >- + https://repo.maven.apache.org/maven2/org/apache/logging/log4j/log4j-core/2.24.3/log4j-core-2.24.3-cyclonedx.xml + signatureUrl: >- + https://repo.maven.apache.org/maven2/org/apache/logging/log4j/log4j-core/2.24.3/log4j-core-2.24.3-cyclonedx.xml.asc + checksums: + - algType: MD5 + algValue: 2e1a525afc81b0a8ecff114b8b743de9 + - algType: SHA-1 + algValue: 5a7d4caef63c5c5ccdf07c39337323529eb5a770 + - uuid: dfa35519-9734-4259-bba1-3e825cf4be06 + version: 7 + name: Vulnerability Disclosure Report + type: VULNERABILITIES + formats: + - mediaType: application/vnd.cyclonedx+xml + description: CycloneDX VDR (XML) + url: https://logging.apache.org/cyclonedx/vdr.xml + checksums: + - algType: SHA-256 + algValue: 75b81020b3917cb682b1a7605ade431e062f7a4c01a412f0b87543b6e995ad2a + artifact-type: + type: string + description: Specifies the type of external reference. + enum: + - ATTESTATION + - BOM + - BUILD_META + - CERTIFICATION + - FORMULATION + - LICENSE + - RELEASE_NOTES + - SECURITY_TXT + - THREAT_MODEL + - VULNERABILITIES + - OTHER + artifact-format: + type: object + description: A security-related document in a specific format + properties: + mediaType: + type: string + description: The Media Type of the document + description: + type: string + description: A free text describing the TEA Artifact + url: + type: string + description: Direct download URL for the TEA Artifact + format: url + signatureUrl: + type: string + description: Direct download URL for an external signature of the TEA Artifact + format: url + checksums: + type: array + description: List of checksums for the TEA Artifact + items: + $ref: "#/components/schemas/checksum" + checksum: + type: object + properties: + algType: + description: Checksum algorithm + $ref: "#/components/schemas/checksum-type" + algValue: + type: string + description: Checksum value + required: + - algType + - algValue + checksum-type: + type: string + description: Checksum algorithm + enum: + - MD5 + - SHA-1 + - SHA-256 + - SHA-384 + - SHA-512 + - SHA3-256 + - SHA3-384 + - SHA3-512 + - BLAKE2b-256 + - BLAKE2b-384 + - BLAKE2b-512 + - BLAKE3 + unknown-error-type: + type: string + description: Classification of TEA error response + enum: + - OBJECT_UNKNOWN + - OBJECT_NOT_SHAREABLE + error-response: + type: object + description: Error response + additionalProperties: false + properties: + error: + $ref: "#/components/schemas/unknown-error-type" + required: + - error + tea-server-info: + type: object + description: TEA server information including URL, versions, and optional priority + additionalProperties: false + properties: + rootUrl: + description: Root URL of the TEA server for this TEI without trailing slash + type: string + format: uri + example: https://api.teaexample.com + versions: + description: Supported TEA API versions at this server without v prefix + type: array + minItems: 1 + items: + type: string + example: + - 0.2.0-beta.2 + - 1.0.0 + priority: + description: Optional priority for this server (0.0 to 1.0, where 1.0 is highest priority) + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.8 + required: + - rootUrl + - versions + discovery-info: + type: object + description: Discovery information for a TEI + additionalProperties: false + properties: + productReleaseUuid: + description: UUID of the resolved TEA Product Release + $ref: "#/components/schemas/uuid" + example: d4d9f54a-abcf-11ee-ac79-1a52914d44b + servers: + description: Array of TEA server information + type: array + minItems: 1 + items: + $ref: "#/components/schemas/tea-server-info" + required: + - productReleaseUuid + - servers + cle-event-type: + type: string + description: The type of CLE lifecycle event + enum: + - released + - endOfDevelopment + - endOfSupport + - endOfLife + - endOfDistribution + - endOfMarketing + - supersededBy + - componentRenamed + - withdrawn + cle-version-specifier: + type: object + description: A version specifier that can be either a single version or a version range + properties: + version: + type: string + description: A specific version string + range: + type: string + description: A version range in vers format (e.g. "vers:npm/>=1.0.0|<2.0.0") + cle-event: + type: object + description: A discrete lifecycle event from the CLE specification + properties: + id: + type: integer + description: A unique, auto-incrementing integer identifier for the event + type: + description: The type of lifecycle event + $ref: "#/components/schemas/cle-event-type" + effective: + type: string + format: date-time + description: ISO 8601 timestamp (UTC) when the event takes effect + published: + type: string + format: date-time + description: ISO 8601 timestamp (UTC) when the event was first published + version: + type: string + description: Version string (used by released event type) + versions: + type: array + description: List of version specifiers affected by this event + items: + $ref: "#/components/schemas/cle-version-specifier" + supportId: + type: string + description: Reference to a support policy ID defined in the definitions section + license: + type: string + description: License identifier (used by released event type) + supersededByVersion: + type: string + description: Version string that supersedes the affected versions (used by supersededBy event type) + identifiers: + type: array + description: New identifiers for the component (used by componentRenamed event type) + items: + $ref: "#/components/schemas/identifier" + eventId: + type: integer + description: ID of the event being withdrawn (used by withdrawn event type) + reason: + type: string + description: Human-readable explanation (used by withdrawn event type) + description: + type: string + description: Human-readable description of the event + references: + type: array + description: List of URLs to supporting documentation + items: + type: string + format: uri + required: + - id + - type + - effective + - published + examples: + - id: 1 + type: released + effective: "2024-01-01T00:00:00Z" + version: 1.0.0 + license: MIT + published: "2023-06-01T00:00:00Z" + - id: 3 + type: endOfSupport + effective: "2024-01-01T00:00:00Z" + versions: + - version: 1.0.0 + supportId: standard + published: "2023-06-01T00:00:00Z" + cle-support-definition: + type: object + description: A support policy definition from CLE + properties: + id: + type: string + description: Unique identifier for the support policy + description: + type: string + description: Human-readable description of the policy + url: + type: string + format: uri + description: URL to detailed documentation about this support policy + required: + - id + - description + examples: + - id: standard + description: Standard product support policy + url: https://example.com/support/standard + cle-definitions: + type: object + description: Container for reusable CLE policy definitions + properties: + support: + type: array + description: List of support policies + items: + $ref: "#/components/schemas/cle-support-definition" + cle: + type: object + description: > + Common Lifecycle Enumeration (CLE) object based on ECMA-428 TC54 TG3 CLE Specification + v1.0.0. + + Contains lifecycle events and optional reusable definitions for a component or product. + properties: + events: + type: array + description: | + Ordered array of CLE Event objects representing lifecycle events. + MUST be ordered by ID in descending order (newest events with highest IDs first). + items: + $ref: "#/components/schemas/cle-event" + definitions: + description: Container for reusable policy definitions referenced by events + $ref: "#/components/schemas/cle-definitions" + required: + - events + examples: + - events: + - id: 3 + type: endOfSupport + effective: "2025-06-01T00:00:00Z" + versions: + - range: vers:npm/>=1.0.0|<2.0.0 + supportId: standard + published: "2025-01-01T00:00:00Z" + - id: 2 + type: endOfDevelopment + effective: "2025-01-01T00:00:00Z" + versions: + - version: 1.0.0 + supportId: standard + published: "2024-06-01T00:00:00Z" + - id: 1 + type: released + effective: "2024-01-01T00:00:00Z" + version: 1.0.0 + license: Apache-2.0 + published: "2024-01-01T00:00:00Z" + definitions: + support: + - id: standard + description: Standard product support policy + url: https://example.com/support/standard + pagination-details: + type: object + properties: + hasNext: + type: boolean + description: | + A flag (to aid clients) to know whether there is a next page of results to fetch. + + `nextPageToken` will always be supplied, hence this hint is included to aid clients. + default: false + nextPageToken: + type: string + nullable: false + description: | + A token that can be used in a following request to retrieve the next page or results. + + It must always be supplied in responses. + required: + - hasNext + - nextPageToken + paginated-product-response: + description: A paginated response containing TEA Products + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/product" + required: + - results + paginated-product-release-response: + description: A paginated response containing TEA Product Releases + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/productRelease" + required: + - results + paginated-component-response: + description: A paginated response containing TEA Components + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/component" + required: + - results + paginated-component-release-response: + description: A paginated response containing TEA Component Releases + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/release" + required: + - results + paginated-collection-response: + description: A paginated response containing TEA Collections + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/collection" + required: + - results + publisher-error-type: + type: string + description: | + Classification of a publication error. + + Extends the consumption specification's `unknown-error-type` with the + failures only a writer can hit. The two consumption values are repeated + so a client can branch on one enumeration rather than two. + enum: + - OBJECT_UNKNOWN + - OBJECT_NOT_SHAREABLE + - INVALID_REQUEST + - UNAUTHORIZED + - FORBIDDEN + - DUPLICATE_OBJECT + - VERSION_CONFLICT + - ACCESS_WIDENS_PARENT + - UNSUPPORTED_MEDIA_TYPE + - PAYLOAD_TOO_LARGE + - CHECKSUM_MISMATCH + - SIGNATURE_INVALID + publisher-error-response: + type: object + description: | + A publication error. Same envelope as the consumption specification's + `error-response`, over the wider publication enumeration, with an + optional human-readable message and a pointer to the offending field. + + Publication is automated — a release pipeline is usually the caller — + so a failure has to be actionable without a human reading prose. + additionalProperties: false + properties: + error: + $ref: "#/components/schemas/publisher-error-type" message: type: string - description: Human-readable explanation. Not a stable interface. + description: Human-readable explanation. Not a stable interface; branch on `error`. field: type: string description: JSON Pointer to the offending member of the request body, where one applies. required: - - code - - message - - distribution: + - error + access-policy: type: object description: | Who may read an object through the consumption API. - TEA describes how transparency data is *fetched*, not who is entitled to - fetch it, which leaves every publisher to invent an answer. A publisher - API cannot avoid the question: the same server holds material that is - deliberately public, material shared with named counterparties under an - agreement, and material that is purely internal. + TEA describes how transparency data is fetched, not who is entitled to + fetch it. That is a reasonable boundary for a consumption API and an + impossible one for a publication API, where the same server holds + material that is deliberately public, material shared with named + counterparties under agreement, and material that is purely internal. + Absent this, every publisher invents an answer, incompatibly. + + Resolved by inheritance: an artifact takes its collection's policy, a + collection its release's, a release its product's. An object MAY narrow + what it inherits and MUST NOT widen it — a server rejects the attempt + with `ACCESS_WIDENS_PARENT`. Without that rule, marking a product + private would guarantee nothing, because any artifact beneath it could + quietly be made public. - Distribution is resolved by inheritance. An object with no distribution - of its own takes its parent's — artifact from collection, collection - from release, release from product. An object MAY narrow what it - inherits and MUST NOT widen it: a server MUST reject with - `DISTRIBUTION_WIDENS_PARENT` any request that would make a child more - visible than its parent. Without that rule, publishing a product - privately would be no guarantee at all, because any artifact beneath it - could quietly be made public. + A consumer denied by this policy is answered with the consumption + specification's existing `OBJECT_NOT_SHAREABLE`, so enforcement needs + no new consumer-side behaviour. + additionalProperties: false properties: visibility: type: string description: | `private` — only the publishing organisation may read it. - `shared` — readable by the publishing organisation and by the + `shared` — readable by the publishing organisation and the organisations named in `sharedWith`. Authentication is required and the reader's identity decides the answer. - `public` — readable without authentication. This is the only value - that makes an object part of the open transparency web, and it is - irreversible in practice: what has been fetched cannot be recalled. + `public` — readable without authentication. The only value that + makes an object part of the open transparency web, and irreversible + in practice: what has been fetched cannot be recalled. enum: - private - shared @@ -592,54 +2172,49 @@ components: type: array description: | Organisations entitled to read this object when `visibility` is - `shared`. Identified by the consuming organisation's UUID on this - server. MUST be empty for any other visibility, so that a later - change to `private` cannot silently leave a grant in place. + `shared`. MUST be empty for any other visibility, so that a later + change to `private` cannot silently leave a grant standing. items: - $ref: '#/components/schemas/shareGrant' + $ref: "#/components/schemas/share-grant" publishTo: type: array description: | Remote TEA servers this object is mirrored to. Mirroring is a copy, not a reference: once an object reaches another server, this - server's distribution no longer governs it. + server's policy no longer governs it. items: - $ref: '#/components/schemas/publicationTarget' + $ref: "#/components/schemas/publication-target" required: - visibility - - shareGrant: + share-grant: type: object description: An entitlement for one organisation to read one object. + additionalProperties: false properties: organizationUuid: - type: string - format: uuid - description: The consuming organisation's UUID on this server. + description: The consuming organisation's identifier on this server. + $ref: "#/components/schemas/uuid" organizationName: type: string - description: Display name at the time of granting. Advisory; the UUID is the identity. + description: Display name when the grant was made. Advisory; the UUID is the identity. grantedAt: - type: string - format: date-time + $ref: "#/components/schemas/date-time" expiresAt: - type: string - format: date-time description: | - When the grant lapses. A grant for the duration of an evaluation or - a contract is the common case, and a server that cannot express an + When the grant lapses. A grant for the length of an evaluation or a + contract is the common case, and a server that cannot express an expiry pushes publishers towards grants nobody ever revokes. + $ref: "#/components/schemas/date-time" required: - organizationUuid - - publicationTarget: + publication-target: type: object description: A remote TEA server this publisher mirrors objects to. + additionalProperties: false properties: uuid: - type: string - format: uuid description: Identifier of a target already configured on this server. + $ref: "#/components/schemas/uuid" domain: type: string description: | @@ -654,29 +2229,30 @@ components: - published - failed lastAttemptAt: - type: string - format: date-time + $ref: "#/components/schemas/date-time" detail: type: string description: Why the last attempt failed, when it did. required: - uuid - - artifactSignature: + artifact-signature: type: object description: | A detached signature over an artifact format's bytes. - The consumption API exposes only a `signatureUrl`, which tells a - consumer where a signature lives but not what it is, so verifying one - means guessing the scheme. A publisher knows, and stating it here is - what makes the signature checkable rather than merely present. + signatures/signature.md calls for an indication of the hash algorithm, + an indicator of the certificate used, and the intermediate and signing + certificates. This carries them, plus the transparency-log entry where + the scheme has one — a signature that is itself publicly logged is far + harder to backdate. + additionalProperties: false properties: signatureType: type: string - description: The signature scheme, so a consumer knows how to verify rather than having to guess. + description: The scheme, so a consumer knows how to verify rather than having to guess. enum: - jws + - jsf - cose - pgp - x509-cms @@ -686,1242 +2262,533 @@ components: type: string format: url description: Direct download URL for the detached signature. + algorithm: + type: string + description: Hash algorithm the signature was computed over, e.g. SHA-256. keyId: type: string description: Key identifier the signature was produced with, where the scheme has one. certificateUrl: type: string format: url - description: Signing certificate or chain, where verification needs one. + description: Signing certificate, where verification needs one. + certificateChainUrl: + type: string + format: url + description: Intermediate certificates between the signing certificate and a trusted root. transparencyLogUrl: type: string format: url - description: | - Entry in a signature transparency log, such as Rekor. A signature - that is itself publicly logged is far harder to backdate. + description: Entry in a signature transparency log, such as Rekor. signedAt: - type: string - format: date-time + $ref: "#/components/schemas/date-time" required: - signatureType - url - - identifier: - type: object - description: An identifier with a specified type - properties: - idType: - description: Type of identifier, e.g. `tei`, `purl`, `cpe`, `swid` - type: string - enum: - - cpe - - tei - - purl - - swid - idValue: - description: Identifier value - type: string - required: - - idType - - idValue - artifactChecksum: - type: object - properties: - algType: - description: Checksum algorithm - type: string - enum: - - SHA-256 - - SHA-384 - - SHA-512 - - SHA3-256 - - SHA3-384 - - SHA3-512 - - BLAKE2b-256 - - BLAKE2b-384 - - BLAKE2b-512 - - BLAKE3 - algValue: - type: string - description: Checksum value - required: - - algType - - algValue - product: - type: object - properties: - identifier: - $ref: '#/components/schemas/typeUuid' - description: A unique identifier for the TEA product - name: - type: string - description: Product name - barcode: - type: string - description: Barcode - sku: - type: string - description: Product SKU - vendorUuid: - $ref: '#/components/schemas/typeUuid' - description: Vendor UUID - identifiers: - type: array - description: List of identifiers for the product - items: - $ref: '#/components/schemas/identifier' - type: - $ref: '#/components/schemas/typePackage' - description: Product type - namespace: - type: string - description: Product namespace (optional) - version: - type: string - description: Product version (optional) - qualifiers: - type: array - items: - type: object - additionalProperties: - type: string - description: Array of qualifier objects containing related key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - components: - type: array - description: List of TEA component UUIDs - items: - $ref: '#/components/schemas/typeUuid' - required: - - identifier - - name - - identifiers - - components - component: - type: object - description: A TEA component - properties: - uuid: - description: A unique identifier for the TEA component - $ref: '#/components/schemas/typeUuid' - name: - type: string - description: Component name - identifiers: - type: array - description: List of identifiers for the component - items: - $ref: '#/components/schemas/identifier' - versions: # Added to match consumer schema requirements - type: array - description: List of available versions for this component - items: - type: string - releases: - type: array - description: List of UUIDs referencing TEA Release entries - items: - $ref: '#/components/schemas/typeUuid' - required: - - uuid - - name - - identifiers - - versions - release: - type: object - description: A TEA release - properties: - uuid: - description: A unique identifier for the TEA Component Release - $ref: '#/components/schemas/typeUuid' - version: - description: Version number - type: string - example: 1.2.3 - releaseDate: - description: Timestamp of the release (for sorting purposes) - type: string - format: date-time - example: '2024-03-20T15:30:00Z' - preRelease: - type: boolean - description: A flag indicating pre-release (or beta) status - identifiers: - type: array - description: List of identifiers for the component - items: - $ref: '#/components/schemas/identifier' - collectionReferences: - type: array - items: - $ref: '#/components/schemas/typeUuid' - description: Array of UUIDs referencing TEA Collection entries - default: [] - required: - - uuid - - version - - releaseDate - collectionUpdateReason: - type: object - description: Reason for the update to the TEA collection - properties: - type: - description: Type of update reason - type: string - enum: - - INITIAL_RELEASE - - VEX_UPDATED - - ARTIFACT_UPDATED - - ARTIFACT_ADDED - - ARTIFACT_REMOVED - comment: - type: string - description: Free text description - required: - - type - collection: + access-policy-status: type: object - description: A collection of security-related documents + description: An object's declared access policy alongside the one actually enforced. + additionalProperties: false properties: uuid: + description: The object the policy applies to. + $ref: "#/components/schemas/uuid" + declared: description: | - UUID of the TEA Collection object. Note that this is equal to the UUID of the associated TEA Release object. - $ref: '#/components/schemas/typeUuid' - version: - type: integer - description: TEA Collection version, incremented each time its content changes. Versions start with 1. - minimum: 1 - releaseDate: - description: The date when the TEA Collection version was created. - type: string - format: date-time - example: '2024-03-20T15:30:00Z' - updateReason: - description: Reason for the update/release of the TEA Collection object. - $ref: '#/components/schemas/collectionUpdateReason' - artifacts: - type: array - description: List of TEA artifact objects. - items: - $ref: '#/components/schemas/artifact' - required: - - uuid - - version - - releaseDate - - updateReason - artifact: - type: object - description: A security-related document - properties: - uuid: - description: UUID of the TEA Artifact object. - $ref: '#/components/schemas/typeUuid' - name: - type: string - description: Artifact name - type: - description: Type of artifact - $ref: '#/components/schemas/typeCollectionArtifactType' - author: - $ref: '#/components/schemas/typeAuthor' - description: Author of the artifact - formats: - type: array - description: List of objects with the same content, but in different formats. - items: - $ref: '#/components/schemas/artifactFormat' + What this object sets for itself. Absent when it declares nothing + and relies entirely on inheritance. + $ref: "#/components/schemas/access-policy" + effective: + description: What is enforced, after inheritance. + $ref: "#/components/schemas/access-policy" + inheritedFrom: + description: | + The ancestor the effective policy came from, when it was not + declared here. Naming it lets a publisher fix the cause rather than + the symptom. + $ref: "#/components/schemas/uuid" required: - uuid - - name - - type - - author - - formats - artifactFormat: - type: object - description: A security-related document in a specific format - properties: - mimeType: - type: string - description: The MIME type of the document - description: - type: string - description: A free text describing the artifact - url: # Changed from artifactUrl to match consumer - type: string - format: uri - description: Direct download URL for the artifact - signatureUrl: # Already matches consumer - type: string - format: uri - description: Direct download URL for an external signature of the artifact - checksums: # Changed from artifactChecksums to match consumer - type: array - items: - $ref: '#/components/schemas/artifactChecksum' - description: List of checksums for the artifact - required: - - mimeType - - description - - url - - checksums - typeAuthor: - type: object - properties: - name: - type: string - email: - type: string - format: email - organization: - type: string - required: - - name - - email - - organization - typeCollectionArtifactType: - type: string - title: Type - description: Specifies the type of external reference. - enum: - - ATTESTATION - - BOM - - BUILD_META - - CERTIFICATION - - FORMULATION - - LICENSE - - RELEASE_NOTES - - SECURITY_TXT - - THREAT_MODEL - - VULNERABILITIES - - OTHER - typeUuid: - type: string - format: uuid - typePackage: - type: string - description: Package type following PURL specification - enum: - - alpm - - apk - - bitbucket - - bitnami - - cargo - - cocoapods - - composer - - conan - - conda - - cpan - - cran - - deb - - docker - - gem - - generic - - github - - golang - - hackage - - hex - - huggingface - - luarocks - - maven - - mlflow - - npm - - nuget - - oci - - pub - - pypi - - qpkg - - rpm - - swid - - swift + - effective responses: - 204-object-deleted: - description: | - The object was deleted and is no longer resolvable through the - consumption API. - - Deletion does not retract what consumers already hold. An object that - was published outside this server SHOULD also be withdrawn through a CLE - `withdrawn` event, so a consumer who acted on it can learn that it was - retracted rather than simply finding it gone. - 409-conflict: - description: | - The request conflicts with the current state of the object: a create - that would duplicate an existing natural key, an update against a - version that has since been superseded, or a distribution change that - would make an object more visible than the object it belongs to. + 204-common-delete: + description: Object deleted successfully + content: + application/json: {} + 400-invalid-request: + description: Request was Invalid + content: + application/json: {} + 401-unauthorized: + description: Authentication required + content: + application/json: {} + 404-object-by-id-not-found: + description: Object requested by identifier not found content: application/json: schema: - $ref: '#/components/schemas/error' - 413-payload-too-large: - description: The uploaded artifact or signature exceeds the size this server accepts. + $ref: "#/components/schemas/error-response" + discovery-response: + description: Discovery information for the requested TEI content: application/json: schema: - $ref: '#/components/schemas/error' - 415-unsupported-media-type: - description: The supplied media type is not one this server accepts for the object. + type: array + items: + $ref: "#/components/schemas/discovery-info" + paginated-product: + description: A paginated response containing TEA Products + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-product-response" + paginated-product-release: + description: A paginated response containing TEA Product Releases + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-product-release-response" + paginated-component: + description: A paginated response containing TEA Components content: application/json: schema: - $ref: '#/components/schemas/error' - 403-forbidden: + $ref: "#/components/schemas/paginated-component-response" + paginated-component-release: + description: A paginated response containing TEA Component Releases + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-component-release-response" + paginated-collection: + description: A paginated response containing TEA Collections + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-collection-response" + object-deleted: description: | - Authenticated, but not entitled to perform this operation on this - object. Distinct from 401 on purpose: a publisher debugging a pipeline - needs to know whether the credential was rejected or the permission was. + The object was deleted and is no longer resolvable through the + consumption API. + publisher-bad-request: + description: The request body is invalid. content: application/json: schema: - $ref: '#/components/schemas/error' - 200-tea-collection-updated: - description: TEA Collection updated successfully + $ref: "#/components/schemas/publisher-error-response" + publisher-unauthorized: + description: Authentication is required, or the credential was rejected. content: application/json: schema: - $ref: '#/components/schemas/collection' - 201-tea-artifact-created: - description: TEA Artifact created successfully + $ref: "#/components/schemas/publisher-error-response" + publisher-forbidden: + description: | + Authenticated, but not entitled to perform this operation on this + object. Distinct from 401 on purpose: a publisher debugging a pipeline + needs to know whether the credential was rejected or the permission + was. content: application/json: schema: - $ref: '#/components/schemas/artifact' - 200-tea-artifact-updated: - description: TEA Artifact updated successfully + $ref: "#/components/schemas/publisher-error-response" + publisher-not-found: + description: No such object, or the caller may not see it. content: application/json: schema: - $ref: '#/components/schemas/artifact' - 200-tea-distribution: - description: The distribution in force for the object + $ref: "#/components/schemas/publisher-error-response" + publisher-conflict: + description: | + The request conflicts with the object's current state: a create that + would duplicate an existing natural key, an update against a version + since superseded, or an access policy wider than the object's parent. content: application/json: schema: - type: object - properties: - uuid: - type: string - format: uuid - description: The object the distribution applies to. - declared: - description: | - What this object sets for itself. Absent when the object - declares nothing and relies entirely on inheritance. - $ref: '#/components/schemas/distribution' - effective: - description: What is actually enforced, after inheritance. - $ref: '#/components/schemas/distribution' - inheritedFrom: - type: string - format: uuid - description: | - The ancestor the effective distribution came from, when it was - not declared on this object. Naming it is what lets a - publisher fix the cause rather than the symptom. - required: - - uuid - - effective - 200-tea-product-updated: - description: TEA Product updated successfully + $ref: "#/components/schemas/publisher-error-response" + publisher-payload-too-large: + description: The uploaded content exceeds the size this server accepts. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-unsupported-media-type: + description: The supplied media type is not one this server accepts for the object. content: application/json: schema: - $ref: '#/components/schemas/product' - 200-tea-component-updated: - description: TEA Component updated successfully + $ref: "#/components/schemas/publisher-error-response" + product-written: + description: The product as the consumption API will serve it. content: application/json: schema: - $ref: '#/components/schemas/component' - 200-tea-release-updated: - description: TEA Release updated successfully + $ref: "#/components/schemas/product" + component-written: + description: The component as the consumption API will serve it. content: application/json: schema: - $ref: '#/components/schemas/release' - 201-tea-product-created: - description: Product created successfully + $ref: "#/components/schemas/component" + product-release-written: + description: The product release as the consumption API will serve it. content: application/json: schema: - type: object - properties: - identifier: - $ref: '#/components/schemas/typeUuid' - description: Unique identifier for the created product - name: - type: string - description: Product name - barcode: - type: string - description: Barcode - sku: - type: string - description: Product SKU - vendorUuid: - $ref: '#/components/schemas/typeUuid' - description: Vendor UUID - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the product (TEI, CPE, SWID, etc.) - type: - $ref: '#/components/schemas/typePackage' - description: Product type - namespace: - type: string - description: Product namespace (optional) - version: - type: string - description: Product version (optional) - qualifiers: - type: array - items: - type: object - additionalProperties: - type: string - description: Array of qualifier objects containing related key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - required: - - identifier - - name - 201-tea-component-created: - description: TEA Component created successfully + $ref: "#/components/schemas/productRelease" + component-release-written: + description: The component release as the consumption API will serve it. content: application/json: schema: - $ref: '#/components/schemas/component' - 201-tea-release-created: - description: TEA Release created successfully + $ref: "#/components/schemas/release" + collection-written: + description: The newly published collection version. content: application/json: schema: - $ref: '#/components/schemas/release' - 201-tea-collection-created: - description: TEA Collection created successfully + $ref: "#/components/schemas/collection" + artifact-written: + description: The artifact as the consumption API will serve it. content: application/json: schema: - $ref: '#/components/schemas/collection' - 400-invalid-request-body: - description: Invalid request body - content: - application/json: {} - 400-invalid-parameters: - description: Invalid parameters - content: - application/json: {} - 401-unauthorized: - description: Authentication required - content: - application/json: {} - 404-object-by-id-not-found: - description: Object requested by identifier not found + $ref: "#/components/schemas/artifact" + access-policy-read: + description: The access policy in force for the object. content: - application/json: {} + application/json: + schema: + $ref: "#/components/schemas/access-policy-status" parameters: - objectUuid: - name: uuid - in: path - required: true - description: UUID of the object in the TEA server. - schema: - type: string - format: uuid - formatIndex: - name: formatIndex - in: path - required: true - description: | - Zero-based index of the format within the artifact's `formats` array. - Formats are addressed positionally because a media type is not unique - within an artifact — the same document is often published both - compressed and not. + page-size: + name: pageSize + description: The maximum number of results to return. + in: query + required: false schema: type: integer - minimum: 0 - idempotencyKey: - name: Idempotency-Key - in: header - required: false + format: int64 + minimum: 1 + maximum: 100 + default: 25 + page-token: + name: pageToken description: | - A client-chosen key making a create safe to retry. - - Publication runs in release pipelines, which retry. Without this, a - timeout that actually succeeded produces a second product on the next - attempt, and the duplicate is discovered by a consumer rather than by - the publisher. A server that supports the key MUST return the original - result for a repeat of the same key rather than creating again. - schema: - type: string - maxLength: 255 - contentDigest: - name: Content-Digest - in: header + An opaque token used to retrieve the next page of results. + This should be copied exactly from the `nextPageToken` field of a previous response. + in: query required: false - description: | - Digest of the request body, per RFC 9530, e.g. `sha-256=::`. - Where supplied, the server MUST verify it against the bytes received and - reject a mismatch rather than storing content that does not match the - checksum it will be published under. schema: type: string - page-offset: - name: pageOffset - description: Pagination offset + sort-order: + name: sortOrder + description: The direction of the sort. in: query required: false schema: - type: integer - format: int64 - default: 0 - pageSize: - name: pageSize - description: Pagination size + type: string + enum: + - asc + - desc + default: asc + sort-field-collection: + name: sortField + description: The field by which to sort the results. in: query required: false - schema: - type: integer - format: int64 - default: 100 - teiUrn: - name: teiUrn - description: Transparency Exchange Identifier (URN) - in: path - required: true - schema: - type: string - pattern: ^urn:tei:[a-zA-Z0-9]+:[a-zA-Z0-9\.-]+:.+$ - productIdentifier: - name: productIdentifier - description: Customer unique Product Identifier - in: path - required: true schema: type: string - format: uuid - collectionIdentifier: - name: collectionIdentifier - description: TEA Collection Identifier - in: path - required: true + enum: + - version + default: version + sort-field-component: + name: sortField + description: The field by which to sort the results. + in: query + required: false schema: type: string - format: uuid - barcode: - name: barcode - description: Barcode + enum: + - name + default: name + sort-field-component-release: + name: sortField + description: The field by which to sort the results. in: query required: false schema: type: string - sku: - name: sku - description: Product SKU + enum: + - createdDate + - releaseDate + - version + default: createdDate + sort-field-product: + name: sortField + description: The field by which to sort the results. in: query required: false schema: type: string - vendorUuid: - name: vendorUuid - description: Vendor UUID + enum: + - name + default: name + sort-field-product-release: + name: sortField + description: The field by which to sort the results. in: query required: false schema: - $ref: '#/components/schemas/typeUuid' - idType: + type: string + enum: + - createdDate + - releaseDate + - version + default: createdDate + id-type: name: idType description: Type of identifier specified in the `idValue` parameter in: query schema: - type: string - enum: - - cpe - - tei - - purl + $ref: "#/components/schemas/identifier-type" id-value: name: idValue - description: If present, only the objects with the given identifier value will be returned + description: If present, only the objects with the given identifier value will be returned. in: query schema: type: string + object-uuid: + name: uuid + in: path + required: true + description: UUID of the object. + schema: + $ref: "#/components/schemas/uuid" + artifact-uuid: + name: uuid + in: path + required: true + description: UUID of the TEA Artifact. + schema: + $ref: "#/components/schemas/uuid" + format-index: + name: formatIndex + in: path + required: true + description: | + Zero-based index of the format within the artifact's `formats` array. + Formats are addressed positionally because a media type is not unique + within an artifact — the same document is often published both + compressed and not. + schema: + type: integer + minimum: 0 + idempotency-key: + name: Idempotency-Key + in: header + required: false + description: | + A client-chosen key that makes a create safe to retry. + + Publication runs in release pipelines, and pipelines retry. Without + this, a timeout that actually succeeded produces a second object on the + next attempt — a duplicate discovered by a consumer rather than by the + publisher. A server supporting the key MUST return the original result + for a repeat of the same key rather than creating again. + schema: + type: string + maxLength: 255 + content-digest: + name: Content-Digest + in: header + required: false + description: | + Digest of the request body per RFC 9530, e.g. `sha-256=::`. + Where supplied, the server MUST verify it against the bytes received + and reject a mismatch rather than storing content that does not match + the checksum it will be published under. + schema: + type: string securitySchemes: bearerAuth: type: http scheme: bearer - bearerFormat: JWT - description: | - Bearer token authentication is a stateless authentication scheme where the client sends a token in the HTTP Authorization header. - The server validates the token and grants access to the requested resource to anyone with the token without needing to authenticate - again, and stolen tokens can be used by anyone. basicAuth: type: http scheme: basic - description: | - Basic authentication is a plaintext scheme built into the HTTP protocol and has been deemed insecure because headers are commonly - logged in plaintext. - mutualTLS: - type: mutualTLS - description: | - Client certificate authentication is a secure authentication scheme where the client presents a valid certificate to the server - for authentication. The server validates the certificate and grants access to the requested resource, but stolen certificates can be - used by anyone. - pasetoAuth: - type: http - scheme: bearer - bearerFormat: PASETO - description: | - Platform-Agnostic Security Tokens (PASETO) is a secure alternative to JWT that is resistant to many cryptographic vulnerabilities. - PASETO tokens use versioned protocols that specify modern cryptographic algorithms. - verifiableCredentials: - type: http - scheme: bearer - bearerFormat: VC-JWT - description: | - W3C Verifiable Credentials 2.0 with proof field is a standard for expressing verifiable credentials as signed JWT tokens. - The integrity and authenticity of the credential can be cryptographically verified. - hmacAuth: - type: apiKey - in: header - name: X-HMAC-Signature - description: | - HMAC authentication uses a shared secret key to create a signature of the request content using a secure hash algorithm (SHA256, - SHA384, SHA512, SHA3-256, SHA3-384, or SHA3-512). The server verifies this signature to authenticate the request. requestBodies: - UpdateTeaCollection: - required: true - content: - application/json: - schema: - type: object - properties: - updateReason: - $ref: '#/components/schemas/collectionUpdateReason' - distribution: - $ref: '#/components/schemas/distribution' - required: - - updateReason - CreateTeaArtifact: + write-product: required: true content: application/json: schema: type: object + additionalProperties: false properties: - collectionUuid: - type: string - format: uuid - description: The collection this artifact belongs to. name: type: string - description: Human-readable name of the artifact. - type: - type: string - description: Type of artifact, as defined by the consumption API. - author: - type: string - description: Author of the artifact. - formats: + description: Product name. + identifiers: type: array description: | - The representations this artifact is published in. A format - may carry a `url` when the publisher hosts the bytes itself; - otherwise the bytes are uploaded to this server afterwards. + Identifiers for the product. A server assigns the TEI itself + — it names the server's own authority over the object, so a + publisher supplying one would be asserting something only the + server can know. items: - $ref: '#/components/schemas/artifactFormat' - distribution: - $ref: '#/components/schemas/distribution' - required: - - collectionUuid - - name - - formats - UpdateTeaArtifact: - required: true - content: - application/json: - schema: - type: object - description: | - Metadata only. Stored bytes are immutable — see the operation - description for why replacing content means a new format or a new - artifact rather than an overwrite. - properties: - name: - type: string - type: - type: string - author: - type: string - distribution: - $ref: '#/components/schemas/distribution' - UpdateTeaProduct: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-component: required: true content: application/json: schema: type: object + additionalProperties: false properties: name: type: string - description: Product name (required) - barcode: - type: string - description: Barcode - sku: - type: string - description: Product SKU - vendorUuid: - $ref: '#/components/schemas/typeUuid' - description: Vendor UUID + description: Component name. identifiers: type: array items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the product (TEI, CPE, SWID, etc.) - type: - $ref: '#/components/schemas/typePackage' - description: Product type (required) - namespace: - type: string - description: Product namespace (optional) - version: - type: string - description: Product version (optional) - qualifiers: - type: array - items: - type: object - additionalProperties: - type: string - description: Array of qualifier objects containing related key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - examples: - basic: - summary: Basic product update - value: - name: Updated Product Name - barcode: '123456789012' - sku: PROD-001 - vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - type: generic - version: 1.0.0 - qualifiers: - - os: linux - arch: x86_64 - full: - summary: Full product update with all fields - value: - name: Updated Product Name - barcode: '123456789012' - sku: PROD-001 - vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - identifiers: - - idType: tei - idValue: urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 - - idType: cpe - idValue: cpe:2.3:a:example:product:2.1.0:*:*:*:*:*:*:* - type: maven - namespace: org.example - version: 2.1.0 - qualifiers: - - classifier: sources - repository_url: https://repo.example.com - subpath: src/main - CreateTeaProduct: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-product-release: required: true content: application/json: schema: type: object + additionalProperties: false properties: - name: - type: string - barcode: - type: string - description: Barcode - sku: - type: string - description: Product SKU - vendorUuid: - $ref: '#/components/schemas/typeUuid' - description: Vendor UUID - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the product (TEI, CPE, SWID, etc.) - type: - $ref: '#/components/schemas/typePackage' - description: Product type (required) - namespace: - type: string - description: Product namespace (optional) + product: + description: UUID of the product this release belongs to. + $ref: "#/components/schemas/uuid" version: type: string - description: Product version (optional) - qualifiers: - type: array - items: - type: object - additionalProperties: - type: string - description: Array of qualifier objects containing related key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - required: - - name - examples: - basic: - summary: Basic product creation - value: - name: Example Product - type: generic - qualifiers: - - os: linux - arch: x86_64 - full: - summary: Full product creation with all fields - value: - name: Complete Product Example - barcode: '123456789012' - sku: PROD-001 - vendorUuid: 123e4567-e89b-12d3-a456-426614174000 - identifiers: - - idType: tei - idValue: urn:tei:uuid:example.com:d4d9f54a-abcf-11ee-ac79-1a52914d44b1 - - idType: cpe - idValue: cpe:2.3:a:example:product:2.1.0:*:*:*:*:*:*:* - type: maven - namespace: org.example - version: 2.1.0 - qualifiers: - - classifier: sources - repository_url: https://repo.example.com - subpath: src/main - CreateTeaComponent: - required: true - content: - application/json: - schema: - type: object - properties: - productIdentifier: - $ref: '#/components/schemas/typeUuid' - name: - type: string - description: Component name + description: Version of the release. + releaseDate: + $ref: "#/components/schemas/date-time" + preRelease: + type: boolean + description: Whether this release is a pre-release. identifiers: type: array items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the component (TEI, CPE, SWID, etc.) - type: - $ref: '#/components/schemas/typePackage' - description: Component type (required) - namespace: - type: string - description: Component namespace (optional) - version: - type: string - description: Component version (optional) - qualifiers: - type: array - items: - type: object - additionalProperties: - type: string - description: Array of qualifier objects containing related key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - required: - - productIdentifier - - name - examples: - basic: - summary: Basic component creation - value: - productIdentifier: 123e4567-e89b-12d3-a456-426614174000 - name: Example Component - type: maven - qualifiers: - - classifier: jar - type: binary - full: - summary: Full component creation with all fields - value: - productIdentifier: 123e4567-e89b-12d3-a456-426614174000 - name: Apache Log4j Core Component - identifiers: - - idType: cpe - idValue: cpe:2.3:a:apache:log4j:2.17.1:*:*:*:*:*:*:* - - idType: tei - idValue: urn:tei:uuid:apache.org:log4j-core-component - type: maven - namespace: org.apache.logging.log4j - version: 2.17.1 - qualifiers: - - classifier: sources - repository_url: https://github.com/apache/logging-log4j2 - subpath: log4j-core - UpdateTeaComponent: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-component-release: required: true content: application/json: schema: type: object + additionalProperties: false properties: - name: - type: string - description: Component name - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the component (TEI, CPE, SWID, etc.) - type: - $ref: '#/components/schemas/typePackage' - description: Component type - namespace: - type: string - description: Component namespace (optional) + component: + description: UUID of the component this release belongs to. + $ref: "#/components/schemas/uuid" version: type: string - description: Component version (optional) - qualifiers: + description: Version of the release. + releaseDate: + $ref: "#/components/schemas/date-time" + preRelease: + type: boolean + description: Whether this release is a pre-release. + identifiers: type: array items: - type: object - additionalProperties: - type: string - description: Array of qualifier objects containing related key-value pairs (optional) - subpath: - type: string - description: Subpath (optional) - examples: - basic: - summary: Basic component update - value: - name: Updated Component Name - type: maven - version: 2.0.0 - qualifiers: - - classifier: jar - type: binary - full: - summary: Full component update with all fields - value: - name: Updated Apache Log4j Core Component - identifiers: - - idType: cpe - idValue: cpe:2.3:a:apache:log4j:2.18.0:*:*:*:*:*:*:* - - idType: tei - idValue: urn:tei:uuid:apache.org:log4j-core-component-updated - type: maven - namespace: org.apache.logging.log4j - version: 2.18.0 - qualifiers: - - classifier: sources - repository_url: https://github.com/apache/logging-log4j2 - - type: library - scope: compile - subpath: log4j-core - CreateTeaRelease: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-collection: required: true content: application/json: schema: type: object + additionalProperties: false properties: - componentIdentifier: - $ref: '#/components/schemas/typeUuid' - description: Customer unique identifier of the TEA Component - version: - type: string - description: Version number - releaseDate: - type: string - format: date-time - example: '2024-03-20T15:30:00Z' - description: Timestamp of the release - preRelease: - type: boolean - description: Flag indicating pre-release status - identifiers: - type: array - items: - $ref: '#/components/schemas/identifier' - description: List of identifiers for the release + updateReason: + description: Why this version was published. + $ref: "#/components/schemas/collection-update-reason" + accessPolicy: + $ref: "#/components/schemas/access-policy" required: - - componentIdentifier - - version - - releaseDate - examples: - basic: - summary: Basic release creation - value: - componentIdentifier: 123e4567-e89b-12d3-a456-426614174000 - version: 1.0.0 - releaseDate: '2024-03-20T15:30:00Z' - preRelease: false - identifiers: - - idType: purl - idValue: pkg:maven/org.example/component@1.0.0 - UpdateTeaRelease: + - updateReason + write-artifact: required: true content: application/json: schema: type: object + additionalProperties: false properties: - version: - type: string - description: Version number - releaseDate: + collection: + description: | + UUID of the collection this artifact belongs to, which is the + UUID of its release. + $ref: "#/components/schemas/uuid" + name: type: string - format: date-time - example: '2024-03-20T15:30:00Z' - preRelease: - type: boolean - description: May only be set to false if previously true - identifiers: + description: Human-readable name of the artifact. + type: + $ref: "#/components/schemas/artifact-type" + formats: type: array + description: | + The representations this artifact is published in. A format + may carry a `url` when the publisher hosts the bytes itself; + otherwise they are uploaded to this server afterwards. items: - $ref: '#/components/schemas/identifier' - examples: - basic: - summary: Basic release update - value: - version: 1.0.1 - releaseDate: '2024-03-21T10:15:00Z' - preRelease: false - full: - summary: Full release update with identifiers - value: - version: 2.0.0 - releaseDate: '2024-04-15T14:30:00Z' - preRelease: false - identifiers: - - idType: purl - idValue: pkg:maven/org.example/component@2.0.0 - - idType: cpe - idValue: cpe:2.3:a:example:component:2.0.0:*:*:*:*:*:*:* - CreateTeaCollection: + $ref: "#/components/schemas/artifact-format" + accessPolicy: + $ref: "#/components/schemas/access-policy" + required: + - collection + - name + - formats + update-artifact: required: true content: application/json: schema: type: object + additionalProperties: false + description: | + Metadata only. Stored bytes are immutable — replacing content + means a new format or a new artifact, not an overwrite. properties: - releaseIdentifier: - $ref: '#/components/schemas/typeUuid' - description: Customer unique identifier of the TEA Release - updateReason: - $ref: '#/components/schemas/collectionUpdateReason' - description: Reason for collection creation or update - artifacts: - type: array - items: - type: object - properties: - name: - type: string - description: Artifact name - type: - $ref: '#/components/schemas/typeCollectionArtifactType' - description: Type of artifact - author: - $ref: '#/components/schemas/typeAuthor' - description: Author of the artifact - formats: - type: array - items: - type: object - properties: - description: - type: string - description: A free text describing the artifact - mimeType: - type: string - description: The MIME type of the document - url: - type: string - format: uri - description: Direct download URL for the artifact - checksums: - type: array - items: - $ref: '#/components/schemas/artifactChecksum' - description: List of checksums for the artifact - signatureUrl: - type: string - format: uri - description: URL for an external signature of the artifact - required: - - description - - mimeType - - url - - checksums - required: - - name - - type - - author - - formats - required: - - releaseIdentifier - - updateReason - examples: - basic: - summary: Basic collection creation - value: - releaseIdentifier: 123e4567-e89b-12d3-a456-426614174000 - updateReason: - type: INITIAL_RELEASE - comment: Initial release of collection - artifacts: - - name: Example SBOM - type: BOM - author: - name: John Doe - email: john.doe@example.com - organization: Example Org - formats: - - description: CycloneDX SBOM (XML) - mimeType: application/vnd.cyclonedx+xml - url: https://example.com/sbom.xml - checksums: - - algType: SHA-256 - algValue: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - signatureUrl: https://example.com/sbom.xml.sig + name: + type: string + type: + $ref: "#/components/schemas/artifact-type" + accessPolicy: + $ref: "#/components/schemas/access-policy" security: - bearerAuth: [] - basicAuth: [] - - mutualTLS: [] - - pasetoAuth: [] - - verifiableCredentials: [] - - hmacAuth: [] tags: - name: TEA Product - description: Operations related to TEA Products + - name: TEA Product Release - name: TEA Component - description: Operations related to TEA Components - - name: TEA Release - description: Operations related to TEA Releases - - name: TEA Collection - description: Operations related to TEA Collections + - name: TEA Component Release - name: TEA Artifact + - name: TEA Discovery + - name: CLE + - name: TEA Publication description: | - Operations on the artifacts a collection publishes, including uploading - their content and attaching detached signatures. - - name: TEA Distribution + Operations that create, update and delete the objects the consumption API + serves. + - name: TEA Access Policy description: | Operations controlling who may read an object through the consumption API: the publishing organisation alone, named counterparties, or anyone. diff --git a/spec/publisher/overlay.yaml b/spec/publisher/overlay.yaml new file mode 100644 index 0000000..620f24c --- /dev/null +++ b/spec/publisher/overlay.yaml @@ -0,0 +1,1114 @@ +# Publication operations, overlaid onto the consumption specification. +# +# Everything shared with the consumption API is referenced, never redefined. +# build.mjs fails the build if this file declares a path method, schema, +# parameter, response or request body that the consumption specification +# already owns — which is how the previous copy drifted. + +paths: + # ── Product ─────────────────────────────────────────────────────────────── + /product: + post: + summary: Create a TEA Product + description: | + Create a product and return the object the consumption API will serve, + including the UUID and identifiers the server assigned. + operationId: createTeaProduct + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-product" + responses: + "201": + $ref: "#/components/responses/product-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /product/{uuid}: + patch: + summary: Update a TEA Product + description: Update a product's mutable fields. Omitted fields are left unchanged. + operationId: updateTeaProduct + parameters: + - $ref: "#/components/parameters/object-uuid" + requestBody: + $ref: "#/components/requestBodies/write-product" + responses: + "200": + $ref: "#/components/responses/product-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + delete: + summary: Delete a TEA Product + description: | + Delete a product and everything beneath it. + + Deletion does not retract what consumers already hold. A product that + has been public SHOULD also be withdrawn through a CLE `withdrawn` + event, so a consumer who acted on it learns that it was retracted + rather than simply finding it gone. + operationId: deleteTeaProduct + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + + # ── Component ───────────────────────────────────────────────────────────── + /component: + post: + summary: Create a TEA Component + operationId: createTeaComponent + description: Create a component and return the object the consumption API will serve. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-component" + responses: + "201": + $ref: "#/components/responses/component-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /component/{uuid}: + patch: + summary: Update a TEA Component + description: Update a component's mutable fields. Omitted fields are left unchanged. + operationId: updateTeaComponent + parameters: + - $ref: "#/components/parameters/object-uuid" + requestBody: + $ref: "#/components/requestBodies/write-component" + responses: + "200": + $ref: "#/components/responses/component-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + delete: + summary: Delete a TEA Component + description: Delete a component and its releases. + operationId: deleteTeaComponent + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + + # ── Releases ────────────────────────────────────────────────────────────── + # Split to match the consumption API, which has distinguished product + # releases from component releases since 0.1.0. A single /release could not + # say which of the two it was creating. + /productRelease: + post: + summary: Create a TEA Product Release + operationId: createTeaProductRelease + description: Create a release of a product. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-product-release" + responses: + "201": + $ref: "#/components/responses/product-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /productRelease/{uuid}: + patch: + summary: Update a TEA Product Release + operationId: updateTeaProductRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Update a product release's mutable fields. + requestBody: + $ref: "#/components/requestBodies/write-product-release" + responses: + "200": + $ref: "#/components/responses/product-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + delete: + summary: Delete a TEA Product Release + operationId: deleteTeaProductRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Delete a product release and the collection it holds. + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /componentRelease: + post: + summary: Create a TEA Component Release + operationId: createTeaComponentRelease + description: Create a release of a component. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-component-release" + responses: + "201": + $ref: "#/components/responses/component-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /componentRelease/{uuid}: + patch: + summary: Update a TEA Component Release + operationId: updateTeaComponentRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Update a component release's mutable fields. + requestBody: + $ref: "#/components/requestBodies/write-component-release" + responses: + "200": + $ref: "#/components/responses/component-release-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + delete: + summary: Delete a TEA Component Release + operationId: deleteTeaComponentRelease + parameters: + - $ref: "#/components/parameters/object-uuid" + description: Delete a component release and the collection it holds. + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + + # ── Collections ─────────────────────────────────────────────────────────── + # A collection is not an independent object: the consumption specification + # states its UUID matches the release it belongs to, and that an update only + # changes the version. Publishing one is therefore addressed through its + # release, and is a PUT rather than a POST — there is one current collection + # per release, and this replaces it with the next version. + /productRelease/{uuid}/collection: + put: + summary: Publish a new collection version for a product release + operationId: publishTeaProductReleaseCollection + parameters: + - $ref: "#/components/parameters/object-uuid" + description: | + Publish the next version of this release's collection. + + The server assigns the version, incrementing from the current one. + `updateReason` is required: a collection whose contents change without + saying why is indistinguishable, to a consumer, from one that was + tampered with. + requestBody: + $ref: "#/components/requestBodies/write-collection" + responses: + "200": + $ref: "#/components/responses/collection-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /componentRelease/{uuid}/collection: + put: + summary: Publish a new collection version for a component release + operationId: publishTeaComponentReleaseCollection + parameters: + - $ref: "#/components/parameters/object-uuid" + description: | + Publish the next version of this release's collection. The server + assigns the version, and `updateReason` is required. + requestBody: + $ref: "#/components/requestBodies/write-collection" + responses: + "200": + $ref: "#/components/responses/collection-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + + # ── Artifacts ───────────────────────────────────────────────────────────── + /artifact: + post: + summary: Create a TEA Artifact + operationId: createTeaArtifact + description: | + Register an artifact within a release's collection. + + This creates the entry and its formats; it does not carry the bytes. + Content goes up per format afterwards. Keeping the two apart matters + because an SBOM is routinely tens of megabytes, and a publisher who + must resend all of it to correct one metadata field will in practice + not correct the field. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-artifact" + responses: + "201": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /artifact/{uuid}: + patch: + summary: Update a TEA Artifact + operationId: updateTeaArtifact + description: | + Update an artifact's metadata. + + Stored bytes are immutable: a checksum a consumer has already recorded + must not begin describing different content. Replacing content means a + new format or a new artifact version, never an overwrite in place. + parameters: + - $ref: "#/components/parameters/artifact-uuid" + requestBody: + $ref: "#/components/requestBodies/update-artifact" + responses: + "200": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + delete: + summary: Delete a TEA Artifact + operationId: deleteTeaArtifact + description: Delete an artifact and any content this server holds for it. + parameters: + - $ref: "#/components/parameters/artifact-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + /artifact/{uuid}/format/{formatIndex}/content: + put: + summary: Upload artifact content + operationId: uploadTeaArtifactContent + description: | + Upload the bytes of one artifact format. + + The body is the artifact itself in its own media type, rather than + base64 inside JSON, which would inflate it by a third for no benefit. + + A client SHOULD send `Content-Digest` (RFC 9530). Where it does, the + server MUST verify it against the received bytes and reject a mismatch. + That is what makes an upload safe to retry: a truncated transfer fails + loudly instead of publishing corrupt content under a checksum that says + otherwise. + parameters: + - $ref: "#/components/parameters/artifact-uuid" + - $ref: "#/components/parameters/format-index" + - $ref: "#/components/parameters/content-digest" + requestBody: + required: true + description: The artifact bytes, in the media type declared by the format. + content: + application/octet-stream: + schema: + type: string + format: binary + application/vnd.cyclonedx+json: + schema: + type: string + format: binary + application/spdx+json: + schema: + type: string + format: binary + application/sarif+json: + schema: + type: string + format: binary + responses: + "200": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "413": + $ref: "#/components/responses/publisher-payload-too-large" + "415": + $ref: "#/components/responses/publisher-unsupported-media-type" + tags: + - TEA Publication + /artifact/{uuid}/format/{formatIndex}/signature: + put: + summary: Attach a detached signature to an artifact format + operationId: uploadTeaArtifactSignature + description: | + Attach a detached signature, declaring the scheme it was produced with. + + The consumption API exposes a `signatureUrl` and nothing more, so a + consumer must infer the scheme from the bytes before it can verify + anything. The publisher knows, and recording it is what turns a + signature from something that exists into something that can be + checked. See signatures/signature.md. + parameters: + - $ref: "#/components/parameters/artifact-uuid" + - $ref: "#/components/parameters/format-index" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/artifact-signature" + responses: + "200": + $ref: "#/components/responses/artifact-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "415": + $ref: "#/components/responses/publisher-unsupported-media-type" + tags: + - TEA Publication + + # ── Access policy ───────────────────────────────────────────────────────── + # Named "access policy" rather than "distribution" because the consumption + # specification already uses `release-distribution` for something else + # entirely: a downloadable build with its own URL and checksums. Reusing the + # word for access control in a combined document would be actively + # misleading. + /accessPolicy/{uuid}: + get: + summary: Read the access policy in force for an object + operationId: getTeaAccessPolicy + description: | + The access policy actually in force, after inheritance. + + Reported as both what the object declares and what it effectively has, + because the difference between the two is exactly where accidental + disclosure hides. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "200": + $ref: "#/components/responses/access-policy-read" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Access Policy + put: + summary: Set the access policy for an object + operationId: setTeaAccessPolicy + description: | + Replace the object's own access policy. + + A server MUST reject a policy wider than the effective policy of the + object's parent, with `ACCESS_WIDENS_PARENT`. A server SHOULD require + a separate confirmation for a change to `public`, because no later + request undoes it: whatever was fetched while it was public stays + fetched. + parameters: + - $ref: "#/components/parameters/object-uuid" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/access-policy" + responses: + "200": + $ref: "#/components/responses/access-policy-read" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Access Policy + +components: + schemas: + publisher-error-type: + type: string + description: | + Classification of a publication error. + + Extends the consumption specification's `unknown-error-type` with the + failures only a writer can hit. The two consumption values are repeated + so a client can branch on one enumeration rather than two. + enum: + - OBJECT_UNKNOWN + - OBJECT_NOT_SHAREABLE + - INVALID_REQUEST + - UNAUTHORIZED + - FORBIDDEN + - DUPLICATE_OBJECT + - VERSION_CONFLICT + - ACCESS_WIDENS_PARENT + - UNSUPPORTED_MEDIA_TYPE + - PAYLOAD_TOO_LARGE + - CHECKSUM_MISMATCH + - SIGNATURE_INVALID + + publisher-error-response: + type: object + description: | + A publication error. Same envelope as the consumption specification's + `error-response`, over the wider publication enumeration, with an + optional human-readable message and a pointer to the offending field. + + Publication is automated — a release pipeline is usually the caller — + so a failure has to be actionable without a human reading prose. + additionalProperties: false + properties: + error: + $ref: "#/components/schemas/publisher-error-type" + message: + type: string + description: Human-readable explanation. Not a stable interface; branch on `error`. + field: + type: string + description: JSON Pointer to the offending member of the request body, where one applies. + required: + - error + + access-policy: + type: object + description: | + Who may read an object through the consumption API. + + TEA describes how transparency data is fetched, not who is entitled to + fetch it. That is a reasonable boundary for a consumption API and an + impossible one for a publication API, where the same server holds + material that is deliberately public, material shared with named + counterparties under agreement, and material that is purely internal. + Absent this, every publisher invents an answer, incompatibly. + + Resolved by inheritance: an artifact takes its collection's policy, a + collection its release's, a release its product's. An object MAY narrow + what it inherits and MUST NOT widen it — a server rejects the attempt + with `ACCESS_WIDENS_PARENT`. Without that rule, marking a product + private would guarantee nothing, because any artifact beneath it could + quietly be made public. + + A consumer denied by this policy is answered with the consumption + specification's existing `OBJECT_NOT_SHAREABLE`, so enforcement needs + no new consumer-side behaviour. + additionalProperties: false + properties: + visibility: + type: string + description: | + `private` — only the publishing organisation may read it. + + `shared` — readable by the publishing organisation and the + organisations named in `sharedWith`. Authentication is required and + the reader's identity decides the answer. + + `public` — readable without authentication. The only value that + makes an object part of the open transparency web, and irreversible + in practice: what has been fetched cannot be recalled. + enum: + - private + - shared + - public + default: private + sharedWith: + type: array + description: | + Organisations entitled to read this object when `visibility` is + `shared`. MUST be empty for any other visibility, so that a later + change to `private` cannot silently leave a grant standing. + items: + $ref: "#/components/schemas/share-grant" + publishTo: + type: array + description: | + Remote TEA servers this object is mirrored to. Mirroring is a copy, + not a reference: once an object reaches another server, this + server's policy no longer governs it. + items: + $ref: "#/components/schemas/publication-target" + required: + - visibility + + share-grant: + type: object + description: An entitlement for one organisation to read one object. + additionalProperties: false + properties: + organizationUuid: + description: The consuming organisation's identifier on this server. + $ref: "#/components/schemas/uuid" + organizationName: + type: string + description: Display name when the grant was made. Advisory; the UUID is the identity. + grantedAt: + $ref: "#/components/schemas/date-time" + expiresAt: + description: | + When the grant lapses. A grant for the length of an evaluation or a + contract is the common case, and a server that cannot express an + expiry pushes publishers towards grants nobody ever revokes. + $ref: "#/components/schemas/date-time" + required: + - organizationUuid + + publication-target: + type: object + description: A remote TEA server this publisher mirrors objects to. + additionalProperties: false + properties: + uuid: + description: Identifier of a target already configured on this server. + $ref: "#/components/schemas/uuid" + domain: + type: string + description: | + The TEI domain-name component of the target, which is what a + consumer resolves. Reported so a publisher can see where a copy + went without holding the target's credentials. + state: + type: string + description: State of the most recent mirroring attempt for this object. + enum: + - pending + - published + - failed + lastAttemptAt: + $ref: "#/components/schemas/date-time" + detail: + type: string + description: Why the last attempt failed, when it did. + required: + - uuid + + artifact-signature: + type: object + description: | + A detached signature over an artifact format's bytes. + + signatures/signature.md calls for an indication of the hash algorithm, + an indicator of the certificate used, and the intermediate and signing + certificates. This carries them, plus the transparency-log entry where + the scheme has one — a signature that is itself publicly logged is far + harder to backdate. + additionalProperties: false + properties: + signatureType: + type: string + description: The scheme, so a consumer knows how to verify rather than having to guess. + enum: + - jws + - jsf + - cose + - pgp + - x509-cms + - sigstore-bundle + - ssh + url: + type: string + format: url + description: Direct download URL for the detached signature. + algorithm: + type: string + description: Hash algorithm the signature was computed over, e.g. SHA-256. + keyId: + type: string + description: Key identifier the signature was produced with, where the scheme has one. + certificateUrl: + type: string + format: url + description: Signing certificate, where verification needs one. + certificateChainUrl: + type: string + format: url + description: Intermediate certificates between the signing certificate and a trusted root. + transparencyLogUrl: + type: string + format: url + description: Entry in a signature transparency log, such as Rekor. + signedAt: + $ref: "#/components/schemas/date-time" + required: + - signatureType + - url + + access-policy-status: + type: object + description: An object's declared access policy alongside the one actually enforced. + additionalProperties: false + properties: + uuid: + description: The object the policy applies to. + $ref: "#/components/schemas/uuid" + declared: + description: | + What this object sets for itself. Absent when it declares nothing + and relies entirely on inheritance. + $ref: "#/components/schemas/access-policy" + effective: + description: What is enforced, after inheritance. + $ref: "#/components/schemas/access-policy" + inheritedFrom: + description: | + The ancestor the effective policy came from, when it was not + declared here. Naming it lets a publisher fix the cause rather than + the symptom. + $ref: "#/components/schemas/uuid" + required: + - uuid + - effective + + parameters: + object-uuid: + name: uuid + in: path + required: true + description: UUID of the object. + schema: + $ref: "#/components/schemas/uuid" + artifact-uuid: + name: uuid + in: path + required: true + description: UUID of the TEA Artifact. + schema: + $ref: "#/components/schemas/uuid" + format-index: + name: formatIndex + in: path + required: true + description: | + Zero-based index of the format within the artifact's `formats` array. + Formats are addressed positionally because a media type is not unique + within an artifact — the same document is often published both + compressed and not. + schema: + type: integer + minimum: 0 + idempotency-key: + name: Idempotency-Key + in: header + required: false + description: | + A client-chosen key that makes a create safe to retry. + + Publication runs in release pipelines, and pipelines retry. Without + this, a timeout that actually succeeded produces a second object on the + next attempt — a duplicate discovered by a consumer rather than by the + publisher. A server supporting the key MUST return the original result + for a repeat of the same key rather than creating again. + schema: + type: string + maxLength: 255 + content-digest: + name: Content-Digest + in: header + required: false + description: | + Digest of the request body per RFC 9530, e.g. `sha-256=::`. + Where supplied, the server MUST verify it against the bytes received + and reject a mismatch rather than storing content that does not match + the checksum it will be published under. + schema: + type: string + + responses: + object-deleted: + description: | + The object was deleted and is no longer resolvable through the + consumption API. + publisher-bad-request: + description: The request body is invalid. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-unauthorized: + description: Authentication is required, or the credential was rejected. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-forbidden: + description: | + Authenticated, but not entitled to perform this operation on this + object. Distinct from 401 on purpose: a publisher debugging a pipeline + needs to know whether the credential was rejected or the permission + was. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-not-found: + description: No such object, or the caller may not see it. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-conflict: + description: | + The request conflicts with the object's current state: a create that + would duplicate an existing natural key, an update against a version + since superseded, or an access policy wider than the object's parent. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-payload-too-large: + description: The uploaded content exceeds the size this server accepts. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + publisher-unsupported-media-type: + description: The supplied media type is not one this server accepts for the object. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" + product-written: + description: The product as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/product" + component-written: + description: The component as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/component" + product-release-written: + description: The product release as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/productRelease" + component-release-written: + description: The component release as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/release" + collection-written: + description: The newly published collection version. + content: + application/json: + schema: + $ref: "#/components/schemas/collection" + artifact-written: + description: The artifact as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/artifact" + access-policy-read: + description: The access policy in force for the object. + content: + application/json: + schema: + $ref: "#/components/schemas/access-policy-status" + + requestBodies: + write-product: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + description: Product name. + identifiers: + type: array + description: | + Identifiers for the product. A server assigns the TEI itself + — it names the server's own authority over the object, so a + publisher supplying one would be asserting something only the + server can know. + items: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-component: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + description: Component name. + identifiers: + type: array + items: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-product-release: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + product: + description: UUID of the product this release belongs to. + $ref: "#/components/schemas/uuid" + version: + type: string + description: Version of the release. + releaseDate: + $ref: "#/components/schemas/date-time" + preRelease: + type: boolean + description: Whether this release is a pre-release. + identifiers: + type: array + items: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-component-release: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + component: + description: UUID of the component this release belongs to. + $ref: "#/components/schemas/uuid" + version: + type: string + description: Version of the release. + releaseDate: + $ref: "#/components/schemas/date-time" + preRelease: + type: boolean + description: Whether this release is a pre-release. + identifiers: + type: array + items: + $ref: "#/components/schemas/identifier" + accessPolicy: + $ref: "#/components/schemas/access-policy" + write-collection: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + updateReason: + description: Why this version was published. + $ref: "#/components/schemas/collection-update-reason" + accessPolicy: + $ref: "#/components/schemas/access-policy" + required: + - updateReason + write-artifact: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + collection: + description: | + UUID of the collection this artifact belongs to, which is the + UUID of its release. + $ref: "#/components/schemas/uuid" + name: + type: string + description: Human-readable name of the artifact. + type: + $ref: "#/components/schemas/artifact-type" + formats: + type: array + description: | + The representations this artifact is published in. A format + may carry a `url` when the publisher hosts the bytes itself; + otherwise they are uploaded to this server afterwards. + items: + $ref: "#/components/schemas/artifact-format" + accessPolicy: + $ref: "#/components/schemas/access-policy" + required: + - collection + - name + - formats + update-artifact: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + description: | + Metadata only. Stored bytes are immutable — replacing content + means a new format or a new artifact, not an overwrite. + properties: + name: + type: string + type: + $ref: "#/components/schemas/artifact-type" + accessPolicy: + $ref: "#/components/schemas/access-policy" + +tags: + - name: TEA Publication + description: | + Operations that create, update and delete the objects the consumption API + serves. + - name: TEA Access Policy + description: | + Operations controlling who may read an object through the consumption + API: the publishing organisation alone, named counterparties, or anyone. diff --git a/spec/publisher/package-lock.json b/spec/publisher/package-lock.json new file mode 100644 index 0000000..f93e822 --- /dev/null +++ b/spec/publisher/package-lock.json @@ -0,0 +1,3225 @@ +{ + "name": "tea-publisher-spec", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tea-publisher-spec", + "devDependencies": { + "@redocly/cli": "^1.34.5", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz", + "integrity": "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.53.0.tgz", + "integrity": "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.26.0.tgz", + "integrity": "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.26.0.tgz", + "integrity": "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.26.0.tgz", + "integrity": "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.26.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/propagator-b3": "1.26.0", + "@opentelemetry/propagator-jaeger": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/cli": { + "version": "1.34.18", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.34.18.tgz", + "integrity": "sha512-xYt83IDBsQiw+eK0tSYMSjEsOdKpfP4jX1evma6nlsZdW+pYX0VUZDI1GdxngDDAH1gkpruJXSsPdUnIghtV9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-node": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0", + "@redocly/config": "0.22.0", + "@redocly/openapi-core": "1.34.18", + "@redocly/respect-core": "1.34.18", + "abort-controller": "3.0.0", + "chokidar": "3.5.3", + "colorette": "1.4.0", + "core-js": "3.32.1", + "dotenv": "16.4.7", + "form-data": "4.0.6", + "get-port-please": "3.0.1", + "glob": "7.2.3", + "handlebars": "4.7.9", + "js-yaml": "4.3.0", + "mobx": "6.12.3", + "pluralize": "8.0.0", + "react": "^17.0.0 || ^18.2.0 || ^19.2.1", + "react-dom": "^17.0.0 || ^18.2.0 || ^19.2.1", + "redoc": "2.5.0", + "semver": "7.7.4", + "simple-websocket": "9.1.0", + "styled-components": "6.4.1", + "yargs": "17.0.1" + }, + "bin": { + "openapi": "bin/cli.js", + "redocly": "bin/cli.js" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/cli/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.18", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.18.tgz", + "integrity": "sha512-UyKIm0wTPw5BcY7Z2PkbK1Ma260um96LSBWXHrdSMe+ZV0EPMyDfAcUcjjm3qEiGST9OK/1TriekdPCZkn4Q3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.0", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@redocly/respect-core": { + "version": "1.34.18", + "resolved": "https://registry.npmjs.org/@redocly/respect-core/-/respect-core-1.34.18.tgz", + "integrity": "sha512-gMF4o3oscRFUcHtPTT3YP52sSMDyFYiMexOV/tZGvoJSZlh7h1G6SBveDpfHCYBk9iaUdhCEFWve3YHJk3LlRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@faker-js/faker": "7.6.0", + "@redocly/ajv": "8.11.2", + "@redocly/openapi-core": "1.34.18", + "better-ajv-errors": "1.2.0", + "colorette": "2.0.20", + "concat-stream": "2.0.0", + "cookie": "0.7.2", + "dotenv": "16.4.7", + "form-data": "4.0.6", + "jest-diff": "29.7.0", + "jest-matcher-utils": "29.7.0", + "js-yaml": "4.3.0", + "json-pointer": "0.6.2", + "jsonpath-plus": "10.3.0", + "open": "10.1.0", + "openapi-sampler": "1.7.0", + "outdent": "0.8.0", + "set-cookie-parser": "2.7.1", + "undici": "6.27.0" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/respect-core/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/respect-core/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-ajv-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-1.2.0.tgz", + "integrity": "sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "@humanwhocodes/momoa": "^2.0.2", + "chalk": "^4.1.2", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0 < 4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "dev": true, + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-js": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.1.tgz", + "integrity": "sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decko": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz", + "integrity": "sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fast-xml-builder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port-please": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.0.1.tgz", + "integrity": "sha512-R5pcVO8Z1+pVDu8Ml3xaJCEkBiiy1VQN9za0YqH8GIi1nIqD4IzQhzY6dDzMRtdS1lyiGlucRzm8IN8wtLIXng==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/json-pointer": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", + "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "foreach": "^2.0.4" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpath-plus": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mobx": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.12.3.tgz", + "integrity": "sha512-c8NKkO4R2lShkSXZ2Ongj1ycjugjzFFo/UswHBnS62y07DMcTc9Rvo03/3nRyszIvwPNljlkd4S828zIBv/piw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.2.2.tgz", + "integrity": "sha512-ShszmQzR/VrhU3M0cQ7DA/s8qNcLcF2emSuudJ/TnDILS3C1Im48mdaG6CpyjZHy8+WqgXUCC9mPqSRIwPPMuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mobx-react-lite": "^4.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.1.tgz", + "integrity": "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-sampler": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.7.0.tgz", + "integrity": "sha512-fWq32F5vqGpgRJYIarC/9Y1wC9tKnRDcCOjsDJ7MIcSv2HsE7kNifcXIZ8FVtNStBUWxYrEk/MKqVF0SwZ5gog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.7", + "fast-xml-parser": "^5.3.4", + "json-pointer": "0.6.2" + } + }, + "node_modules/outdent": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/perfect-scrollbar": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.6.tgz", + "integrity": "sha512-rixgxw3SxyJbCaSpo1n35A/fwI1r2rdwMKOTCg/AcG+xOEyZcE8UHVjpZMFCVImzsFoCZeJTT+M/rdEIQYO2nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-tabs": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.1.1.tgz", + "integrity": "sha512-CPiuKoMFf89B7QlbFfdBD9XmUWiE3qudQputMVZB8GQvPJZRX/gqjDaDWOPDwGinEfpJKEuBCkGt83Tt4efeyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redoc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.5.0.tgz", + "integrity": "sha512-NpYsOZ1PD9qFdjbLVBZJWptqE+4Y6TkUuvEOqPUmoH7AKOmPcE+hYjotLxQNTqVoWL4z0T2uxILmcc8JGDci+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.4.0", + "classnames": "^2.3.2", + "decko": "^1.2.0", + "dompurify": "^3.2.4", + "eventemitter3": "^5.0.1", + "json-pointer": "^0.6.2", + "lunr": "^2.3.9", + "mark.js": "^8.11.1", + "marked": "^4.3.0", + "mobx-react": "^9.1.1", + "openapi-sampler": "^1.5.0", + "path-browserify": "^1.0.1", + "perfect-scrollbar": "^1.5.5", + "polished": "^4.2.2", + "prismjs": "^1.29.0", + "prop-types": "^15.8.1", + "react-tabs": "^6.0.2", + "slugify": "~1.4.7", + "stickyfill": "^1.1.1", + "swagger2openapi": "^7.0.8", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=6.9", + "npm": ">=3.0.0" + }, + "peerDependencies": { + "core-js": "^3.1.4", + "mobx": "^6.0.4", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "styled-components": "^4.1.1 || ^5.1.1 || ^6.0.5" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "dev": true, + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/simple-websocket": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz", + "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "queue-microtask": "^1.2.2", + "randombytes": "^2.1.0", + "readable-stream": "^3.6.0", + "ws": "^7.4.2" + } + }, + "node_modules/slugify": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.7.tgz", + "integrity": "sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stickyfill": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stickyfill/-/stickyfill-1.1.1.tgz", + "integrity": "sha512-GCp7vHAfpao+Qh/3Flh9DXEJ/qSi0KJwJw6zYlZOtRYXWUIpMM6mC2rIep/dK8RQqwW0KxGJIllmjPIBOGN8AA==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/styled-components": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.1.tgz", + "integrity": "sha512-ADu2dF53esUzzM4I0ewxhxFtsDd6v4V6dNkg3vG0iFKhnt06sJneTZnRvujAosZwW0XD58IKgGMQoqri4wHRqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "stylis": "4.3.6" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "css-to-react-native": ">= 3.2.0", + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-native": ">= 0.68.0" + }, + "peerDependenciesMeta": { + "css-to-react-native": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "dev": true, + "license": "BSD" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", + "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + } + } +} diff --git a/spec/publisher/package.json b/spec/publisher/package.json new file mode 100644 index 0000000..b2fe786 --- /dev/null +++ b/spec/publisher/package.json @@ -0,0 +1,15 @@ +{ + "name": "tea-publisher-spec", + "private": true, + "type": "module", + "description": "Builds spec/publisher/openapi.yaml from the consumption specification plus a publication overlay.", + "scripts": { + "build": "node build.mjs", + "check": "node build.mjs --check", + "lint": "redocly lint openapi.yaml" + }, + "devDependencies": { + "@redocly/cli": "^1.34.5", + "js-yaml": "^4.1.0" + } +} From 2164536b2b84ec4b5a9a426ec9c347439b7cc024 Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 15:31:44 +1000 Subject: [PATCH 13/17] feat(publisher): specify distribution publication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createTeaDistribution and deleteTeaDistribution, on the component release because that is where the consumption schema puts release.distributions. distributionId is absent from the request body by design: the server derives it, and a publisher-supplied identifier would let one tenant claim another's. identifiers stays optional — Homebrew and Scoop have no accepted purl type, and an identifier naming a coordinate that does not resolve is worse than none, because a consumer will follow it. Regenerated openapi.yaml. Redocly reports the same 24 errors as before, all inherited from the upstream consumption specification. Signed-off-by: Christopher Langton --- spec/publisher/openapi.yaml | 124 +++++++++++++++++++++++++++++++++ spec/publisher/overlay.yaml | 134 ++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 8b91747..1d82811 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -883,6 +883,70 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + /componentRelease/{uuid}/distribution: + post: + summary: Declare a distribution of a component release + operationId: createTeaDistribution + description: | + Declare one delivered form of this release: a file to download, or a + channel a user installs from. + + Idempotent on the `url`, or on the `description` where a channel has no + single file to fetch — `brew install`, `scoop install`, `nix run`. A + pipeline that retries therefore updates the link rather than leaving a + consumer to choose between two identical ones. + + At least one of `url` and `description` is required. A distribution + with neither is a UUID: it gives a consumer nothing to follow and + nothing to recognise. + + Checksums are recorded verbatim. This server does not hold the bytes a + distribution points at, so it cannot verify them and does not claim to + have — which is why a publisher should copy the digests from the same + manifest its users verify against rather than recomputing them. + parameters: + - $ref: "#/components/parameters/object-uuid" + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-distribution" + responses: + "201": + $ref: "#/components/responses/distribution-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /distribution/{uuid}: + delete: + summary: Withdraw a distribution + operationId: deleteTeaDistribution + description: | + Stop advertising this download. + + It does not unpublish whatever the URL pointed at, and it does not + reach anyone who already resolved it. Withdrawing a link is a statement + about what this catalogue advertises from now on, not a recall. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication /artifact: post: summary: Create a TEA Artifact @@ -2456,6 +2520,12 @@ components: application/json: schema: $ref: "#/components/schemas/artifact" + distribution-written: + description: The distribution as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/release-distribution" access-policy-read: description: The access policy in force for the object. content: @@ -2724,6 +2794,60 @@ components: $ref: "#/components/schemas/access-policy" required: - updateReason + write-distribution: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + description: | + `distributionId` is absent by design: the server assigns it, and + a publisher-supplied identifier would let one tenant claim + another's. It is derived from the release and the natural key + below, so re-publishing the same download is an update. + properties: + description: + type: string + description: | + What this channel or format is, in terms a human choosing how + to install would recognise. Required when there is no `url`, + and used as the natural key in that case. + url: + type: string + format: url + description: | + Direct download URL. Must be absolute and http(s): a relative + path would resolve against the consumer's own origin, and + other schemes ask a consumer to do something it did not + request. + signatureUrl: + type: string + format: url + description: Direct download URL for the detached signature. + identifiers: + type: array + description: | + Identifiers for this distribution specifically. Optional + because several real channels have no accepted purl type — + Homebrew and Scoop among them — and an identifier that names + a coordinate which does not resolve is worse than none, since + a consumer will follow it. + items: + $ref: "#/components/schemas/identifier" + checksums: + type: array + description: | + Recorded verbatim. This server does not hold the bytes and + cannot verify them, so these are the publisher's assertion, + not ours. + items: + $ref: "#/components/schemas/checksum" + anyOf: + - required: + - url + - required: + - description write-artifact: required: true content: diff --git a/spec/publisher/overlay.yaml b/spec/publisher/overlay.yaml index 620f24c..9534d76 100644 --- a/spec/publisher/overlay.yaml +++ b/spec/publisher/overlay.yaml @@ -330,6 +330,82 @@ paths: tags: - TEA Publication + # ── Distributions ───────────────────────────────────────────────────────── + # + # A collection says what evidence exists about a release. A distribution says + # how the release is obtained. Publishing the first without the second leaves + # a consumer that resolved a TEI holding an SBOM and no download link. + # + # Attached to a component release because that is where the consumption + # schema defines them: `release.distributions` exists, `productRelease` has no + # such field. A publisher wanting download links therefore decomposes its + # product into at least one component — the model the consumption + # specification already assumes, not an extra requirement of this overlay. + /componentRelease/{uuid}/distribution: + post: + summary: Declare a distribution of a component release + operationId: createTeaDistribution + description: | + Declare one delivered form of this release: a file to download, or a + channel a user installs from. + + Idempotent on the `url`, or on the `description` where a channel has no + single file to fetch — `brew install`, `scoop install`, `nix run`. A + pipeline that retries therefore updates the link rather than leaving a + consumer to choose between two identical ones. + + At least one of `url` and `description` is required. A distribution + with neither is a UUID: it gives a consumer nothing to follow and + nothing to recognise. + + Checksums are recorded verbatim. This server does not hold the bytes a + distribution points at, so it cannot verify them and does not claim to + have — which is why a publisher should copy the digests from the same + manifest its users verify against rather than recomputing them. + parameters: + - $ref: "#/components/parameters/object-uuid" + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-distribution" + responses: + "201": + $ref: "#/components/responses/distribution-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + "409": + $ref: "#/components/responses/publisher-conflict" + tags: + - TEA Publication + /distribution/{uuid}: + delete: + summary: Withdraw a distribution + operationId: deleteTeaDistribution + description: | + Stop advertising this download. + + It does not unpublish whatever the URL pointed at, and it does not + reach anyone who already resolved it. Withdrawing a link is a statement + about what this catalogue advertises from now on, not a recall. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + # ── Artifacts ───────────────────────────────────────────────────────────── /artifact: post: @@ -941,6 +1017,12 @@ components: application/json: schema: $ref: "#/components/schemas/artifact" + distribution-written: + description: The distribution as the consumption API will serve it. + content: + application/json: + schema: + $ref: "#/components/schemas/release-distribution" access-policy-read: description: The access policy in force for the object. content: @@ -1053,6 +1135,58 @@ components: $ref: "#/components/schemas/access-policy" required: - updateReason + write-distribution: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + description: | + `distributionId` is absent by design: the server assigns it, and + a publisher-supplied identifier would let one tenant claim + another's. It is derived from the release and the natural key + below, so re-publishing the same download is an update. + properties: + description: + type: string + description: | + What this channel or format is, in terms a human choosing how + to install would recognise. Required when there is no `url`, + and used as the natural key in that case. + url: + type: string + format: url + description: | + Direct download URL. Must be absolute and http(s): a relative + path would resolve against the consumer's own origin, and + other schemes ask a consumer to do something it did not + request. + signatureUrl: + type: string + format: url + description: Direct download URL for the detached signature. + identifiers: + type: array + description: | + Identifiers for this distribution specifically. Optional + because several real channels have no accepted purl type — + Homebrew and Scoop among them — and an identifier that names + a coordinate which does not resolve is worse than none, since + a consumer will follow it. + items: + $ref: "#/components/schemas/identifier" + checksums: + type: array + description: | + Recorded verbatim. This server does not hold the bytes and + cannot verify them, so these are the publisher's assertion, + not ours. + items: + $ref: "#/components/schemas/checksum" + anyOf: + - required: [url] + - required: [description] write-artifact: required: true content: From 738add1171ec5d9aac237e4cc84a55cc8032de90 Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 19:32:56 +1000 Subject: [PATCH 14/17] feat(publisher): close the gaps an implementation actually hits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited the overlay against a working TEA server and its console — Vulnetix Resolve — rather than against itself. Five things the specification could not express, each of which the implementation had to answer on its own. A product release could not state its components. The consumption API lists `components` among productRelease's required members, so every release a conformant publisher could create had to be served with an empty list: a claim about the product rather than a gap in the record. An artifact could not say which distributions it describes. `distributionIds` exists in the consumption artifact and was unreachable from the publication side, which matters exactly where an SBOM is not one document. There was no publisher read surface, and the consumption API cannot substitute for one: it answers what a reader is entitled to see, so an object correctly withheld and an object that was never created are indistinguishable from outside. `GET /publications` and `/publications/{uuid}/releases` report what was published alongside the policy in force for it. `public` asked for "a separate confirmation" without naming one, so the implementation invented `?confirm=public`. An unspecified requirement is one every publisher satisfies differently, which is the failure this document exists to prevent, so the parameter is now specified. `publishTo` named targets nothing could configure and left failure semantics undefined. Targets are now registered through `/publicationTargets`, mirroring is asynchronous and never blocks the local write, per-target state is reported on the access policy, and a server that does not mirror answers `MIRRORING_UNSUPPORTED` instead of accepting the request and discarding it. One change to the consumption specification: an empty alternative in the global `security`. Without it `visibility: public` cannot mean what it says, because discovery has no step at which a credential could be obtained and a server that answers an anonymous request is non-conformant for doing so. It is a widening, so nothing conformant breaks. build.mjs gives every overlay operation an explicit credential requirement in return, so anonymous writes stay non-conformant. Redocly still reports the same 24 errors as the consumption specification alone; the overlay adds none. Signed-off-by: Christopher Langton --- spec/openapi.yaml | 16 ++ spec/publisher/README.md | 69 ++++- spec/publisher/build.mjs | 27 +- spec/publisher/openapi.yaml | 550 +++++++++++++++++++++++++++++++++++- spec/publisher/overlay.yaml | 499 +++++++++++++++++++++++++++++++- 5 files changed, 1126 insertions(+), 35 deletions(-) diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 79f85c8..d615b13 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -1754,6 +1754,22 @@ components: security: - bearerAuth: [] - basicAuth: [] + # Authentication is optional, not absent. + # + # Every operation in this document is a read, and discovery has no step at + # which a credential could be obtained: a consumer takes the domain out of a + # TEI, fetches that host's discovery document, and calls the root it names. + # Requiring a credential of all three would close the only entry point the + # specification defines, so a server MUST be able to answer an anonymous + # request. What it answers with is a separate question — an object the caller + # is not entitled to is `OBJECT_UNKNOWN`, exactly as it is for an object that + # does not exist. + # + # A caller that does present a credential has asserted an identity, and a + # server MUST verify it rather than quietly downgrading to anonymous: an + # expired key that silently returns only public data is indistinguishable + # from a publisher having withdrawn everything. + - {} tags: - name: TEA Product - name: TEA Product Release diff --git a/spec/publisher/README.md b/spec/publisher/README.md index 04fe921..e8ae8c5 100644 --- a/spec/publisher/README.md +++ b/spec/publisher/README.md @@ -59,6 +59,15 @@ owns. That is the drift check with teeth: the overlay may only add. field OpenAPI defines, so the three `$ref`s pointing into it never resolved and every delete declared no responses at all. A generator reading the old draft produced deletes that returned nothing. +- **A product release can state its components.** The consumption API lists + `components` among `productRelease`'s required members, and the draft's + create body had no way to set it — so every release a conformant publisher + could produce had to be served with an empty list, which is a claim about the + product rather than a gap in the record. +- **An artifact can say which distributions it describes.** `distributionIds` + exists in the consumption `artifact` and was unreachable from the publication + API. It matters where an SBOM is not one document: a Windows installer and a + source tarball of the same release have different contents. ## What the publication overlay adds @@ -109,8 +118,10 @@ servers. Two rules carry the weight: guarantee nothing, because any artifact beneath it could quietly be made public. - **`public` is one-way in practice.** No later request recalls what has already - been fetched, so a server should require a separate confirmation for that - transition rather than treating it as an ordinary field update. + been fetched, so that transition requires `confirm=public` rather than being + an ordinary field update. The mechanism is named, not merely asked for: an + unspecified requirement is one every publisher satisfies differently, which + is the failure this document exists to avoid. This needs no new consumer-side behaviour: a consumer denied by the policy gets the consumption specification's existing `OBJECT_NOT_SHAREABLE`, which is @@ -121,6 +132,28 @@ already in `unknown-error-type`. and effective is where accidental disclosure hides, so it is reported rather than left to be reconstructed. +Mirroring to other TEA servers is `publishTo`, naming targets registered +through `/publicationTargets`. Registration is separate from use because a +credential is involved, and because handing an object to another server is a +decision that outlives the object — once a copy lands there, this server's +policy no longer governs it. Mirroring is asynchronous and never blocks the +local write: refusing to record a publisher's own release because a mirror is +unreachable would make every target a single point of failure for publication +itself. State is reported per target on the object's access policy, and a +server that does not mirror answers `MIRRORING_UNSUPPORTED` rather than +accepting the request and doing nothing. + +### The publisher read surface + +`GET /publications` and `GET /publications/{uuid}/releases` report what this +organisation has published together with the policy in force for each. + +The consumption API cannot answer this. It answers what a *reader* is entitled +to see, and the thing a publisher most needs to verify — that something private +really is private — is exactly what a consumption response cannot show, because +an object correctly withheld and an object that was never created look +identical from outside. + ### Idempotency `Idempotency-Key` on creates. Publication runs in pipelines, and pipelines @@ -141,13 +174,33 @@ These are worth fixing in the consumption specification rather than papering over here, since patching them in this fork would put it at odds with upstream on a file upstream owns. +## The one change this makes to the consumption specification + +`spec/openapi.yaml` gains an empty alternative in its global `security`, so +authentication is optional rather than mandatory. + +Without it the publication API cannot mean what it says. `visibility: public` +is defined as "readable without authentication", and the discovery sequence a +consumer follows — take the domain out of a TEI, fetch that host's discovery +document, call the root it names — has no step at which a credential could be +obtained. A specification that requires one on every operation closes the only +entry point it defines, and any server that actually serves a public object +anonymously is then non-conformant for doing the right thing. + +It is a widening, so no conformant client or server is broken by it: a server +may still refuse every anonymous request, and one that answers is now allowed +to. The publication operations are unaffected — `build.mjs` gives every +operation the overlay contributes an explicit `bearerAuth`/`basicAuth` +requirement, so anonymous writes are never conformant. + ## Open questions -- **No publisher-side read surface.** A publisher reconciling state has to go - through the consumption API, which by design will not show it anything - private — so there is currently no way to list what you have published. -- **`publishTo` does not define failure semantics.** Whether a failed mirror - should block the local publication is unspecified. - **Artifact versions.** The consumption API addresses artifacts as `/artifact/{uuid}/{artifactVersion}`, but the overlay's create and update do - not yet say how a publisher advances that version. + not yet say how a publisher advances that version. The rule is probably that + content is immutable and a new revision is a new version, but "probably" is + not a specification. +- **Mirror authentication is one-directional.** A target is registered with a + credential this server presents. Nothing says how the receiving server + decides whether to accept a mirrored object, or how it records that the + object came from elsewhere rather than being published to it directly. diff --git a/spec/publisher/build.mjs b/spec/publisher/build.mjs index 7acddc3..445a825 100644 --- a/spec/publisher/build.mjs +++ b/spec/publisher/build.mjs @@ -43,9 +43,34 @@ const overlay = yaml.load(readFileSync(overlayPath, 'utf8')) */ const collisions = [] +// The consumption document's global `security` now offers an anonymous +// alternative, because every operation in it is a read and discovery has no +// step at which a credential could be obtained. +// +// That must not reach the publication operations. Inheriting the global list +// would make anonymous writes conformant, which is the opposite of what the +// access policy exists to say — so every operation the overlay contributes +// declares its own requirement here. Stated once, rather than repeated on +// twenty operations where one omission would be a silent hole. +const WRITE_SECURITY = [{ bearerAuth: [] }, { basicAuth: [] }] + +// A path item also holds `parameters`, `summary` and `$ref`, none of which take +// a security requirement. +const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']) + +function requireCredential(method, op) { + // An overlay operation may still set `security` itself; this only supplies + // the default. + if (HTTP_METHODS.has(method)) op.security ??= WRITE_SECURITY + return op +} + function mergeSection(target, source, path) { for (const [key, value] of Object.entries(source ?? {})) { if (target[key] === undefined) { + if (path === 'paths') { + for (const [method, op] of Object.entries(value)) requireCredential(method, op) + } target[key] = value continue } @@ -59,7 +84,7 @@ function mergeSection(target, source, path) { collisions.push(`${path}.${key}.${method}`) continue } - target[key][method] = op + target[key][method] = requireCredential(method, op) } continue } diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index 1d82811..af31caf 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -81,6 +81,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] delete: summary: Delete a TEA Product description: | @@ -104,6 +107,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /product/{uuid}/releases: get: description: Get releases of the product @@ -173,6 +179,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] delete: summary: Delete a TEA Product Release operationId: deleteTeaProductRelease @@ -190,6 +199,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /productRelease/{uuid}/cle: get: description: Get the CLE (Common Lifecycle Enumeration) data for a TEA Product Release @@ -321,6 +333,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] delete: summary: Delete a TEA Component description: Delete a component and its releases. @@ -338,6 +353,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /component/{uuid}/releases: get: description: Get releases of the component @@ -467,6 +485,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] delete: summary: Delete a TEA Component Release operationId: deleteTeaComponentRelease @@ -484,6 +505,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /componentRelease/{uuid}/cle: get: description: Get the CLE (Common Lifecycle Enumeration) data for a TEA Component Release @@ -765,6 +789,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /component: post: summary: Create a TEA Component @@ -787,6 +814,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /productRelease: post: summary: Create a TEA Product Release @@ -809,6 +839,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /componentRelease: post: summary: Create a TEA Component Release @@ -831,6 +864,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /productRelease/{uuid}/collection: put: summary: Publish a new collection version for a product release @@ -859,6 +895,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /componentRelease/{uuid}/collection: put: summary: Publish a new collection version for a component release @@ -883,6 +922,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /componentRelease/{uuid}/distribution: post: summary: Declare a distribution of a component release @@ -924,6 +966,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /distribution/{uuid}: delete: summary: Withdraw a distribution @@ -947,6 +992,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /artifact: post: summary: Create a TEA Artifact @@ -978,6 +1026,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /artifact/{uuid}: patch: summary: Update a TEA Artifact @@ -1005,6 +1056,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] delete: summary: Delete a TEA Artifact operationId: deleteTeaArtifact @@ -1022,6 +1076,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /artifact/{uuid}/format/{formatIndex}/content: put: summary: Upload artifact content @@ -1078,6 +1135,9 @@ paths: $ref: "#/components/responses/publisher-unsupported-media-type" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /artifact/{uuid}/format/{formatIndex}/signature: put: summary: Attach a detached signature to an artifact format @@ -1114,6 +1174,182 @@ paths: $ref: "#/components/responses/publisher-unsupported-media-type" tags: - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] + /publicationTargets: + get: + summary: List registered publication targets + operationId: listTeaPublicationTargets + description: The remote servers this organisation may mirror objects to. + parameters: + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-publication-target" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "501": + $ref: "#/components/responses/publisher-mirroring-unsupported" + tags: + - TEA Publication Target + security: + - bearerAuth: [] + - basicAuth: [] + post: + summary: Register a publication target + operationId: createTeaPublicationTarget + description: | + Register a remote TEA server this organisation may mirror to. + + A server SHOULD verify the target before accepting it — that the root + answers, advertises a compatible version, and accepts the credential — + so that a target which was never going to work fails here, in front of + whoever configured it, rather than later inside an asynchronous mirror + nobody is watching. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-publication-target" + responses: + "201": + $ref: "#/components/responses/publication-target-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + "501": + $ref: "#/components/responses/publisher-mirroring-unsupported" + tags: + - TEA Publication Target + security: + - bearerAuth: [] + - basicAuth: [] + /publicationTargets/{uuid}: + get: + summary: Read a publication target + operationId: getTeaPublicationTarget + description: The target as registered. The credential is never returned. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "200": + $ref: "#/components/responses/publication-target-written" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication Target + security: + - bearerAuth: [] + - basicAuth: [] + delete: + summary: Deregister a publication target + operationId: deleteTeaPublicationTarget + description: | + Stop mirroring to this target and discard its credential. + + It does not recall the copies already sent. The target holds them under + its own policy, which is what mirroring means, so removing a target is + a statement about future copies only. + + A server MUST also remove the target from any `publishTo` naming it, + rather than leaving policies pointing at a target that no longer + exists. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication Target + security: + - bearerAuth: [] + - basicAuth: [] + /publications: + get: + summary: List the products this organisation has published + operationId: listTeaPublications + description: | + The caller's own products, each with the access policy actually in + force and the number of releases beneath it. + + Paginated on the same terms as the consumption listings, because a + catalogue large enough to be worth reconciling is large enough to need + it. + parameters: + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-publication" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + tags: + - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] + /publications/{uuid}/releases: + get: + summary: List the releases published under a product + operationId: listTeaPublishedReleases + description: | + The releases of one of the caller's own products, each with the policy + in force, the current collection version and how many artifacts it + holds. + + The collection version is what makes this worth having separately from + the consumption listing: a release whose collection is still at version + 0 has been created and never populated, which is invisible to a + consumer and is exactly the state a publisher wants to find. + parameters: + - $ref: "#/components/parameters/object-uuid" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product-release" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-published-release" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + security: + - bearerAuth: [] + - basicAuth: [] /accessPolicy/{uuid}: get: summary: Read the access policy in force for an object @@ -1137,6 +1373,9 @@ paths: $ref: "#/components/responses/publisher-not-found" tags: - TEA Access Policy + security: + - bearerAuth: [] + - basicAuth: [] put: summary: Set the access policy for an object operationId: setTeaAccessPolicy @@ -1144,12 +1383,17 @@ paths: Replace the object's own access policy. A server MUST reject a policy wider than the effective policy of the - object's parent, with `ACCESS_WIDENS_PARENT`. A server SHOULD require - a separate confirmation for a change to `public`, because no later + object's parent, with `ACCESS_WIDENS_PARENT`. + + A change to `public` MUST carry `confirm=public`, because no later request undoes it: whatever was fetched while it was public stays - fetched. + fetched. Naming the mechanism rather than only asking for "a + confirmation" is deliberate — an unspecified requirement is one every + publisher satisfies differently, which is the failure this document + exists to avoid. parameters: - $ref: "#/components/parameters/object-uuid" + - $ref: "#/components/parameters/confirm-public" requestBody: required: true content: @@ -1171,6 +1415,9 @@ paths: $ref: "#/components/responses/publisher-conflict" tags: - TEA Access Policy + security: + - bearerAuth: [] + - basicAuth: [] components: schemas: date-time: @@ -2170,6 +2417,8 @@ components: - PAYLOAD_TOO_LARGE - CHECKSUM_MISMATCH - SIGNATURE_INVALID + - MIRRORING_UNSUPPORTED + - UNKNOWN_PUBLICATION_TARGET publisher-error-response: type: object description: | @@ -2243,11 +2492,20 @@ components: publishTo: type: array description: | - Remote TEA servers this object is mirrored to. Mirroring is a copy, - not a reference: once an object reaches another server, this - server's policy no longer governs it. + Registered publication targets this object is mirrored to, by + UUID. Mirroring is a copy, not a reference: once an object reaches + another server, this server's policy no longer governs it, which is + why a target has to be registered deliberately rather than named + inline by URL. + + A server that does not mirror MUST reject a non-empty list with + `MIRRORING_UNSUPPORTED`. Accepting it and doing nothing is the one + unacceptable response, because the publisher would then believe a + copy exists somewhere it does not. + + Naming a target that is not registered is `UNKNOWN_PUBLICATION_TARGET`. items: - $ref: "#/components/schemas/publication-target" + $ref: "#/components/schemas/uuid" required: - visibility share-grant: @@ -2273,21 +2531,75 @@ components: - organizationUuid publication-target: type: object - description: A remote TEA server this publisher mirrors objects to. + description: | + A remote TEA server this publisher mirrors objects to, as this server + holds it. + + Registered as its own object rather than named inline in a policy for + two reasons. A credential is involved, and a credential repeated in + every policy is a credential in every audit log. And mirroring is a + copy the receiving server then governs, so which servers a publisher is + willing to hand objects to is a decision worth making once and + reviewing, not one made in passing while setting a visibility. additionalProperties: false properties: uuid: - description: Identifier of a target already configured on this server. + description: Identifier of this target on this server. $ref: "#/components/schemas/uuid" + rootUrl: + type: string + format: uri + description: | + The remote server's TEA root, without a trailing slash — the same + value that server advertises as `rootUrl` in its own discovery + document. domain: type: string description: | The TEI domain-name component of the target, which is what a - consumer resolves. Reported so a publisher can see where a copy - went without holding the target's credentials. + consumer resolves. Derived from `rootUrl` and reported so a + publisher can see where a copy went. + description: + type: string + description: What this target is, for whoever reviews the list later. + credential: + type: string + writeOnly: true + description: | + Bearer credential this server presents when publishing to the + target. Write-only: a server MUST NOT return it, and MUST NOT + report whether one is held in any form that distinguishes one + credential from another. + createdAt: + $ref: "#/components/schemas/date-time" + required: + - uuid + - rootUrl + mirror-status: + type: object + description: | + The state of mirroring one object to one target. + + Reported rather than set: a publisher states where an object should go + and the server reports how far it got. A publisher that cannot see a + failed mirror has to discover it from the counterparty. + additionalProperties: false + properties: + target: + description: The publication target this state refers to. + $ref: "#/components/schemas/uuid" + domain: + type: string + description: The target's TEI domain, repeated so a reader need not resolve the UUID. state: type: string - description: State of the most recent mirroring attempt for this object. + description: | + State of the most recent attempt. + + `pending` — accepted locally, not yet copied. `published` — the + target acknowledged it. `failed` — the last attempt did not + succeed and `detail` says why; the server SHOULD retry, and MUST + NOT silently give up without leaving this state visible. enum: - pending - published @@ -2298,7 +2610,8 @@ components: type: string description: Why the last attempt failed, when it did. required: - - uuid + - target + - state artifact-signature: type: object description: | @@ -2371,9 +2684,99 @@ components: declared here. Naming it lets a publisher fix the cause rather than the symptom. $ref: "#/components/schemas/uuid" + mirrors: + type: array + description: | + How far mirroring has got, per target. Absent where the object is + mirrored nowhere, or where the server does not mirror at all. + items: + $ref: "#/components/schemas/mirror-status" required: - uuid - effective + publication: + type: object + description: | + One product this organisation has published, as the consumption API + serves it, alongside the access policy in force for it. + + The product is carried whole rather than flattened so that a publisher + can compare it byte for byte against what `GET /product/{uuid}` + returns; a summary in a different shape would have to be trusted rather + than checked. + additionalProperties: false + properties: + product: + $ref: "#/components/schemas/product" + accessPolicy: + description: Declared and effective policy, so the gap between them is visible here too. + $ref: "#/components/schemas/access-policy-status" + releaseCount: + type: integer + minimum: 0 + description: How many releases exist beneath this product. + required: + - product + - accessPolicy + published-release: + type: object + description: One release this organisation has published, with what a consumer would find in it. + additionalProperties: false + properties: + release: + $ref: "#/components/schemas/productRelease" + accessPolicy: + $ref: "#/components/schemas/access-policy-status" + collectionVersion: + type: integer + minimum: 0 + description: | + Version of this release's current collection, or 0 where no + collection has been published yet — a release a consumer can + resolve and find nothing in. + artifactCount: + type: integer + minimum: 0 + description: How many artifacts the current collection holds. + required: + - release + - accessPolicy + paginated-publication-target-response: + description: A paginated response containing registered publication targets + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/publication-target" + required: + - results + paginated-publication-response: + description: A paginated response containing this organisation's published products + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/publication" + required: + - results + paginated-published-release-response: + description: A paginated response containing this organisation's published releases + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/published-release" + required: + - results responses: 204-common-delete: description: Object deleted successfully @@ -2472,6 +2875,15 @@ components: application/json: schema: $ref: "#/components/schemas/publisher-error-response" + publisher-mirroring-unsupported: + description: | + This server does not mirror to other TEA servers. Answered rather than + quietly ignoring the request, so a publisher is never left believing a + copy exists somewhere it does not. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" publisher-payload-too-large: description: The uploaded content exceeds the size this server accepts. content: @@ -2532,6 +2944,30 @@ components: application/json: schema: $ref: "#/components/schemas/access-policy-status" + publication-target-written: + description: The publication target as registered, without its credential. + content: + application/json: + schema: + $ref: "#/components/schemas/publication-target" + paginated-publication-target: + description: A paginated response containing registered publication targets. + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-publication-target-response" + paginated-publication: + description: A paginated response containing this organisation's published products. + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-publication-response" + paginated-published-release: + description: A paginated response containing this organisation's published releases. + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-published-release-response" parameters: page-size: name: pageSize @@ -2656,6 +3092,21 @@ components: schema: type: integer minimum: 0 + confirm-public: + name: confirm + in: query + required: false + description: | + Acknowledges an irreversible change. Required, with the value `public`, + on a request that sets `visibility` to `public`; ignored otherwise. + + A server MUST reject such a request without it, so that publishing to + the open web is something a caller states outright rather than + something a mistyped field does on its behalf. + schema: + type: string + enum: + - public idempotency-key: name: Idempotency-Key in: header @@ -2752,6 +3203,21 @@ components: type: array items: $ref: "#/components/schemas/identifier" + components: + type: array + description: | + The components this release is composed of, optionally + pinning a specific component release. + + Present because the consumption specification lists + `components` among `productRelease`'s required members: a + publisher with no way to state it can only ever produce + releases a server has to serve as an empty list, which is a + claim about the product rather than a gap in the record. + + Omitted means unchanged on update, and empty on create. + items: + $ref: "#/components/schemas/component-ref" accessPolicy: $ref: "#/components/schemas/access-policy" write-component-release: @@ -2779,6 +3245,37 @@ components: $ref: "#/components/schemas/identifier" accessPolicy: $ref: "#/components/schemas/access-policy" + write-publication-target: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + description: | + `uuid` and `domain` are absent by design: the server assigns the + first and derives the second from `rootUrl`, so the two cannot + disagree about which server a target actually is. + properties: + rootUrl: + type: string + format: uri + description: | + The remote server's TEA root, without a trailing slash. Must + be an absolute https URL — a mirror carries a credential, and + sending one over http would publish it to the network as well + as to the target. + description: + type: string + description: What this target is, for whoever reviews the list later. + credential: + type: string + writeOnly: true + description: | + Bearer credential to present when publishing to the target. + Never returned by any operation. + required: + - rootUrl write-collection: required: true content: @@ -2874,6 +3371,20 @@ components: otherwise they are uploaded to this server afterwards. items: $ref: "#/components/schemas/artifact-format" + distributionIds: + type: array + description: | + The distributions this artifact describes. Absent or empty + means it applies to all of them, which is what the + consumption specification says of the same field. + + It matters for exactly the case where an SBOM is not one + document: a Windows installer and a source tarball of the + same release have different contents, and an artifact that + cannot say which one it describes is attached to both + incorrectly. + items: + $ref: "#/components/schemas/uuid" accessPolicy: $ref: "#/components/schemas/access-policy" required: @@ -2895,11 +3406,19 @@ components: type: string type: $ref: "#/components/schemas/artifact-type" + distributionIds: + type: array + description: | + Replaces the set of distributions this artifact describes. + An empty array means all of them. + items: + $ref: "#/components/schemas/uuid" accessPolicy: $ref: "#/components/schemas/access-policy" security: - bearerAuth: [] - basicAuth: [] + - {} tags: - name: TEA Product - name: TEA Product Release @@ -2916,6 +3435,11 @@ tags: description: | Operations controlling who may read an object through the consumption API: the publishing organisation alone, named counterparties, or anyone. + - name: TEA Publication Target + description: | + Registration of the remote TEA servers a publisher mirrors objects to. + Mirroring is a copy the receiving server then governs, so the set of + targets is configured deliberately rather than named in passing. externalDocs: description: Transparency Exchange API specification url: https://github.com/CycloneDX/transparency-exchange-api diff --git a/spec/publisher/overlay.yaml b/spec/publisher/overlay.yaml index 9534d76..2593b35 100644 --- a/spec/publisher/overlay.yaml +++ b/spec/publisher/overlay.yaml @@ -575,6 +575,192 @@ paths: tags: - TEA Publication + # ── Publication targets ─────────────────────────────────────────────────── + # + # Where `publishTo` sends a copy. Registration is separate from use because a + # credential is involved and because handing an object to another server is a + # decision that outlives the object: once a copy lands there, this server's + # access policy no longer governs it. + # + # Mirroring is asynchronous and MUST NOT block or fail the local write. A + # remote outage is the target's problem; refusing to record a publisher's own + # release because a mirror is unreachable would make every target a single + # point of failure for publication itself. The state is reported on the + # object's access policy instead, where a publisher can find it. + # + # Mirroring is one hop. A server MUST NOT onward-mirror an object it received + # as a copy, or two servers configured towards each other would exchange the + # same object indefinitely. + /publicationTargets: + get: + summary: List registered publication targets + operationId: listTeaPublicationTargets + description: The remote servers this organisation may mirror objects to. + parameters: + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-publication-target" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "501": + $ref: "#/components/responses/publisher-mirroring-unsupported" + tags: + - TEA Publication Target + post: + summary: Register a publication target + operationId: createTeaPublicationTarget + description: | + Register a remote TEA server this organisation may mirror to. + + A server SHOULD verify the target before accepting it — that the root + answers, advertises a compatible version, and accepts the credential — + so that a target which was never going to work fails here, in front of + whoever configured it, rather than later inside an asynchronous mirror + nobody is watching. + parameters: + - $ref: "#/components/parameters/idempotency-key" + requestBody: + $ref: "#/components/requestBodies/write-publication-target" + responses: + "201": + $ref: "#/components/responses/publication-target-written" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "409": + $ref: "#/components/responses/publisher-conflict" + "501": + $ref: "#/components/responses/publisher-mirroring-unsupported" + tags: + - TEA Publication Target + /publicationTargets/{uuid}: + get: + summary: Read a publication target + operationId: getTeaPublicationTarget + description: The target as registered. The credential is never returned. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "200": + $ref: "#/components/responses/publication-target-written" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication Target + delete: + summary: Deregister a publication target + operationId: deleteTeaPublicationTarget + description: | + Stop mirroring to this target and discard its credential. + + It does not recall the copies already sent. The target holds them under + its own policy, which is what mirroring means, so removing a target is + a statement about future copies only. + + A server MUST also remove the target from any `publishTo` naming it, + rather than leaving policies pointing at a target that no longer + exists. + parameters: + - $ref: "#/components/parameters/object-uuid" + responses: + "204": + $ref: "#/components/responses/object-deleted" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication Target + + # ── Publisher read surface ──────────────────────────────────────────────── + # + # A publisher cannot reconcile its own state through the consumption API, + # because that API answers what a *reader* is entitled to see. The one thing + # a publisher most needs to check — whether something private is in fact + # private — is precisely what a consumption response cannot tell it, since an + # object correctly withheld and an object that was never created look + # identical from outside. + # + # So these two operations report the objects the caller published together + # with the policy in force for each. They are scoped to the caller's own + # organisation and to no other; there is no cross-publisher listing here, + # deliberately. + /publications: + get: + summary: List the products this organisation has published + operationId: listTeaPublications + description: | + The caller's own products, each with the access policy actually in + force and the number of releases beneath it. + + Paginated on the same terms as the consumption listings, because a + catalogue large enough to be worth reconciling is large enough to need + it. + parameters: + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-publication" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + tags: + - TEA Publication + /publications/{uuid}/releases: + get: + summary: List the releases published under a product + operationId: listTeaPublishedReleases + description: | + The releases of one of the caller's own products, each with the policy + in force, the current collection version and how many artifacts it + holds. + + The collection version is what makes this worth having separately from + the consumption listing: a release whose collection is still at version + 0 has been created and never populated, which is invisible to a + consumer and is exactly the state a publisher wants to find. + parameters: + - $ref: "#/components/parameters/object-uuid" + - $ref: "#/components/parameters/page-size" + - $ref: "#/components/parameters/page-token" + - $ref: "#/components/parameters/sort-field-product-release" + - $ref: "#/components/parameters/sort-order" + responses: + "200": + $ref: "#/components/responses/paginated-published-release" + "400": + $ref: "#/components/responses/publisher-bad-request" + "401": + $ref: "#/components/responses/publisher-unauthorized" + "403": + $ref: "#/components/responses/publisher-forbidden" + "404": + $ref: "#/components/responses/publisher-not-found" + tags: + - TEA Publication + # ── Access policy ───────────────────────────────────────────────────────── # Named "access policy" rather than "distribution" because the consumption # specification already uses `release-distribution` for something else @@ -611,12 +797,17 @@ paths: Replace the object's own access policy. A server MUST reject a policy wider than the effective policy of the - object's parent, with `ACCESS_WIDENS_PARENT`. A server SHOULD require - a separate confirmation for a change to `public`, because no later + object's parent, with `ACCESS_WIDENS_PARENT`. + + A change to `public` MUST carry `confirm=public`, because no later request undoes it: whatever was fetched while it was public stays - fetched. + fetched. Naming the mechanism rather than only asking for "a + confirmation" is deliberate — an unspecified requirement is one every + publisher satisfies differently, which is the failure this document + exists to avoid. parameters: - $ref: "#/components/parameters/object-uuid" + - $ref: "#/components/parameters/confirm-public" requestBody: required: true content: @@ -662,6 +853,8 @@ components: - PAYLOAD_TOO_LARGE - CHECKSUM_MISMATCH - SIGNATURE_INVALID + - MIRRORING_UNSUPPORTED + - UNKNOWN_PUBLICATION_TARGET publisher-error-response: type: object @@ -737,11 +930,20 @@ components: publishTo: type: array description: | - Remote TEA servers this object is mirrored to. Mirroring is a copy, - not a reference: once an object reaches another server, this - server's policy no longer governs it. + Registered publication targets this object is mirrored to, by + UUID. Mirroring is a copy, not a reference: once an object reaches + another server, this server's policy no longer governs it, which is + why a target has to be registered deliberately rather than named + inline by URL. + + A server that does not mirror MUST reject a non-empty list with + `MIRRORING_UNSUPPORTED`. Accepting it and doing nothing is the one + unacceptable response, because the publisher would then believe a + copy exists somewhere it does not. + + Naming a target that is not registered is `UNKNOWN_PUBLICATION_TARGET`. items: - $ref: "#/components/schemas/publication-target" + $ref: "#/components/schemas/uuid" required: - visibility @@ -769,21 +971,76 @@ components: publication-target: type: object - description: A remote TEA server this publisher mirrors objects to. + description: | + A remote TEA server this publisher mirrors objects to, as this server + holds it. + + Registered as its own object rather than named inline in a policy for + two reasons. A credential is involved, and a credential repeated in + every policy is a credential in every audit log. And mirroring is a + copy the receiving server then governs, so which servers a publisher is + willing to hand objects to is a decision worth making once and + reviewing, not one made in passing while setting a visibility. additionalProperties: false properties: uuid: - description: Identifier of a target already configured on this server. + description: Identifier of this target on this server. $ref: "#/components/schemas/uuid" + rootUrl: + type: string + format: uri + description: | + The remote server's TEA root, without a trailing slash — the same + value that server advertises as `rootUrl` in its own discovery + document. domain: type: string description: | The TEI domain-name component of the target, which is what a - consumer resolves. Reported so a publisher can see where a copy - went without holding the target's credentials. + consumer resolves. Derived from `rootUrl` and reported so a + publisher can see where a copy went. + description: + type: string + description: What this target is, for whoever reviews the list later. + credential: + type: string + writeOnly: true + description: | + Bearer credential this server presents when publishing to the + target. Write-only: a server MUST NOT return it, and MUST NOT + report whether one is held in any form that distinguishes one + credential from another. + createdAt: + $ref: "#/components/schemas/date-time" + required: + - uuid + - rootUrl + + mirror-status: + type: object + description: | + The state of mirroring one object to one target. + + Reported rather than set: a publisher states where an object should go + and the server reports how far it got. A publisher that cannot see a + failed mirror has to discover it from the counterparty. + additionalProperties: false + properties: + target: + description: The publication target this state refers to. + $ref: "#/components/schemas/uuid" + domain: + type: string + description: The target's TEI domain, repeated so a reader need not resolve the UUID. state: type: string - description: State of the most recent mirroring attempt for this object. + description: | + State of the most recent attempt. + + `pending` — accepted locally, not yet copied. `published` — the + target acknowledged it. `failed` — the last attempt did not + succeed and `detail` says why; the server SHOULD retry, and MUST + NOT silently give up without leaving this state visible. enum: - pending - published @@ -794,7 +1051,8 @@ components: type: string description: Why the last attempt failed, when it did. required: - - uuid + - target + - state artifact-signature: type: object @@ -869,10 +1127,105 @@ components: declared here. Naming it lets a publisher fix the cause rather than the symptom. $ref: "#/components/schemas/uuid" + mirrors: + type: array + description: | + How far mirroring has got, per target. Absent where the object is + mirrored nowhere, or where the server does not mirror at all. + items: + $ref: "#/components/schemas/mirror-status" required: - uuid - effective + publication: + type: object + description: | + One product this organisation has published, as the consumption API + serves it, alongside the access policy in force for it. + + The product is carried whole rather than flattened so that a publisher + can compare it byte for byte against what `GET /product/{uuid}` + returns; a summary in a different shape would have to be trusted rather + than checked. + additionalProperties: false + properties: + product: + $ref: "#/components/schemas/product" + accessPolicy: + description: Declared and effective policy, so the gap between them is visible here too. + $ref: "#/components/schemas/access-policy-status" + releaseCount: + type: integer + minimum: 0 + description: How many releases exist beneath this product. + required: + - product + - accessPolicy + + published-release: + type: object + description: One release this organisation has published, with what a consumer would find in it. + additionalProperties: false + properties: + release: + $ref: "#/components/schemas/productRelease" + accessPolicy: + $ref: "#/components/schemas/access-policy-status" + collectionVersion: + type: integer + minimum: 0 + description: | + Version of this release's current collection, or 0 where no + collection has been published yet — a release a consumer can + resolve and find nothing in. + artifactCount: + type: integer + minimum: 0 + description: How many artifacts the current collection holds. + required: + - release + - accessPolicy + + paginated-publication-target-response: + description: A paginated response containing registered publication targets + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/publication-target" + required: + - results + + paginated-publication-response: + description: A paginated response containing this organisation's published products + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/publication" + required: + - results + + paginated-published-release-response: + description: A paginated response containing this organisation's published releases + allOf: + - $ref: "#/components/schemas/pagination-details" + - type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/published-release" + required: + - results + parameters: object-uuid: name: uuid @@ -900,6 +1253,21 @@ components: schema: type: integer minimum: 0 + confirm-public: + name: confirm + in: query + required: false + description: | + Acknowledges an irreversible change. Required, with the value `public`, + on a request that sets `visibility` to `public`; ignored otherwise. + + A server MUST reject such a request without it, so that publishing to + the open web is something a caller states outright rather than + something a mistyped field does on its behalf. + schema: + type: string + enum: + - public idempotency-key: name: Idempotency-Key in: header @@ -969,6 +1337,15 @@ components: application/json: schema: $ref: "#/components/schemas/publisher-error-response" + publisher-mirroring-unsupported: + description: | + This server does not mirror to other TEA servers. Answered rather than + quietly ignoring the request, so a publisher is never left believing a + copy exists somewhere it does not. + content: + application/json: + schema: + $ref: "#/components/schemas/publisher-error-response" publisher-payload-too-large: description: The uploaded content exceeds the size this server accepts. content: @@ -1029,6 +1406,30 @@ components: application/json: schema: $ref: "#/components/schemas/access-policy-status" + publication-target-written: + description: The publication target as registered, without its credential. + content: + application/json: + schema: + $ref: "#/components/schemas/publication-target" + paginated-publication-target: + description: A paginated response containing registered publication targets. + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-publication-target-response" + paginated-publication: + description: A paginated response containing this organisation's published products. + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-publication-response" + paginated-published-release: + description: A paginated response containing this organisation's published releases. + content: + application/json: + schema: + $ref: "#/components/schemas/paginated-published-release-response" requestBodies: write-product: @@ -1093,6 +1494,21 @@ components: type: array items: $ref: "#/components/schemas/identifier" + components: + type: array + description: | + The components this release is composed of, optionally + pinning a specific component release. + + Present because the consumption specification lists + `components` among `productRelease`'s required members: a + publisher with no way to state it can only ever produce + releases a server has to serve as an empty list, which is a + claim about the product rather than a gap in the record. + + Omitted means unchanged on update, and empty on create. + items: + $ref: "#/components/schemas/component-ref" accessPolicy: $ref: "#/components/schemas/access-policy" write-component-release: @@ -1120,6 +1536,37 @@ components: $ref: "#/components/schemas/identifier" accessPolicy: $ref: "#/components/schemas/access-policy" + write-publication-target: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + description: | + `uuid` and `domain` are absent by design: the server assigns the + first and derives the second from `rootUrl`, so the two cannot + disagree about which server a target actually is. + properties: + rootUrl: + type: string + format: uri + description: | + The remote server's TEA root, without a trailing slash. Must + be an absolute https URL — a mirror carries a credential, and + sending one over http would publish it to the network as well + as to the target. + description: + type: string + description: What this target is, for whoever reviews the list later. + credential: + type: string + writeOnly: true + description: | + Bearer credential to present when publishing to the target. + Never returned by any operation. + required: + - rootUrl write-collection: required: true content: @@ -1213,6 +1660,20 @@ components: otherwise they are uploaded to this server afterwards. items: $ref: "#/components/schemas/artifact-format" + distributionIds: + type: array + description: | + The distributions this artifact describes. Absent or empty + means it applies to all of them, which is what the + consumption specification says of the same field. + + It matters for exactly the case where an SBOM is not one + document: a Windows installer and a source tarball of the + same release have different contents, and an artifact that + cannot say which one it describes is attached to both + incorrectly. + items: + $ref: "#/components/schemas/uuid" accessPolicy: $ref: "#/components/schemas/access-policy" required: @@ -1234,6 +1695,13 @@ components: type: string type: $ref: "#/components/schemas/artifact-type" + distributionIds: + type: array + description: | + Replaces the set of distributions this artifact describes. + An empty array means all of them. + items: + $ref: "#/components/schemas/uuid" accessPolicy: $ref: "#/components/schemas/access-policy" @@ -1246,3 +1714,8 @@ tags: description: | Operations controlling who may read an object through the consumption API: the publishing organisation alone, named counterparties, or anyone. + - name: TEA Publication Target + description: | + Registration of the remote TEA servers a publisher mirrors objects to. + Mirroring is a copy the receiving server then governs, so the set of + targets is configured deliberately rather than named in passing. From 674723462630efd1406ff4bcbff0c24aff986dd2 Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 20:54:59 +1000 Subject: [PATCH 15/17] fix(ci): stop the lint gate aborting before it compares anything The step has failed on every run since it was added, and not for the reason it looks like: `npx redocly lint` exits non-zero whenever it reports an error, both documents report 24 by inheritance, and GitHub Actions runs the step under `bash -e`. With `set -o pipefail` the pipeline inherits that exit status, so errexit killed the script at the first assignment, before either count was echoed or compared. The log showed an exit code and no output, which is what made it read as a lint regression. Discard redocly's exit status in the helper, where the count already carries everything the status would have told us. Two things worth keeping while in here. Empty output now fails loudly rather than counting as zero errors, because a gate that passes when the linter did not run is worse than no gate. And the diagnostic lint in the failure branch gets `|| true`, so errexit cannot cut it short before the explicit exit. Verified against the step body extracted from the workflow itself, under `bash -e`: equal counts exit 0; a linter producing no output exits 1 with a message; a publisher document carrying one extra error exits 1 and reports the delta. Signed-off-by: Christopher Langton --- .github/workflows/publisher-spec-sync.yaml | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publisher-spec-sync.yaml b/.github/workflows/publisher-spec-sync.yaml index 0493594..3dc1319 100644 --- a/.github/workflows/publisher-spec-sync.yaml +++ b/.github/workflows/publisher-spec-sync.yaml @@ -47,15 +47,35 @@ jobs: # publisher document is worse than the spec it is built from. run: | set -o pipefail + # `redocly lint` exits non-zero whenever it reports an error, + # and both documents report 24, so its exit status says + # nothing the count does not. Discard it: this step runs under + # `bash -e`, and without the `|| true` the pipeline's inherited + # failure aborts the assignment below before anything is + # compared. That is not a hypothetical — it is why this step + # failed on every run before this comment existed. count() { - npx redocly lint "$1" --format=json 2>/dev/null \ - | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);console.log(j.problems.filter(p=>p.severity==="error").length)})' + { npx redocly lint "$1" --format=json 2>/dev/null || true; } | node -e ' + let s = "" + process.stdin.on("data", d => s += d).on("end", () => { + // No output means redocly itself failed. Counting that + // as zero errors would leave the gate below passing + // while checking nothing. + if (!s.trim()) { + console.error("redocly produced no output for the document under lint") + process.exit(1) + } + const j = JSON.parse(s) + console.log(j.problems.filter(p => p.severity === "error").length) + })' } base=$(count ../openapi.yaml) pub=$(count openapi.yaml) echo "consumption errors: $base, publisher errors: $pub" if [ "$pub" -gt "$base" ]; then echo "The publication overlay introduced $((pub - base)) new error(s)." - npx redocly lint openapi.yaml + # `|| true` so errexit does not cut the diagnostic short + # before the explicit exit below. + npx redocly lint openapi.yaml || true exit 1 fi From b38373a1e77bce869b2d1491fb8dec0a42f6cf89 Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 21:05:46 +1000 Subject: [PATCH 16/17] fix(publisher): resolve access policy by the narrowest declaration The inheritance rule said a child may narrow what it inherits and must not widen it, and described resolution as taking the nearest declared policy. Those two statements do not combine into the guarantee the document claims. ACCESS_WIDENS_PARENT rejects a declaration that is too wide at the moment it is written. It says nothing about a parent narrowed afterwards. So under nearest-wins, an artifact that had declared public goes on being public once its product is set to private, because the artifact's own declaration is the nearest one and therefore becomes the answer. Marking a product private is then a statement about a single row, which is the opposite of what the section says it is for. Resolution is now the narrowest declaration anywhere between the object and the root. Where several levels declare shared, entitlements intersect and the earliest expiry applies, since a grant on a child cannot create an entitlement its parent withheld. Descendants' declarations are explicitly not rewritten when an ancestor narrows. Folding is reversible and rewriting is not: widening a product back restores each descendant to its own declaration and no further, which is what makes a temporary embargo something a publisher can undo. Redocly still reports the same 24 errors as the consumption specification alone. Signed-off-by: Christopher Langton --- spec/publisher/README.md | 20 ++++++++++++----- spec/publisher/openapi.yaml | 44 ++++++++++++++++++++++++++++--------- spec/publisher/overlay.yaml | 44 ++++++++++++++++++++++++++++--------- 3 files changed, 82 insertions(+), 26 deletions(-) diff --git a/spec/publisher/README.md b/spec/publisher/README.md index e8ae8c5..5be8773 100644 --- a/spec/publisher/README.md +++ b/spec/publisher/README.md @@ -111,12 +111,20 @@ Three visibilities — `private`, `shared` (to named organisation UUIDs, with optional expiry) and `public` — plus `publishTo` for mirroring to other TEA servers. Two rules carry the weight: -- **Inheritance narrows, never widens.** An artifact takes its collection's - policy, a collection its release's, a release its product's. A child may - restrict what it inherits and must not loosen it; a server rejects the attempt - with `ACCESS_WIDENS_PARENT`. Without this, marking a product private would - guarantee nothing, because any artifact beneath it could quietly be made - public. +- **The narrowest declaration on the chain wins, not the nearest.** An artifact + sits under its collection, a collection under its release, a release under its + product, and the policy in force is the most restrictive declared anywhere + between the object and the root. `ACCESS_WIDENS_PARENT` rejects a declaration + that is too wide when it is written, which is not the same guarantee: it says + nothing about a parent narrowed afterwards. Under nearest-wins, an artifact + that had declared `public` stays public after its product is set to `private`, + so marking a product private would be a statement about one row. + + Where several levels declare `shared`, entitlements intersect and the earliest + expiry applies, because a grant on a child cannot create an entitlement its + parent withheld. Descendants' declarations are never rewritten, so widening a + product back restores each descendant to its own declaration and no further. + That is what makes a temporary embargo reversible. - **`public` is one-way in practice.** No later request recalls what has already been fetched, so that transition requires `confirm=public` rather than being an ordinary field update. The mechanism is named, not merely asked for: an diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index af31caf..e87ac24 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -2452,12 +2452,30 @@ components: counterparties under agreement, and material that is purely internal. Absent this, every publisher invents an answer, incompatibly. - Resolved by inheritance: an artifact takes its collection's policy, a - collection its release's, a release its product's. An object MAY narrow - what it inherits and MUST NOT widen it — a server rejects the attempt - with `ACCESS_WIDENS_PARENT`. Without that rule, marking a product - private would guarantee nothing, because any artifact beneath it could - quietly be made public. + Resolved along the chain from the object to the root: an artifact sits + under its collection, a collection under its release, a release under + its product. The policy in force is the NARROWEST declared anywhere on + that chain, not the nearest. + + The distinction is the whole guarantee. Under nearest-wins, an artifact + that had declared `public` before its product was set to `private` goes + on being public, because its own declaration is the nearest one and so + becomes the answer. `ACCESS_WIDENS_PARENT` alone does not prevent this: + it rejects a declaration that is too wide at the moment it is written, + and says nothing about a parent narrowed afterwards. A publisher + narrowing a product is entitled to expect that to mean something about + everything beneath it. + + Where more than one level declares `shared`, the entitlements are the + INTERSECTION of their `sharedWith` sets and the earliest expiry applies. + A grant made on a child cannot create an entitlement its parent + withheld. + + A server MUST NOT rewrite the declarations of descendants when an + ancestor narrows. Folding is reversible and rewriting is not: widening + a product back restores each descendant to its own declaration and no + further, which is what makes a temporary embargo a supported operation + rather than a decision that cannot be undone. A consumer denied by this policy is answered with the consumption specification's existing `OBJECT_NOT_SHAREABLE`, so enforcement needs @@ -2676,13 +2694,19 @@ components: and relies entirely on inheritance. $ref: "#/components/schemas/access-policy" effective: - description: What is enforced, after inheritance. + description: | + What is enforced: the narrowest policy declared between this object + and the root, with `shared` entitlements intersected. $ref: "#/components/schemas/access-policy" inheritedFrom: description: | - The ancestor the effective policy came from, when it was not - declared here. Naming it lets a publisher fix the cause rather than - the symptom. + The ancestor that constrained the answer, so a publisher can fix the + cause rather than the symptom. + + Present whenever an ancestor is what makes the effective policy + narrower than this object's own declaration, which includes the case + where the object declares something of its own and is overruled. An + ancestor that merely agrees is not the cause and is not reported. $ref: "#/components/schemas/uuid" mirrors: type: array diff --git a/spec/publisher/overlay.yaml b/spec/publisher/overlay.yaml index 2593b35..ef1d807 100644 --- a/spec/publisher/overlay.yaml +++ b/spec/publisher/overlay.yaml @@ -890,12 +890,30 @@ components: counterparties under agreement, and material that is purely internal. Absent this, every publisher invents an answer, incompatibly. - Resolved by inheritance: an artifact takes its collection's policy, a - collection its release's, a release its product's. An object MAY narrow - what it inherits and MUST NOT widen it — a server rejects the attempt - with `ACCESS_WIDENS_PARENT`. Without that rule, marking a product - private would guarantee nothing, because any artifact beneath it could - quietly be made public. + Resolved along the chain from the object to the root: an artifact sits + under its collection, a collection under its release, a release under + its product. The policy in force is the NARROWEST declared anywhere on + that chain, not the nearest. + + The distinction is the whole guarantee. Under nearest-wins, an artifact + that had declared `public` before its product was set to `private` goes + on being public, because its own declaration is the nearest one and so + becomes the answer. `ACCESS_WIDENS_PARENT` alone does not prevent this: + it rejects a declaration that is too wide at the moment it is written, + and says nothing about a parent narrowed afterwards. A publisher + narrowing a product is entitled to expect that to mean something about + everything beneath it. + + Where more than one level declares `shared`, the entitlements are the + INTERSECTION of their `sharedWith` sets and the earliest expiry applies. + A grant made on a child cannot create an entitlement its parent + withheld. + + A server MUST NOT rewrite the declarations of descendants when an + ancestor narrows. Folding is reversible and rewriting is not: widening + a product back restores each descendant to its own declaration and no + further, which is what makes a temporary embargo a supported operation + rather than a decision that cannot be undone. A consumer denied by this policy is answered with the consumption specification's existing `OBJECT_NOT_SHAREABLE`, so enforcement needs @@ -1119,13 +1137,19 @@ components: and relies entirely on inheritance. $ref: "#/components/schemas/access-policy" effective: - description: What is enforced, after inheritance. + description: | + What is enforced: the narrowest policy declared between this object + and the root, with `shared` entitlements intersected. $ref: "#/components/schemas/access-policy" inheritedFrom: description: | - The ancestor the effective policy came from, when it was not - declared here. Naming it lets a publisher fix the cause rather than - the symptom. + The ancestor that constrained the answer, so a publisher can fix the + cause rather than the symptom. + + Present whenever an ancestor is what makes the effective policy + narrower than this object's own declaration, which includes the case + where the object declares something of its own and is overruled. An + ancestor that merely agrees is not the cause and is not reported. $ref: "#/components/schemas/uuid" mirrors: type: array From 82ecd236409be0a3504a078be1991092aa92e9e8 Mon Sep 17 00:00:00 2001 From: Christopher Langton Date: Sun, 2 Aug 2026 21:15:29 +1000 Subject: [PATCH 17/17] style(publisher): drop the em dashes from the specification prose Every em dash in the publisher overlay, its README, the generator and the one comment added to the consumption specification is rewritten. Each was replaced according to the grammar of its own sentence, using a colon, a comma, a full stop or a parenthesis as the clause required, rather than by substituting one character for another. Wording only. `npm run check` reports the document still in sync, and redocly reports the same 24 errors on both documents. Signed-off-by: Christopher Langton --- .github/workflows/publisher-spec-sync.yaml | 2 +- spec/openapi.yaml | 2 +- spec/publisher/README.md | 36 +++++++------- spec/publisher/build.mjs | 13 ++--- spec/publisher/openapi.yaml | 53 ++++++++++---------- spec/publisher/overlay.yaml | 58 +++++++++++----------- 6 files changed, 85 insertions(+), 79 deletions(-) diff --git a/.github/workflows/publisher-spec-sync.yaml b/.github/workflows/publisher-spec-sync.yaml index 3dc1319..8545ad3 100644 --- a/.github/workflows/publisher-spec-sync.yaml +++ b/.github/workflows/publisher-spec-sync.yaml @@ -52,7 +52,7 @@ jobs: # nothing the count does not. Discard it: this step runs under # `bash -e`, and without the `|| true` the pipeline's inherited # failure aborts the assignment below before anything is - # compared. That is not a hypothetical — it is why this step + # compared. That is not a hypothetical: it is why this step # failed on every run before this comment existed. count() { { npx redocly lint "$1" --format=json 2>/dev/null || true; } | node -e ' diff --git a/spec/openapi.yaml b/spec/openapi.yaml index d615b13..04acc09 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -1761,7 +1761,7 @@ security: # TEI, fetches that host's discovery document, and calls the root it names. # Requiring a credential of all three would close the only entry point the # specification defines, so a server MUST be able to answer an anonymous - # request. What it answers with is a separate question — an object the caller + # request. What it answers with is a separate question: an object the caller # is not entitled to is `OBJECT_UNKNOWN`, exactly as it is for an object that # does not exist. # diff --git a/spec/publisher/README.md b/spec/publisher/README.md index 5be8773..d0e99cb 100644 --- a/spec/publisher/README.md +++ b/spec/publisher/README.md @@ -18,7 +18,7 @@ npm run lint # redocly It used to be a hand-maintained copy, and it drifted, as copies do. While the consumption API moved to 0.4.0 the copy still carried schemas from 0.0.3, under -different names — `artifactFormat` beside the real `artifact-format`, `typeUuid` +different names: `artifactFormat` beside the real `artifact-format`, `typeUuid` beside `uuid`, a private `artifactChecksum` beside `checksum`. A combined document then holds two definitions of the same concept, each free to move independently, with nothing to say which one a publisher should believe. @@ -41,18 +41,18 @@ owns. That is the drift check with teeth: the overlay may only add. `release` in the consumption schemas. - **Collections are addressed through their release.** The consumption specification states that a collection's UUID matches the release it belongs - to, and that an update only changes the version — so a collection is not an + to, and that an update only changes the version, so a collection is not an independently created object. Publishing one is now `PUT /productRelease/{uuid}/collection` (and the component equivalent), which publishes the next version, rather than `POST /collection`. - **Errors reuse the consumption envelope.** The draft returned - `application/json: {}` — an empty schema saying nothing about failures. + `application/json: {}`, an empty schema saying nothing about failures. `publisher-error-response` uses the same `{ error: }` shape as `error-response`, over an enumeration that repeats the consumption values (`OBJECT_UNKNOWN`, `OBJECT_NOT_SHAREABLE`) and adds the ones only a writer can hit, so a client branches on one enumeration rather than two. - **`distribution` was renamed `access-policy`.** The consumption specification - already uses `release-distribution` for something else entirely — a + already uses `release-distribution` for something else entirely: a downloadable build with its own URL and checksums. Reusing the word for access control in a combined document would have been actively misleading. - **The invalid delete indirection is gone.** `components.operations` is not a @@ -61,7 +61,7 @@ owns. That is the drift check with teeth: the overlay may only add. draft produced deletes that returned nothing. - **A product release can state its components.** The consumption API lists `components` among `productRelease`'s required members, and the draft's - create body had no way to set it — so every release a conformant publisher + create body had no way to set it, so every release a conformant publisher could produce had to be served with an empty list, which is a claim about the product rather than a gap in the record. - **An artifact can say which distributions it describes.** `distributionIds` @@ -83,7 +83,7 @@ correct one metadata field will in practice not correct the field. Content is `PUT` per format in its own media type rather than base64 inside JSON, which would inflate it by a third for no benefit. -Stored bytes are immutable — a checksum a consumer already recorded must not +Stored bytes are immutable: a checksum a consumer already recorded must not begin describing different content, so replacing content means a new format or a new artifact. Uploads accept `Content-Digest` (RFC 9530) and a mismatch is rejected, which is what makes a retry safe: a truncated transfer fails loudly @@ -96,7 +96,7 @@ indicator of the certificate used, and the intermediate and signing certificates. The consumption API exposes only a `signatureUrl`, so a consumer must infer the scheme from the bytes before it can verify anything. `artifact-signature` records the scheme, algorithm, key identifier, certificate -and chain, and the transparency-log entry where the scheme has one — which is +and chain, and the transparency-log entry where the scheme has one. That is what turns a signature from something that exists into something checkable. ### Access policy @@ -107,8 +107,8 @@ publication API, where the same server holds material that is deliberately public, material shared with named counterparties under agreement, and material that is purely internal. Absent an answer, every publisher invents one. -Three visibilities — `private`, `shared` (to named organisation UUIDs, with -optional expiry) and `public` — plus `publishTo` for mirroring to other TEA +Three visibilities (`private`, `shared` to named organisation UUIDs with an +optional expiry, and `public`), plus `publishTo` for mirroring to other TEA servers. Two rules carry the weight: - **The narrowest declaration on the chain wins, not the nearest.** An artifact @@ -143,7 +143,7 @@ than left to be reconstructed. Mirroring to other TEA servers is `publishTo`, naming targets registered through `/publicationTargets`. Registration is separate from use because a credential is involved, and because handing an object to another server is a -decision that outlives the object — once a copy lands there, this server's +decision that outlives the object: once a copy lands there, this server's policy no longer governs it. Mirroring is asynchronous and never blocks the local write: refusing to record a publisher's own release because a mirror is unreachable would make every target a single point of failure for publication @@ -157,8 +157,8 @@ accepting the request and doing nothing. organisation has published together with the policy in force for each. The consumption API cannot answer this. It answers what a *reader* is entitled -to see, and the thing a publisher most needs to verify — that something private -really is private — is exactly what a consumption response cannot show, because +to see, and the thing a publisher most needs to verify, that something private +really is private, is exactly what a consumption response cannot show, because an object correctly withheld and an object that was never created look identical from outside. @@ -166,7 +166,7 @@ identical from outside. `Idempotency-Key` on creates. Publication runs in pipelines, and pipelines retry. Without it a timeout that actually succeeded yields a second object on -the next attempt — a duplicate found by a consumer rather than by the publisher. +the next attempt, a duplicate found by a consumer rather than by the publisher. ## Known lint baseline @@ -174,8 +174,8 @@ the next attempt — a duplicate found by a consumer rather than by the publishe `spec/openapi.yaml` on its own reports exactly the same 24, so the publication overlay contributes none. -- 23 × `operation-summary` — consumption operations have no `summary`. -- 1 × `struct` — `pagination-details.nextPageToken` uses `nullable: false`, +- 23 × `operation-summary`: consumption operations have no `summary`. +- 1 × `struct`: `pagination-details.nextPageToken` uses `nullable: false`, which OpenAPI 3.1 removed in favour of a type union. These are worth fixing in the consumption specification rather than papering @@ -189,15 +189,15 @@ authentication is optional rather than mandatory. Without it the publication API cannot mean what it says. `visibility: public` is defined as "readable without authentication", and the discovery sequence a -consumer follows — take the domain out of a TEI, fetch that host's discovery -document, call the root it names — has no step at which a credential could be +consumer follows (take the domain out of a TEI, fetch that host's discovery +document, call the root it names) has no step at which a credential could be obtained. A specification that requires one on every operation closes the only entry point it defines, and any server that actually serves a public object anonymously is then non-conformant for doing the right thing. It is a widening, so no conformant client or server is broken by it: a server may still refuse every anonymous request, and one that answers is now allowed -to. The publication operations are unaffected — `build.mjs` gives every +to. The publication operations are unaffected: `build.mjs` gives every operation the overlay contributes an explicit `bearerAuth`/`basicAuth` requirement, so anonymous writes are never conformant. diff --git a/spec/publisher/build.mjs b/spec/publisher/build.mjs index 445a825..bfe9b54 100644 --- a/spec/publisher/build.mjs +++ b/spec/publisher/build.mjs @@ -4,7 +4,7 @@ // // The publisher document used to be a hand-maintained copy of the consumption // spec. It drifted, as copies do: while the consumption API reached 0.4.0 the -// copy was still carrying schemas from 0.0.3, under different names — +// copy was still carrying schemas from 0.0.3, under different names: // `artifactFormat` beside the real `artifact-format`, `typeUuid` beside `uuid`, // a private `artifactChecksum` beside `checksum`. A combined document then has // two definitions of the same concept, each free to move independently, and @@ -38,8 +38,8 @@ const overlay = yaml.load(readFileSync(overlayPath, 'utf8')) * * Deliberately shallow-but-structured rather than a generic deep merge: the * overlay may only ADD. If it defines a key the consumption spec already - * defines, that is drift reappearing — the publisher redefining something the - * consumer owns — so it is a build failure rather than a silent overwrite. + * defines, that is drift reappearing, the publisher redefining something the + * consumer owns, so it is a build failure rather than a silent overwrite. */ const collisions = [] @@ -49,7 +49,7 @@ const collisions = [] // // That must not reach the publication operations. Inheriting the global list // would make anonymous writes conformant, which is the opposite of what the -// access policy exists to say — so every operation the overlay contributes +// access policy exists to say, so every operation the overlay contributes // declares its own requirement here. Stated once, rather than repeated on // twenty operations where one omission would be a silent hole. const WRITE_SECURITY = [{ bearerAuth: [] }, { basicAuth: [] }] @@ -103,8 +103,9 @@ consumer.info = { consumer.info.description?.trim(), '', 'This document is the consumption specification plus the publication', - 'operations. Everything the two share — products, components, releases,', - 'collections, artifacts, checksums, identifiers — has a single definition,', + 'operations. Everything the two share, whether products, components,', + 'releases, collections, artifacts, checksums or identifiers, has a single', + 'definition,', 'taken from the consumption specification, so a publisher and a consumer', 'cannot hold different ideas of the same object.', '', diff --git a/spec/publisher/openapi.yaml b/spec/publisher/openapi.yaml index e87ac24..887f063 100644 --- a/spec/publisher/openapi.yaml +++ b/spec/publisher/openapi.yaml @@ -15,8 +15,9 @@ info: TBC This document is the consumption specification plus the publication - operations. Everything the two share — products, components, releases, - collections, artifacts, checksums, identifiers — has a single definition, + operations. Everything the two share, whether products, components, + releases, collections, artifacts, checksums or identifiers, has a single + definition, taken from the consumption specification, so a publisher and a consumer cannot hold different ideas of the same object. @@ -934,7 +935,8 @@ paths: channel a user installs from. Idempotent on the `url`, or on the `description` where a channel has no - single file to fetch — `brew install`, `scoop install`, `nix run`. A + single file to fetch, such as `brew install`, `scoop install` or + `nix run`. A pipeline that retries therefore updates the link rather than leaving a consumer to choose between two identical ones. @@ -944,7 +946,7 @@ paths: Checksums are recorded verbatim. This server does not hold the bytes a distribution points at, so it cannot verify them and does not claim to - have — which is why a publisher should copy the digests from the same + have, which is why a publisher should copy the digests from the same manifest its users verify against rather than recomputing them. parameters: - $ref: "#/components/parameters/object-uuid" @@ -1208,9 +1210,9 @@ paths: description: | Register a remote TEA server this organisation may mirror to. - A server SHOULD verify the target before accepting it — that the root - answers, advertises a compatible version, and accepts the credential — - so that a target which was never going to work fails here, in front of + A server SHOULD verify the target before accepting it, confirming that + the root answers, advertises a compatible version and accepts the + credential, so that a target which was never going to work fails here, in front of whoever configured it, rather than later inside an asynchronous mirror nobody is watching. parameters: @@ -1388,7 +1390,7 @@ paths: A change to `public` MUST carry `confirm=public`, because no later request undoes it: whatever was fetched while it was public stays fetched. Naming the mechanism rather than only asking for "a - confirmation" is deliberate — an unspecified requirement is one every + confirmation" is deliberate: an unspecified requirement is one every publisher satisfies differently, which is the failure this document exists to avoid. parameters: @@ -2426,7 +2428,7 @@ components: `error-response`, over the wider publication enumeration, with an optional human-readable message and a pointer to the offending field. - Publication is automated — a release pipeline is usually the caller — + Publication is automated, and a release pipeline is usually the caller, so a failure has to be actionable without a human reading prose. additionalProperties: false properties: @@ -2485,13 +2487,13 @@ components: visibility: type: string description: | - `private` — only the publishing organisation may read it. + `private`: only the publishing organisation may read it. - `shared` — readable by the publishing organisation and the + `shared`: readable by the publishing organisation and the organisations named in `sharedWith`. Authentication is required and the reader's identity decides the answer. - `public` — readable without authentication. The only value that + `public`: readable without authentication. The only value that makes an object part of the open transparency web, and irreversible in practice: what has been fetched cannot be recalled. enum: @@ -2568,8 +2570,8 @@ components: type: string format: uri description: | - The remote server's TEA root, without a trailing slash — the same - value that server advertises as `rootUrl` in its own discovery + The remote server's TEA root, without a trailing slash. This is the + same value that server advertises as `rootUrl` in its own discovery document. domain: type: string @@ -2614,8 +2616,8 @@ components: description: | State of the most recent attempt. - `pending` — accepted locally, not yet copied. `published` — the - target acknowledged it. `failed` — the last attempt did not + `pending` means accepted locally and not yet copied. `published` + means the target acknowledged it. `failed` means the last attempt did not succeed and `detail` says why; the server SHOULD retry, and MUST NOT silently give up without leaving this state visible. enum: @@ -2638,7 +2640,7 @@ components: signatures/signature.md calls for an indication of the hash algorithm, an indicator of the certificate used, and the intermediate and signing certificates. This carries them, plus the transparency-log entry where - the scheme has one — a signature that is itself publicly logged is far + the scheme has one. A signature that is itself publicly logged is far harder to backdate. additionalProperties: false properties: @@ -2756,7 +2758,7 @@ components: minimum: 0 description: | Version of this release's current collection, or 0 where no - collection has been published yet — a release a consumer can + collection has been published yet, a release a consumer can resolve and find nothing in. artifactCount: type: integer @@ -3111,7 +3113,7 @@ components: description: | Zero-based index of the format within the artifact's `formats` array. Formats are addressed positionally because a media type is not unique - within an artifact — the same document is often published both + within an artifact: the same document is often published both compressed and not. schema: type: integer @@ -3140,7 +3142,7 @@ components: Publication runs in release pipelines, and pipelines retry. Without this, a timeout that actually succeeded produces a second object on the - next attempt — a duplicate discovered by a consumer rather than by the + next attempt, a duplicate discovered by a consumer rather than by the publisher. A server supporting the key MUST return the original result for a repeat of the same key rather than creating again. schema: @@ -3180,7 +3182,8 @@ components: type: array description: | Identifiers for the product. A server assigns the TEI itself - — it names the server's own authority over the object, so a + because it names the server's own authority over the object, + so a publisher supplying one would be asserting something only the server can know. items: @@ -3286,7 +3289,7 @@ components: format: uri description: | The remote server's TEA root, without a trailing slash. Must - be an absolute https URL — a mirror carries a credential, and + be an absolute https URL, because a mirror carries a credential and sending one over http would publish it to the network as well as to the target. description: @@ -3350,8 +3353,8 @@ components: type: array description: | Identifiers for this distribution specifically. Optional - because several real channels have no accepted purl type — - Homebrew and Scoop among them — and an identifier that names + because several real channels have no accepted purl type, + Homebrew and Scoop among them, and an identifier that names a coordinate which does not resolve is worse than none, since a consumer will follow it. items: @@ -3423,7 +3426,7 @@ components: type: object additionalProperties: false description: | - Metadata only. Stored bytes are immutable — replacing content + Metadata only. Stored bytes are immutable: replacing content means a new format or a new artifact, not an overwrite. properties: name: diff --git a/spec/publisher/overlay.yaml b/spec/publisher/overlay.yaml index ef1d807..5102fd0 100644 --- a/spec/publisher/overlay.yaml +++ b/spec/publisher/overlay.yaml @@ -3,7 +3,7 @@ # Everything shared with the consumption API is referenced, never redefined. # build.mjs fails the build if this file declares a path method, schema, # parameter, response or request body that the consumption specification -# already owns — which is how the previous copy drifted. +# already owns, which is how the previous copy drifted. paths: # ── Product ─────────────────────────────────────────────────────────────── @@ -275,7 +275,7 @@ paths: # A collection is not an independent object: the consumption specification # states its UUID matches the release it belongs to, and that an update only # changes the version. Publishing one is therefore addressed through its - # release, and is a PUT rather than a POST — there is one current collection + # release, and is a PUT rather than a POST: there is one current collection # per release, and this replaces it with the next version. /productRelease/{uuid}/collection: put: @@ -339,7 +339,7 @@ paths: # Attached to a component release because that is where the consumption # schema defines them: `release.distributions` exists, `productRelease` has no # such field. A publisher wanting download links therefore decomposes its - # product into at least one component — the model the consumption + # product into at least one component, the model the consumption # specification already assumes, not an extra requirement of this overlay. /componentRelease/{uuid}/distribution: post: @@ -350,7 +350,8 @@ paths: channel a user installs from. Idempotent on the `url`, or on the `description` where a channel has no - single file to fetch — `brew install`, `scoop install`, `nix run`. A + single file to fetch, such as `brew install`, `scoop install` or + `nix run`. A pipeline that retries therefore updates the link rather than leaving a consumer to choose between two identical ones. @@ -360,7 +361,7 @@ paths: Checksums are recorded verbatim. This server does not hold the bytes a distribution points at, so it cannot verify them and does not claim to - have — which is why a publisher should copy the digests from the same + have, which is why a publisher should copy the digests from the same manifest its users verify against rather than recomputing them. parameters: - $ref: "#/components/parameters/object-uuid" @@ -619,9 +620,9 @@ paths: description: | Register a remote TEA server this organisation may mirror to. - A server SHOULD verify the target before accepting it — that the root - answers, advertises a compatible version, and accepts the credential — - so that a target which was never going to work fails here, in front of + A server SHOULD verify the target before accepting it, confirming that + the root answers, advertises a compatible version and accepts the + credential, so that a target which was never going to work fails here, in front of whoever configured it, rather than later inside an asynchronous mirror nobody is watching. parameters: @@ -692,8 +693,8 @@ paths: # # A publisher cannot reconcile its own state through the consumption API, # because that API answers what a *reader* is entitled to see. The one thing - # a publisher most needs to check — whether something private is in fact - # private — is precisely what a consumption response cannot tell it, since an + # a publisher most needs to check, whether something private is in fact + # private, is precisely what a consumption response cannot tell it, since an # object correctly withheld and an object that was never created look # identical from outside. # @@ -802,7 +803,7 @@ paths: A change to `public` MUST carry `confirm=public`, because no later request undoes it: whatever was fetched while it was public stays fetched. Naming the mechanism rather than only asking for "a - confirmation" is deliberate — an unspecified requirement is one every + confirmation" is deliberate: an unspecified requirement is one every publisher satisfies differently, which is the failure this document exists to avoid. parameters: @@ -863,7 +864,7 @@ components: `error-response`, over the wider publication enumeration, with an optional human-readable message and a pointer to the offending field. - Publication is automated — a release pipeline is usually the caller — + Publication is automated, and a release pipeline is usually the caller, so a failure has to be actionable without a human reading prose. additionalProperties: false properties: @@ -923,13 +924,13 @@ components: visibility: type: string description: | - `private` — only the publishing organisation may read it. + `private`: only the publishing organisation may read it. - `shared` — readable by the publishing organisation and the + `shared`: readable by the publishing organisation and the organisations named in `sharedWith`. Authentication is required and the reader's identity decides the answer. - `public` — readable without authentication. The only value that + `public`: readable without authentication. The only value that makes an object part of the open transparency web, and irreversible in practice: what has been fetched cannot be recalled. enum: @@ -1008,8 +1009,8 @@ components: type: string format: uri description: | - The remote server's TEA root, without a trailing slash — the same - value that server advertises as `rootUrl` in its own discovery + The remote server's TEA root, without a trailing slash. This is the + same value that server advertises as `rootUrl` in its own discovery document. domain: type: string @@ -1055,8 +1056,8 @@ components: description: | State of the most recent attempt. - `pending` — accepted locally, not yet copied. `published` — the - target acknowledged it. `failed` — the last attempt did not + `pending` means accepted locally and not yet copied. `published` + means the target acknowledged it. `failed` means the last attempt did not succeed and `detail` says why; the server SHOULD retry, and MUST NOT silently give up without leaving this state visible. enum: @@ -1080,7 +1081,7 @@ components: signatures/signature.md calls for an indication of the hash algorithm, an indicator of the certificate used, and the intermediate and signing certificates. This carries them, plus the transparency-log entry where - the scheme has one — a signature that is itself publicly logged is far + the scheme has one. A signature that is itself publicly logged is far harder to backdate. additionalProperties: false properties: @@ -1201,7 +1202,7 @@ components: minimum: 0 description: | Version of this release's current collection, or 0 where no - collection has been published yet — a release a consumer can + collection has been published yet, a release a consumer can resolve and find nothing in. artifactCount: type: integer @@ -1272,7 +1273,7 @@ components: description: | Zero-based index of the format within the artifact's `formats` array. Formats are addressed positionally because a media type is not unique - within an artifact — the same document is often published both + within an artifact: the same document is often published both compressed and not. schema: type: integer @@ -1301,7 +1302,7 @@ components: Publication runs in release pipelines, and pipelines retry. Without this, a timeout that actually succeeded produces a second object on the - next attempt — a duplicate discovered by a consumer rather than by the + next attempt, a duplicate discovered by a consumer rather than by the publisher. A server supporting the key MUST return the original result for a repeat of the same key rather than creating again. schema: @@ -1471,7 +1472,8 @@ components: type: array description: | Identifiers for the product. A server assigns the TEI itself - — it names the server's own authority over the object, so a + because it names the server's own authority over the object, + so a publisher supplying one would be asserting something only the server can know. items: @@ -1577,7 +1579,7 @@ components: format: uri description: | The remote server's TEA root, without a trailing slash. Must - be an absolute https URL — a mirror carries a credential, and + be an absolute https URL, because a mirror carries a credential and sending one over http would publish it to the network as well as to the target. description: @@ -1641,8 +1643,8 @@ components: type: array description: | Identifiers for this distribution specifically. Optional - because several real channels have no accepted purl type — - Homebrew and Scoop among them — and an identifier that names + because several real channels have no accepted purl type, + Homebrew and Scoop among them, and an identifier that names a coordinate which does not resolve is worse than none, since a consumer will follow it. items: @@ -1712,7 +1714,7 @@ components: type: object additionalProperties: false description: | - Metadata only. Stored bytes are immutable — replacing content + Metadata only. Stored bytes are immutable: replacing content means a new format or a new artifact, not an overwrite. properties: name: