diff --git a/.changes/toolkit/0.3.0.md b/.changes/toolkit/0.3.0.md new file mode 100644 index 0000000..418092c --- /dev/null +++ b/.changes/toolkit/0.3.0.md @@ -0,0 +1,5 @@ +## 0.3.0 - 2026-09-21 + +### Added + +- 增加静态文档候选打包、幂等发布回执和文档发现;暂时性故障最多原样重试一次,无法确认时保留候选并报告结果未知。 diff --git a/.changeset/web-runtime-management-documentation.md b/.changeset/web-runtime-management-documentation.md new file mode 100644 index 0000000..dcc68e0 --- /dev/null +++ b/.changeset/web-runtime-management-documentation.md @@ -0,0 +1,5 @@ +--- +'@inkcre/extension-runtime-client-web': minor +--- + +新增精确 Peer 的 Extension 管理入口和独立的精确发行文档查询,使用真实 Core 0.3 SDK 合同。 diff --git a/contracts/documentation.schema.json b/contracts/documentation.schema.json new file mode 100644 index 0000000..7e5397c --- /dev/null +++ b/contracts/documentation.schema.json @@ -0,0 +1,393 @@ +{ + "$defs": { + "DocumentationHosting": { + "additionalProperties": false, + "properties": { + "origin_template": { + "title": "Origin Template", + "type": "string" + } + }, + "required": [ + "origin_template" + ], + "title": "DocumentationHosting", + "type": "object" + }, + "DocumentationPublication": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Build Id" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "expected_etag": { + "anyOf": [ + { + "pattern": "^\"[0-9a-f]{64}\"$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Expected Etag" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision", + "expected_etag" + ], + "title": "DocumentationPublication", + "type": "object" + }, + "DocumentationReceipt": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Build Id" + }, + "committed_at": { + "title": "Committed At", + "type": "string" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "name": { + "maxLength": 129, + "minLength": 3, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + }, + "scope": { + "enum": [ + "global", + "python", + "module-federation" + ], + "title": "Scope", + "type": "string" + }, + "snapshot_etag": { + "title": "Snapshot Etag", + "type": "string" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "snapshot_url": { + "title": "Snapshot Url", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision", + "name", + "version", + "scope", + "snapshot_etag", + "snapshot_url", + "committed_at" + ], + "title": "DocumentationReceipt", + "type": "object" + }, + "DocumentationRecord": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Build Id" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "entry_url": { + "title": "Entry Url", + "type": "string" + }, + "etag": { + "title": "Etag", + "type": "string" + }, + "scope": { + "enum": [ + "global", + "python", + "module-federation" + ], + "title": "Scope", + "type": "string" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "snapshot_url": { + "title": "Snapshot Url", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + }, + "updated_at": { + "title": "Updated At", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision", + "scope", + "etag", + "entry_url", + "snapshot_url", + "updated_at" + ], + "title": "DocumentationRecord", + "type": "object" + }, + "DocumentationUpload": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Build Id" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision" + ], + "title": "DocumentationUpload", + "type": "object" + }, + "ReleaseDocumentation": { + "additionalProperties": false, + "properties": { + "name": { + "maxLength": 129, + "minLength": 3, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + }, + "sets": { + "items": { + "$ref": "#/$defs/DocumentationRecord" + }, + "title": "Sets", + "type": "array" + }, + "state": { + "enum": [ + "preparing", + "published", + "yanked", + "blocked" + ], + "title": "State", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version", + "state", + "sets" + ], + "title": "ReleaseDocumentation", + "type": "object" + } + }, + "additionalProperties": false, + "properties": { + "hosting": { + "$ref": "#/$defs/DocumentationHosting" + }, + "publication": { + "$ref": "#/$defs/DocumentationPublication" + }, + "receipt": { + "$ref": "#/$defs/DocumentationReceipt" + }, + "release": { + "$ref": "#/$defs/ReleaseDocumentation" + }, + "upload": { + "$ref": "#/$defs/DocumentationUpload" + } + }, + "required": [ + "upload", + "publication", + "receipt", + "release", + "hosting" + ], + "title": "DocumentationContracts", + "type": "object" +} diff --git a/contracts/openapi.json b/contracts/openapi.json index b2d519c..603e1fb 100644 --- a/contracts/openapi.json +++ b/contracts/openapi.json @@ -1,6 +1,199 @@ { "components": { "schemas": { + "DocumentationHosting": { + "additionalProperties": false, + "properties": { + "origin_template": { + "title": "Origin Template", + "type": "string" + } + }, + "required": [ + "origin_template" + ], + "title": "DocumentationHosting", + "type": "object" + }, + "DocumentationReceipt": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Build Id" + }, + "committed_at": { + "title": "Committed At", + "type": "string" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "name": { + "maxLength": 129, + "minLength": 3, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + }, + "scope": { + "enum": [ + "global", + "python", + "module-federation" + ], + "title": "Scope", + "type": "string" + }, + "snapshot_etag": { + "title": "Snapshot Etag", + "type": "string" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "snapshot_url": { + "title": "Snapshot Url", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision", + "name", + "version", + "scope", + "snapshot_etag", + "snapshot_url", + "committed_at" + ], + "title": "DocumentationReceipt", + "type": "object" + }, + "DocumentationRecord": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Build Id" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "entry_url": { + "title": "Entry Url", + "type": "string" + }, + "etag": { + "title": "Etag", + "type": "string" + }, + "scope": { + "enum": [ + "global", + "python", + "module-federation" + ], + "title": "Scope", + "type": "string" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "snapshot_url": { + "title": "Snapshot Url", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + }, + "updated_at": { + "title": "Updated At", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision", + "scope", + "etag", + "entry_url", + "snapshot_url", + "updated_at" + ], + "title": "DocumentationRecord", + "type": "object" + }, "ExtensionRecord": { "additionalProperties": false, "properties": { @@ -428,6 +621,64 @@ "title": "PythonEntryPoint", "type": "object" }, + "RegistryError": { + "additionalProperties": false, + "properties": { + "detail": { + "title": "Detail", + "type": "string" + } + }, + "required": [ + "detail" + ], + "title": "RegistryError", + "type": "object" + }, + "ReleaseDocumentation": { + "additionalProperties": false, + "properties": { + "name": { + "maxLength": 129, + "minLength": 3, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + }, + "sets": { + "items": { + "$ref": "#/components/schemas/DocumentationRecord" + }, + "title": "Sets", + "type": "array" + }, + "state": { + "enum": [ + "preparing", + "published", + "yanked", + "blocked" + ], + "title": "State", + "type": "string" + }, + "version": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version", + "state", + "sets" + ], + "title": "ReleaseDocumentation", + "type": "object" + }, "ReleaseRecord": { "additionalProperties": false, "properties": { @@ -799,6 +1050,34 @@ "summary": "Simple Project" } }, + "/v1/documentation-hosting": { + "get": { + "operationId": "documentation_hosting_v1_documentation_hosting_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentationHosting" + } + } + }, + "description": "Successful Response" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Documentation content hosting is not configured." + } + }, + "summary": "Documentation Hosting" + } + }, "/v1/extensions": { "get": { "operationId": "list_extensions_v1_extensions_get", @@ -1021,9 +1300,9 @@ "summary": "Get Release" } }, - "/v1/extensions/{namespace}/{name}/releases/{version}/module-federation": { - "post": { - "operationId": "upload_module_federation_v1_extensions__namespace___name__releases__version__module_federation_post", + "/v1/extensions/{namespace}/{name}/releases/{version}/documentation": { + "get": { + "operationId": "get_documentation_v1_extensions__namespace___name__releases__version__documentation_get", "parameters": [ { "in": "path", @@ -1060,22 +1339,6 @@ "title": "Version", "type": "string" } - }, - { - "in": "header", - "name": "authorization", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Authorization" - } } ], "responses": { @@ -1083,12 +1346,22 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReleaseRecord" + "$ref": "#/components/schemas/ReleaseDocumentation" } } }, "description": "Successful Response" }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Release or publicly readable documentation does not exist." + }, "422": { "content": { "application/json": { @@ -1098,14 +1371,34 @@ } }, "description": "Validation Error" + }, + "451": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "The Release is operator-blocked, including for its publisher." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Documentation content hosting is not configured." } }, - "summary": "Upload Module Federation" + "summary": "Get Documentation" } }, - "/v1/extensions/{namespace}/{name}/releases/{version}/publish": { + "/v1/extensions/{namespace}/{name}/releases/{version}/documentation/{scope}": { "post": { - "operationId": "publish_release_v1_extensions__namespace___name__releases__version__publish_post", + "operationId": "upload_documentation_v1_extensions__namespace___name__releases__version__documentation__scope__post", "parameters": [ { "in": "path", @@ -1143,6 +1436,20 @@ "type": "string" } }, + { + "in": "path", + "name": "scope", + "required": true, + "schema": { + "enum": [ + "global", + "python", + "module-federation" + ], + "title": "Scope", + "type": "string" + } + }, { "in": "header", "name": "authorization", @@ -1160,16 +1467,372 @@ } } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReleaseRecord" - } - } - }, - "description": "Successful Response" + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "content": { + "format": "binary", + "type": "string" + }, + "metadata": { + "contentMediaType": "application/json", + "contentSchema": { + "additionalProperties": false, + "properties": { + "build_id": { + "anyOf": [ + { + "maxLength": 256, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Build Id" + }, + "content_sha256": { + "pattern": "^[0-9a-f]{64}$", + "title": "Content Sha256", + "type": "string" + }, + "entry": { + "default": "index.html", + "maxLength": 768, + "minLength": 1, + "title": "Entry", + "type": "string" + }, + "expected_etag": { + "anyOf": [ + { + "pattern": "^\"[0-9a-f]{64}\"$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Expected Etag" + }, + "snapshot_id": { + "pattern": "^[0-9a-f]{32}$", + "title": "Snapshot Id", + "type": "string" + }, + "source_repository": { + "maxLength": 2048, + "minLength": 1, + "title": "Source Repository", + "type": "string" + }, + "source_revision": { + "maxLength": 256, + "minLength": 1, + "title": "Source Revision", + "type": "string" + } + }, + "required": [ + "snapshot_id", + "content_sha256", + "source_repository", + "source_revision", + "expected_etag" + ], + "title": "DocumentationPublication", + "type": "object" + }, + "description": "JSON-encoded DocumentationPublication; send as a text form field, not a file.", + "type": "string" + } + }, + "required": [ + "metadata", + "content" + ], + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentationReceipt" + } + } + }, + "description": "Historical commit confirmed; current may differ." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Malformed conditional headers, multipart fields, metadata, or static ZIP." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Publisher credential is missing or invalid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Publisher does not own this namespace." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Release or publicly readable documentation does not exist." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Publication identity conflict, stale expected_etag, or missing association." + }, + "411": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "A non-chunked Content-Length is required." + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "The complete multipart request exceeds 20 MiB." + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "The request must use multipart/form-data." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + }, + "451": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "The Release is operator-blocked, including for its publisher." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Documentation content hosting is not configured." + } + }, + "summary": "Upload Documentation" + } + }, + "/v1/extensions/{namespace}/{name}/releases/{version}/module-federation": { + "post": { + "operationId": "upload_module_federation_v1_extensions__namespace___name__releases__version__module_federation_post", + "parameters": [ + { + "in": "path", + "name": "namespace", + "required": true, + "schema": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Namespace", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + } + }, + { + "in": "path", + "name": "version", + "required": true, + "schema": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseRecord" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Upload Module Federation" + } + }, + "/v1/extensions/{namespace}/{name}/releases/{version}/publish": { + "post": { + "operationId": "publish_release_v1_extensions__namespace___name__releases__version__publish_post", + "parameters": [ + { + "in": "path", + "name": "namespace", + "required": true, + "schema": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Namespace", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + } + }, + { + "in": "path", + "name": "version", + "required": true, + "schema": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseRecord" + } + } + }, + "description": "Successful Response" }, "422": { "content": { @@ -1423,6 +2086,138 @@ }, "summary": "Publisher Workspace" } + }, + "/v1/publisher/extensions/{namespace}/{name}/releases/{version}/documentation": { + "get": { + "operationId": "publisher_documentation_v1_publisher_extensions__namespace___name__releases__version__documentation_get", + "parameters": [ + { + "in": "path", + "name": "namespace", + "required": true, + "schema": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Namespace", + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "required": true, + "schema": { + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + "title": "Name", + "type": "string" + } + }, + { + "in": "path", + "name": "version", + "required": true, + "schema": { + "maxLength": 128, + "minLength": 5, + "pattern": "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + "title": "Version", + "type": "string" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Authorization" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReleaseDocumentation" + } + } + }, + "description": "Successful Response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Publisher credential is missing or invalid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Publisher does not own this namespace." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Release or publicly readable documentation does not exist." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + }, + "451": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "The Release is operator-blocked, including for its publisher." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistryError" + } + } + }, + "description": "Documentation content hosting is not configured." + } + }, + "summary": "Publisher Documentation" + } } } } diff --git a/contracts/revision.json b/contracts/revision.json index 56abec5..4dab421 100644 --- a/contracts/revision.json +++ b/contracts/revision.json @@ -1,9 +1,14 @@ { - "contract_revision": 2, + "contract_revision": 3, "distribution_kinds": [ "module_federation", "python" ], + "documentation_scopes": [ + "global", + "module-federation", + "python" + ], "extension_version": "strict-semver-without-build-metadata", "python_upload_filetypes": [ "bdist_wheel" diff --git a/docs/30-unit-tdd/README.md b/docs/30-unit-tdd/README.md index a8ac4a7..5f41740 100644 --- a/docs/30-unit-tdd/README.md +++ b/docs/30-unit-tdd/README.md @@ -5,9 +5,11 @@ independently released units. Public schemas, routes, package metadata, and generated contracts and build checks remain executable authority. - [Registry control plane](registry-control-plane.md) +- [静态文档发布协议](documentation-admission.md):作者构建限制、MIME、候选发布与恢复。 - [Registry Web](registry-web.md): discovery, publisher workspace, and design consumption. - [Extension Developer Toolkit](developer-toolkit.md) - [Core Python Extension Host Runtime](core-python-runtime.md) +- [Client Web Extension Runtime](client-web-runtime.md):浏览器生命周期、精确 Peer 管理与发行文档消费。 Shared product behavior and cross-unit Host contracts remain in the InKCre documentation Hub rather than being copied here. diff --git a/docs/30-unit-tdd/client-web-runtime.md b/docs/30-unit-tdd/client-web-runtime.md new file mode 100644 index 0000000..26a8438 --- /dev/null +++ b/docs/30-unit-tdd/client-web-runtime.md @@ -0,0 +1,13 @@ +# Client Web Extension Runtime + +`@inkcre/extension-runtime-client-web` 是独立发行的浏览器 Runtime。Host SDK `@inkcre/core` 拥有部署持久化模型、Peer 发现、传输和错误分类;Runtime 拥有 Extension 生命周期编排、管理命令消费和 Registry 发行读取。应用继续决定操作当前浏览器 Runtime、在线远端 Host,还是只改变部署中的 desired state。 + +`listAdvertisedExtensionManagementPeers()` 读取 `PeerManager.listLive()`,只保留宣告精确管理 capability 的 Peer。宣告不是协议支持、路由就绪或运行证明。`manageExtensionOnPeer(peerId, command)` 使用调用者指定的精确 Peer 和固定 capability,发送一次 install、enable、disable 或 patch_config 命令。它不替换目标、不重试、不改写 PeerManager 的传输失败分类;管理 HTTP 错误及无效业务响应不附带可能回显配置凭据的原始正文或校验详情。返回的安装记录仍不证明插件持续运行。 + +`getExtensionDocumentation(origin, name, version)` 使用调用者已解析的 Registry origin 和生成 HTTP 合同查询精确发行,只返回实际存在的 scope 与 entry_url。404 返回 null,成功但没有文档返回空数组;网络、其他 HTTP 错误、错误坐标和无效响应抛出 `RegistryDocumentationError`。调用者可以将不可用帮助入口降级为提示,不应据此阻止插件初始化。文档查询不执行安装预检,不要求 MF Distribution 或匹配的 Host SDK,不读取作者正文,也不回退到另一版本或 scope。 + +文档入口只接受无 URL 凭据的绝对 HTTP(S) 地址;UI 拥有外链展示、作者页面或锚点约定,以及断开 opener 的责任。公开发现请求不发送凭据。Registry 拥有文档托管与发行状态的具体合同,Runtime 不复制正文、内容域名构造或作者导航模型。 + +Runtime 的 Core peer dependency 以实际类型与构建验证为准,不使用 ambient SDK stub。新公共 API 通过 Changesets 声明 minor 发行意图,源包版本由正式版本流程修改。 + +Core SDK 尚未提供独立发布产物,因此开发依赖固定 client-web 的 Git revision;该子目录包只提供 manifest 和依赖,不能冒充构建产物。`prepare:sdk` 在临时目录只检出同一 revision 的 `packages/core`,沿用其真实源码、TypeScript 配置及原始 tsdown 配置,用 ext-reg 自身冻结的工具和 SDK 依赖构建 ESM 与类型声明,再将产物放入当前 workspace 的 node_modules 私有目录。它不安装生产者 workspace,不需要 GitHub Packages 凭据,也不改 pnpm store、生产依赖、源码或锁文件;首次构建只需读取公开 GitHub 源码。type-check 与 build 共用这一准备步骤,仓库门禁还通过真实 SDK 和本地 HTTP 运行 `check-boundaries.mjs`。正式 SDK 产物可用后,应改为固定产物开发依赖并删除准备脚本,而不是继续维护另一套 SDK 声明。 diff --git a/docs/30-unit-tdd/documentation-admission.md b/docs/30-unit-tdd/documentation-admission.md new file mode 100644 index 0000000..d065a74 --- /dev/null +++ b/docs/30-unit-tdd/documentation-admission.md @@ -0,0 +1,83 @@ +# 静态文档发布协议 + +本文面向 Extension 文档作者。Registry 托管作者已构建的静态资源,不运行 SSG,也不解释 Markdown、导航或搜索结构。全局文档使用 `global`,渠道文档使用 `python` 或 `module-federation`;每组文档属于一个精确 Extension Release,渠道组要求该 Release 已关联对应 Distribution。 + +## 构建与准入 + +将站点构建为一个输出目录,默认入口为 `index.html`。站点在独立快照 origin 的根目录运行,可以使用根相对资源地址。Registry 不执行作者的服务器配置。以下限制是作者可依赖的准入合同;修改限制或删除已准入的类型时,必须评估现有构建的兼容性并记录变更。 + +| 项目 | 准入要求 | +| ---------- | -------------------------------------------------------------------------------------------------------------------- | +| 请求大小 | 整个 multipart 请求(包括 metadata、边界和 ZIP)不超过 20 MiB,必须有 `Content-Length`,不接受 chunked 上传 | +| ZIP 大小 | ZIP 本身不超过 20 MiB;临近上限时须为 multipart 开销留空间 | +| 展开大小 | 所有文件合计不超过 100 MiB,单文件不超过 20 MiB | +| 成员数量 | 最多 4096 个 ZIP 成员,显式目录也计入;Toolkit 打包仅写文件成员 | +| 压缩比 | 单文件超过 1 MiB 时,展开大小不得超过 `200 × max(压缩大小, 1)` | +| 入口 | 已存在的、以小写 `.html` 结尾的文件;通过 `--entry` 可选择其他路径 | +| 路径 | 相对、规范化的 POSIX 路径,UTF-8 编码不超过 768 字节;禁止空路径、绝对路径、重复斜线、`.`/`..` 和任何以 `.` 开头的段 | +| 路径字符 | 禁止反斜线、`%`、`?`、`#`、`:`、ASCII 控制字符及 DEL | +| 服务器配置 | 任意路径段不得为 `_headers` 或 `_redirects` | +| 文件结构 | 只接受普通文件和目录,禁止 symlink、其他特殊成员、加密成员、重复路径和文件/目录冲突 | +| 文件类型 | 必须具有下表中的小写扩展名;MIME 由 Registry 决定,不采用上传方声明 | + +| 扩展名 | 响应 MIME | +| ----------------- | ----------------------------------- | +| `.html` | `text/html` | +| `.css` | `text/css` | +| `.js`, `.mjs` | `text/javascript` | +| `.json`, `.map` | `application/json` | +| `.txt` | `text/plain` | +| `.xml` | `application/xml` | +| `.svg` | `image/svg+xml` | +| `.png` | `image/png` | +| `.jpg`, `.jpeg` | `image/jpeg` | +| `.gif` | `image/gif` | +| `.webp` | `image/webp` | +| `.avif` | `image/avif` | +| `.ico` | `image/x-icon` | +| `.woff`, `.woff2` | `font/woff`, `font/woff2`,分别对应 | +| `.ttf`, `.otf` | `font/ttf`, `font/otf`,分别对应 | +| `.pdf` | `application/pdf` | +| `.wasm` | `application/wasm` | +| `.webmanifest` | `application/manifest+json` | +| `.mp3` | `audio/mpeg` | +| `.mp4` | `video/mp4` | +| `.webm` | `video/webm` | +| `.ogg` | `audio/ogg` | + +路由按“精确文件 → 目录内 `index.html` → 同名 `.html`”查找;目录缺少结尾斜线时重定向补齐。根路径返回指定入口。没有 SPA fallback,不存在的路径返回 404;SSG 应输出实际页面文件。 + +## 用 Toolkit 发布 + +使用 Toolkit 0.3.x 的 `cli` extra。先用自己的 SSG 生成输出目录,再保存候选: + +```bash +inkcre-ext docs pack --site ./site-output --output ./docs-candidate \ + --name example/my-extension --version 1.0.0 --scope global \ + --source-repository https://github.com/example/my-extension \ + --source-revision COMMIT_SHA +``` + +`pack` 会执行与 Registry 相同的静态资源检查,并保存 ZIP、metadata、随机快照 ID 和写入前提。输出目录必须尚不存在。如果 SSG 需要绝对 origin,先运行 `inkcre-ext docs address --registry-url REGISTRY_URL`,将返回的 origin 用于构建,并将该 `snapshot_id` 传给 `pack --snapshot-id`。这只是生成未绑定地址;实际占用和冲突检查发生在发布时。 + +将发布凭据通过 `INKCRE_EXTENSION_REGISTRY_TOKEN` 环境变量提供,随后提交已保存候选: + +```bash +inkcre-ext docs publish --registry-url REGISTRY_URL --candidate ./docs-candidate +inkcre-ext docs show --registry-url REGISTRY_URL \ + --name example/my-extension --version 1.0.0 +``` + +首次发布的业务字段 `expected_etag` 为 null,表示只允许创建尚不存在的 scope。修订时先通过 `show` 读取对应 scope 的 ETag,审核当前内容,然后将完整的带引号 ETag 传给新候选的 `pack --if-match '"ETAG"'`;该 CLI 选项写入候选的 `expected_etag`,不会发送 HTTP `If-Match`。尚处于 preparing 的 Release 使用 `show --private`;文档可以准备,但在 Release 发布前不可公开阅读。 + +每次新修订必须生成新快照 ID;重试同一次发布则保留原 ZIP、metadata、ID 和写入前提。服务端识别已提交的完整候选并返回稳定回执,即使其他候选已经替换 current,也不会把旧快照重新设为 current。回执的 `committed_at` 与 `snapshot_etag` 描述该次历史提交;要判断当前文档或准备下一次修订,请使用 `show`。同 ID 改变已提交候选的任一身份字段会得到 409。 + +Toolkit 仅做 best-effort 交付:网络错误、500/502/503/504 或无法解析的回执最多触发一次原样重试,共两次前台请求,沿用每次 HTTP 请求的超时。仍无法获得回执时,以非零退出码报告“结果未知”;这不表示发布失败。保存的候选不会被删除,可以稍后原样重试。其他 HTTP 错误不会自动重试;409 的身份冲突或 stale precondition 需要作者检查并决定是否创建新的修订。工具不会后台重试、刷新 ETag 或承诺最终送达。旧快照继续服从其所属 Release 的生命周期:yanked 仍可读,blocked 拒绝历史内容和提交确认。 + +## 直接使用 HTTP + +[JSON Schema](../../contracts/documentation.schema.json) 定义 upload、publication、receipt、release 和 hosting;[OpenAPI](../../contracts/openapi.json) 定义路径、认证、响应和错误。`POST /v1/extensions/{namespace}/{name}/releases/{version}/documentation/{scope}` 接收 `metadata`(JSON 编码的 publication,必须含 `expected_etag`)和 `content`(ZIP 文件)两个 multipart 字段;`metadata` 不能作为带 filename 的文件字段发送。成功响应是提交回执,不携带 current 的 HTTP ETag。 + +`content_sha256` 是文件 manifest 的摘要,不是 ZIP 文件摘要。以文件路径为 key,每项包含文件字节的 `sha256`、字节数 `size` 和上表 `media_type`;对该对象执行 Python `json.dumps(manifest, sort_keys=True, separators=(",", ":"))` 的默认 ASCII 转义序列化,再对 UTF-8 字节计算 SHA-256。目录成员不进入 manifest。其他语言的实现应产生相同字节;Toolkit 的 `inspect_documentation` 提供可直接复用的实现。 + +400 表示输入或归档不合法,401/403 表示发布权限错误,404 表示目标不存在或不公开,409 表示 association/内容/候选身份冲突或写入前提不满足,411/413/415 表示传输不被接受,451 表示 Release 被封禁,503 表示文档托管尚未配置。更换内容指针只发生在所有资源 staging 成功之后;提交前失败不会改变原指针,提交后丢失响应不会撤销已完成的发布。 diff --git a/docs/30-unit-tdd/registry-control-plane.md b/docs/30-unit-tdd/registry-control-plane.md index 3947675..c2341ec 100644 --- a/docs/30-unit-tdd/registry-control-plane.md +++ b/docs/30-unit-tdd/registry-control-plane.md @@ -34,15 +34,53 @@ and materializes only the manifest's public path from canonical The generated [OpenAPI contract](../../contracts/openapi.json), JSON Schemas, models, routes, generated contracts, and build checks are the exact executable interface authorities. +## Extension Documentation + +Documentation is a separate projection of one exact Release, not another Distribution and not an +optional field added to `ReleaseRecord`. A set is addressed by Release and `global`, `python`, or +`module-federation` scope. Channel scopes require their corresponding Distribution association; +the global scope does not. Each publication submits a complete ZIP through POST, with a required +`expected_etag` business field: null creates an absent set, while the observed strong ETag permits +a correction. Every new publication uses a fresh opaque snapshot identity and origin. + +Registry validates normalized paths, admitted static media types, expansion limits, the HTML entry, +and the declared content digest. It stages every object before a short transaction locks the Release, +rechecks the precondition, records the immutable snapshot, and moves the current-set pointer. Failed +staging and stale editors cannot replace the previous pointer. The Toolkit saves the archive, +metadata, snapshot identity, and precondition as one candidate. The snapshot ETag fingerprints the +canonical target, metadata, and original precondition; the atomically committed snapshot is also +the publication receipt. An exact replay returns that historical receipt before checking current, +without staging again, changing the pointer, or changing its timestamp. Reusing an ID with a different +fingerprint conflicts. The receipt exposes `committed_at` and `snapshot_etag`, not a current-set HTTP +ETag. Authentication and blocked checks apply before acknowledging a replay. + +Toolkit makes at most two foreground attempts for transport failures, selected temporary 5xx, or an +invalid receipt. Exhaustion reports an unknown outcome and retains the saved candidate. Definitive +client errors are not retried. There is no background recovery, eventual-delivery guarantee, or +automatic adoption of a newer ETag. Staging failure can leave unreachable objects; commit receipts +confirm a past database transaction, not the continuing availability of every stored byte. + +作者的构建、上传限制、MIME 映射及恢复方式以[静态文档发布协议](documentation-admission.md)为准。JSON Schema 包含 upload、publication、receipt、release 和 hosting 契约;OpenAPI 的 multipart `metadata` 是 JSON 编码的 publication 文本字段,其 `contentSchema` 给出内部结构,HTTP 错误与写入前提也在 OpenAPI 中声明。 + +Management and content use separate origins. The stable management entry redirects to the selected +snapshot; files, root-relative assets, browser storage, and Service Workers remain confined to that +snapshot's origin. The content-origin middleware serves only GET and HEAD, never management routes, +credentials, author headers, redirects, or server execution. Exact and directory-index routes are +supported, with `.html` clean URLs; there is no SPA fallback. Published and yanked Release snapshots +remain readable, with a withdrawal warning on stable yanked entries. `preparing` is publisher-only, +and `blocked` denies discovery, stable entries, and every historical snapshot before revalidation. + ## Persistence and Security -持久化由 PostgreSQL 与 Tortoise ORM / asyncpg 承担。`service/database.py` 定义模型、业务唯一约束、外键和状态约束;`service/repository.py` 拥有查询与事务。Release、Distribution 和 File 使用独立内部主键,公开身份仍是 Extension Name / Version 或 Python Project / Version / Filename。内部 ID 不进入公开 URL 或契约。 +持久化由 PostgreSQL 与 Tortoise ORM / asyncpg 承担。`service/database.py` 定义模型、业务唯一约束、外键和状态约束;`service/repository.py` 拥有 Release、Distribution 与发布身份的查询和事务,`service/documentation.py` 的 `DocumentationRepository` 拥有文档查询、对象 staging 和 current-set 替换事务。两者由 lifespan 创建,共享同一个 `ArtifactStore`,各自持有进程内并发额度;文档仓储不依赖另一个仓储的实例或内部状态。Release、Distribution 和 File 使用独立内部主键,公开身份仍是 Extension Name / Version 或 Python Project / Version / Filename。内部 ID 不进入公开 URL 或契约。 + +文档指针由 PostgreSQL 的 `(snapshot_id, release_id, scope)` 复合外键保证归属一致。Tortoise 保留单列关系用于查询,复合外键由 `0004_documentation_ownership` 原生 DDL 补充;已有指针不能关联其他 Release/scope,所指快照也不能在保留该指针时改变归属。 准备关联时,namespace 行锁串行化同一发布者的身份创建,release 行锁保护关联和状态。唯一约束保证 Python Project / Version 不被另一个 release 占用;失败会回滚同一事务中的 Extension、Release 和关联创建。上传、发布、撤回和恢复均重新检查锁定后的 release 状态。关联、文件名和内容身份不可变,相同提交可以重试。模型的联合约束明确使用数据库外键列名;不依赖迁移生成器猜测关系字段的物理列名。 R2 的网络访问不持有数据库事务锁。boto3 的同步调用在线程中执行;上传先写内容寻址的 staging 对象,再在 PostgreSQL 事务中增加可见关联。发布前检查关联对象存在,文件大小一致。失败可能留下不可达的 staging 对象,它们不是公开状态的来源。每次文件读取先检查 release 可读性,再通过 S3 流式返回;HEAD 只读取对象元数据。响应使用 `Cache-Control: public, no-cache` 和内容 SHA-256 ETag;条件 GET / HEAD 在 release 可读、对象存在之后才能返回 304。缓存必须逐次重验证,blocked 的 451 响应使用 `no-store`。已经被客户端下载的内容无法通过服务端状态变化收回。 -应用启动只初始化连接池,不建表、不运行迁移、不创建凭据或示例数据。交付在启动新版本前运行包内 Tortoise 迁移,并仅允许向前执行。数据迁移工具只在旧 SQLite 快照读取边界使用 SQL,PostgreSQL 业务读写与发布凭据维护使用 ORM。角色与 GRANT 是 PostgreSQL 原生授权 DDL,由追加迁移维护;登录密码由可信交付端设置。运行角色 `registry_app` 仅获得七张业务表的 SELECT / INSERT / UPDATE / DELETE、对应序列的 USAGE 和 schema 的 USAGE,不能修改 schema、角色或迁移记录。后续新增表须在同一迁移显式授予所需权限,不自动授权全部未来表。旧 D1 历史保留用于导入验收,迁移工具不自动访问生产 D1。 +应用启动只初始化连接池,不建表、不运行迁移、不创建凭据或示例数据。交付在启动新版本前运行包内 Tortoise 迁移,并仅允许向前执行。数据迁移工具只在旧 SQLite 快照读取边界使用 SQL,PostgreSQL 业务读写与发布凭据维护使用 ORM。角色与 GRANT 是 PostgreSQL 原生授权 DDL,由追加迁移维护;登录密码由可信交付端设置。运行角色 `registry_app` 仅获得明确列出的业务表的 SELECT / INSERT / UPDATE / DELETE、对应序列的 USAGE 和 schema 的 USAGE,不能修改 schema、角色或迁移记录。后续新增表须在同一迁移显式授予所需权限,不自动授权全部未来表。旧 D1 历史保留用于导入验收,迁移工具不自动访问生产 D1。 Publisher 持有 namespace 范围内的发布能力;原始 token 只用于请求认证和运维命令的标准输入,数据库保存 SHA-256。匿名读取只能看到公开或撤回的 release,blocked 状态拒绝描述和原始文件访问。预览服务只获得所属数据库分支和 R2 桶的凭据;云平台控制凭据只属于可信交付控制器。 diff --git a/docs/40-deployment/local-development.md b/docs/40-deployment/local-development.md index ba1b3c3..e83bad5 100644 --- a/docs/40-deployment/local-development.md +++ b/docs/40-deployment/local-development.md @@ -7,7 +7,9 @@ pdm install --frozen-lockfile pnpm install --frozen-lockfile ``` -配置 `DATABASE_URL`、`PUBLIC_ORIGIN`、`S3_ENDPOINT_URL`、`S3_BUCKET` 和标准 AWS 凭据 `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`。数据库必须是 PostgreSQL;远程连接默认验证 TLS,localhost 可以不启用 TLS。`PUBLIC_ORIGIN` 是 HTTPS origin,本地允许 localhost HTTP,不含路径。文件配置是 dotenv 数据,不应当作 shell 脚本执行;应用从进程环境读取配置。 +配置 `DATABASE_URL`、`PUBLIC_ORIGIN`、`S3_ENDPOINT_URL`、`S3_BUCKET` 和标准 AWS 凭据 `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`。启用插件文档托管时,另设 `DOCUMENTATION_ORIGIN_TEMPLATE`,例如本地的 `http://{snapshot}.docs.localhost`;模板必须把 32 位快照标识放在第一个 DNS label,并且不能与管理 origin 共站。数据库必须是 PostgreSQL;远程连接默认验证 TLS,localhost 可以不启用 TLS。`PUBLIC_ORIGIN` 是 HTTPS origin,本地允许 localhost HTTP,不含路径。文档内容 origin 在生产必须使用 HTTPS;本地只允许 `.localhost`。文件配置是 dotenv 数据,不应当作 shell 脚本执行;应用从进程环境读取配置。 + +生产内容域必须使用不同的可注册域,例如 `registry.example.com` 与 `{snapshot}.exampleusercontent.net`;`{snapshot}.docs.example.com` 会被拒绝。配置校验使用 `publicsuffixlist` 包内的 Public Suffix List(含 private section),不在启动时访问网络;PSL 随锁定依赖升级更新,未知后缀和没有固定可注册域的模板被拒绝。域名应采用 ASCII/Punycode 配置。仅管理端是 HTTP loopback、内容端是 HTTP `.localhost` 的配对配置获得本地例外。内容域必须专用于不可信静态内容,不得部署 SSO、转发认证 Cookie,或与其他持有认证 Cookie 的服务共用父域;PSL 校验只验证所配置的管理域与内容域,无法盘点其他服务的 Cookie 配置。 ```bash pnpm db:migrate @@ -24,10 +26,12 @@ pdm run python -m inkcre_extension_registry REGISTRY_TEST_DATABASE_URL=postgres://registry:registry-check@localhost:5432/registry_check pnpm check ``` -CI 创建 PostgreSQL 17 service;本地使用独立、一次性的 PostgreSQL 实例或 Neon 测试分支。授权迁移会创建实例级角色,因此仅在共享实例内增加一个空数据库不足以隔离这些检查。不得指向共享开发、PR 预览或生产库。验收运行真实迁移,检查模型漂移,通过真实 ORM / PostgreSQL 和本地 Moto S3 服务验证发布、重试、并发冲突、失败回滚、私有分页、blocked 读取、GET / HEAD 和 ETag 重验证。迁移使用 owner,业务验收使用 `registry_app`,并验证创建表、修改业务表、写迁移记录和创建角色均被拒绝。D1 导入验收使用临时 SQLite 快照,比较所有逻辑字段的内容摘要。测试仅在空库插入测试记录,并在退出时清理;失败可能保留 schema 和迁移记录,目标库仍须属于一次性验证生命周期。 +CI 创建 PostgreSQL 17 service;本地使用独立、一次性的 PostgreSQL 实例或 Neon 测试分支。授权迁移会创建实例级角色,因此仅在共享实例内增加一个空数据库不足以隔离这些检查。不得指向共享开发、PR 预览或生产库。验收运行真实迁移,检查模型漂移,通过真实 ORM / PostgreSQL 和本地 Moto S3 服务验证发布、重试、并发冲突、失败回滚、私有分页、文档条件替换与快照域隔离、blocked 读取、GET / HEAD 和 ETag 重验证。迁移使用 owner,业务验收使用 `registry_app`,并验证创建表、修改业务表、写迁移记录和创建角色均被拒绝。D1 导入验收使用临时 SQLite 快照,比较所有逻辑字段的内容摘要。测试仅在空库插入测试记录,并在退出时清理;失败可能保留 schema 和迁移记录,目标库仍须属于一次性验证生命周期。 `pnpm registry:check` 单独执行上述验收,仍需要同一个测试变量。CI 另外拒绝修改或删除已在 base 中存在的迁移,并构建完整服务镜像。测试数据不会写入共享预览。 Registry 通过不可变 Git commit 安装 `InKCre/ui` 的 `@inkcre/ui-web` 公共 Sass 包。修改 `web/registry.scss` 后运行 `pnpm web:build`;生成 CSS 提交到源码,并由 `pnpm web:check` 比对。模板、CSS 和 JavaScript 都随 Python wheel 打包。视觉验收覆盖空态、详情版本选择、Publisher、键盘操作、窄屏和明暗主题。 Toolkit、Core Runtime 与 Web Runtime 保持各自的版本和发布机制。`pnpm build` 构建这些独立产物;Registry 的容器部署不会发布它们,也不会引入单独的前端部署流程。 + +`pnpm packages:check` 在构建后创建独立虚拟环境,通过 wheel metadata 解析安装 Registry 与本次 Toolkit 候选,再执行 `pip check` 和服务 import;它不加载 workspace 源码,防止可编辑安装掩盖错误依赖边界。该检查需要包索引访问并随 `pnpm check` 运行。Registry 的文档 API 要求 Toolkit `>=0.3,<0.4`,Toolkit 版本通过 Changie 准备,合并后的正常 release 流程才发布产物。 diff --git a/docs/_shared b/docs/_shared index 528d735..63b1673 160000 --- a/docs/_shared +++ b/docs/_shared @@ -1 +1 @@ -Subproject commit 528d735d5b630445c49e069f842aa4bf22be5b0e +Subproject commit 63b1673a873782b889797d760055bc201c3a5dcf diff --git a/package.json b/package.json index 387b7f3..ccb37a5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "scripts": { "build": "SOURCE_DATE_EPOCH=315532800 pdm build --no-sdist --dest \"$INIT_CWD/dist\" && SOURCE_DATE_EPOCH=315532800 pdm build --project toolkit --no-sdist --no-clean --dest \"$INIT_CWD/dist\" && SOURCE_DATE_EPOCH=315532800 pdm build --project runtimes/core-py --no-sdist --no-clean --dest \"$INIT_CWD/dist\" && pnpm --filter @inkcre/extension-runtime-client-web build", "changeset": "changeset", - "check": "pnpm web:check && pnpm contracts:check && pnpm format:check && pnpm lint && pnpm type-check && pnpm build && pnpm registry:check", + "check": "pnpm web:check && pnpm contracts:check && pnpm format:check && pnpm lint && pnpm type-check && pnpm build && pnpm --filter @inkcre/extension-runtime-client-web exec node check-boundaries.mjs && pnpm packages:check && pnpm registry:check", + "packages:check": "pdm run python scripts/check_packages.py", "contracts:check": "pdm run python scripts/generate_contracts.py --check && pdm run python runtimes/core-py/scripts/generate_contracts.py --check && pnpm --filter @inkcre/extension-runtime-client-web check:bindings", "contracts:generate": "pdm run python scripts/generate_contracts.py && pdm run python runtimes/core-py/scripts/generate_contracts.py && pnpm --filter @inkcre/extension-runtime-client-web generate:bindings", "format": "pdm run ruff format . && prettier --write .", diff --git a/pdm.lock b/pdm.lock index 98559d0..0f790cd 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:53d384c8cf5d2856e9e675c515a0b98e1b27199d463598923a09c3e9907d5980" +content_hash = "sha256:5597c6ce7689a33aa9b862a4fbedaf1367988150d44be8e092ca73185a564aba" [[metadata.targets]] requires_python = ">=3.12,<3.14" @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "inkcre-extension-toolkit" -version = "0.2.1" +version = "0.3.0" requires_python = ">=3.12,<3.14" editable = true path = "./toolkit" @@ -1150,6 +1150,17 @@ files = [ {file = "psycopg-3.3.5.tar.gz", hash = "sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4"}, ] +[[package]] +name = "publicsuffixlist" +version = "1.0.2.20260919" +requires_python = ">=3.5" +summary = "publicsuffixlist implement" +groups = ["default"] +files = [ + {file = "publicsuffixlist-1.0.2.20260919-py2.py3-none-any.whl", hash = "sha256:60cd2c1e09a2bd66044e562d4f977b96f21e6b151d161a02e41e001fff42fb27"}, + {file = "publicsuffixlist-1.0.2.20260919.tar.gz", hash = "sha256:d9383e510e3464fde4478b9d0f97a24911b6c2bd397aa149c84a88534e98e3eb"}, +] + [[package]] name = "py-partiql-parser" version = "0.6.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a733744..8b0d560 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 0.99.0(typescript@5.9.3) '@inkcre/ui-web': specifier: github:InKCre/ui#4eceec4c60345a52a08545555ebce9ab95053beb&path:/packages/web - version: https://codeload.github.com/InKCre/ui/tar.gz/4eceec4c60345a52a08545555ebce9ab95053beb#path:/packages/web + version: https://codeload.github.com/InKCre/ui/tar.gz/4eceec4c60345a52a08545555ebce9ab95053beb#path:/packages/web(@vueuse/core@14.1.0(vue@3.5.18(typescript@5.9.3)))(dayjs@1.11.23)(vue@3.5.18(typescript@5.9.3)) prettier: specifier: 3.6.2 version: 3.6.2 @@ -42,25 +42,33 @@ importers: specifier: 4.1.12 version: 4.1.12 devDependencies: + '@inkcre/core': + specifier: github:InKCre/client-web#f29a407ae02980b221e9419bb38421aa95805771&path:/packages/core + version: https://codeload.github.com/InKCre/client-web/tar.gz/f29a407ae02980b221e9419bb38421aa95805771#path:/packages/core(@vueuse/core@14.1.0(vue@3.5.18(typescript@5.9.3)))(dotenv@17.4.2)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.18(typescript@5.9.3)))(vue@3.5.18(typescript@5.9.3))(yaml@2.9.0) '@module-federation/runtime': specifier: 0.21.6 version: 0.21.6 '@types/semver': specifier: 7.7.1 version: 7.7.1 + '@vueuse/core': + specifier: 14.1.0 + version: 14.1.0(vue@3.5.18(typescript@5.9.3)) + pinia: + specifier: 3.0.3 + version: 3.0.3(typescript@5.9.3)(vue@3.5.18(typescript@5.9.3)) tsdown: - specifier: 0.15.11 - version: 0.15.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(typescript@5.9.3) + specifier: 0.22.13 + version: 0.22.13(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 + vue: + specifier: 3.5.18 + version: 3.5.18(typescript@5.9.3) packages: - '@babel/generator@7.29.8': - resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -194,18 +202,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -549,6 +548,14 @@ packages: cpu: [x64] os: [win32] + '@inkcre/core@https://codeload.github.com/InKCre/client-web/tar.gz/f29a407ae02980b221e9419bb38421aa95805771#path:/packages/core': + resolution: {gitHosted: true, path: /packages/core, tarball: https://codeload.github.com/InKCre/client-web/tar.gz/f29a407ae02980b221e9419bb38421aa95805771} + version: 0.3.0 + peerDependencies: + '@vueuse/core': ^14.0.0 + pinia: ^3.0.0 + vue: ^3.5.0 + '@inkcre/ui-web@https://codeload.github.com/InKCre/ui/tar.gz/4eceec4c60345a52a08545555ebce9ab95053beb#path:/packages/web': resolution: {gitHosted: true, integrity: sha512-DGaEKHQhalTLpClcIyTYBvCugpkb5uYHcyOhWCUuujGoPLj0ICvOrjkfxgE8sXJuhQhTT+VXzbiEOXsLMxH79w==, path: /packages/web, tarball: https://codeload.github.com/InKCre/ui/tar.gz/4eceec4c60345a52a08545555ebce9ab95053beb} version: 1.4.0 @@ -572,9 +579,6 @@ packages: vue-router: optional: true - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -582,9 +586,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -619,18 +620,8 @@ packages: '@module-federation/sdk@0.21.6': resolution: {integrity: sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==} - '@napi-rs/wasm-runtime@1.2.3': - resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - - '@oxc-project/types@0.127.0': - resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - - '@oxc-project/types@0.95.0': - resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} + '@oxc-project/types@0.150.0': + resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} '@parcel/watcher-android-arm64@2.6.0': resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} @@ -730,193 +721,104 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@rolldown/binding-android-arm64@1.0.0-beta.45': - resolution: {integrity: sha512-bfgKYhFiXJALeA/riil908+2vlyWGdwa7Ju5S+JgWZYdR4jtiPOGdM6WLfso1dojCh+4ZWeiTwPeV9IKQEX+4g==} + '@rolldown/binding-android-arm-eabi@1.2.9': + resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==} engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] + cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + '@rolldown/binding-android-arm64@1.2.9': + resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.45': - resolution: {integrity: sha512-xjCv4CRVsSnnIxTuyH1RDJl5OEQ1c9JYOwfDAHddjJDxCw46ZX9q80+xq7Eok7KC4bRSZudMJllkvOKv0T9SeA==} + '@rolldown/binding-darwin-arm64@1.2.9': + resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0-beta.45': - resolution: {integrity: sha512-ddcO9TD3D/CLUa/l8GO8LHzBOaZqWg5ClMy3jICoxwCuoz47h9dtqPsIeTiB6yR501LQTeDsjA4lIFd7u3Ljfw==} + '@rolldown/binding-darwin-x64@1.2.9': + resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.17': - resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0-beta.45': - resolution: {integrity: sha512-MBTWdrzW9w+UMYDUvnEuh0pQvLENkl2Sis15fHTfHVW7ClbGuez+RWopZudIDEGkpZXdeI4CkRXk+vdIIebrmg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-freebsd-x64@1.0.0-rc.17': - resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + '@rolldown/binding-freebsd-x64@1.2.9': + resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.45': - resolution: {integrity: sha512-4YgoCFiki1HR6oSg+GxxfzfnVCesQxLF1LEnw9uXS/MpBmuog0EOO2rYfy69rWP4tFZL9IWp6KEfGZLrZ7aUog==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': - resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.45': - resolution: {integrity: sha512-LE1gjAwQRrbCOorJJ7LFr10s5vqYf5a00V5Ea9wXcT2+56n5YosJkcp8eQ12FxRBv2YX8dsdQJb+ZTtYJwb6XQ==} + '@rolldown/binding-linux-arm64-gnu@1.2.9': + resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.45': - resolution: {integrity: sha512-tdy8ThO/fPp40B81v0YK3QC+KODOmzJzSUOO37DinQxzlTJ026gqUSOM8tzlVixRbQJltgVDCTYF8HNPRErQTA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': - resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + '@rolldown/binding-linux-arm64-musl@1.2.9': + resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + '@rolldown/binding-linux-s390x-gnu@1.2.9': + resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.45': - resolution: {integrity: sha512-lS082ROBWdmOyVY/0YB3JmsiClaWoxvC+dA8/rbhyB9VLkvVEaihLEOr4CYmrMse151C4+S6hCw6oa1iewox7g==} + '@rolldown/binding-linux-x64-gnu@1.2.9': + resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': - resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.0-beta.45': - resolution: {integrity: sha512-Hi73aYY0cBkr1/SvNQqH8Cd+rSV6S9RB5izCv0ySBcRnd/Wfn5plguUoGYwBnhHgFbh6cPw9m2dUVBR6BG1gxA==} + '@rolldown/binding-linux-x64-musl@1.2.9': + resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': - resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.0-beta.45': - resolution: {integrity: sha512-fljEqbO7RHHogNDxYtTzr+GNjlfOx21RUyGmF+NrkebZ8emYYiIqzPxsaMZuRx0rgZmVmliOzEp86/CQFDKhJQ==} + '@rolldown/binding-openharmony-arm64@1.2.9': + resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': - resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + '@rolldown/binding-win32-arm64-msvc@1.2.9': + resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0-beta.45': - resolution: {integrity: sha512-ZJDB7lkuZE9XUnWQSYrBObZxczut+8FZ5pdanm8nNS1DAo8zsrPuvGwn+U3fwU98WaiFsNrA4XHngesCGr8tEQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': - resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.45': - resolution: {integrity: sha512-zyzAjItHPUmxg6Z8SyRhLdXlJn3/D9KL5b9mObUrBHhWS/GwRH4665xCiFqeuktAhhWutqfc+rOV2LjK4VYQGQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': - resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.45': - resolution: {integrity: sha512-wODcGzlfxqS6D7BR0srkJk3drPwXYLu7jPHN27ce2c4PUnVVmJnp9mJzUQGT4LpmHmmVdMZ+P6hKvyTGBzc1CA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.45': - resolution: {integrity: sha512-wiU40G1nQo9rtfvF9jLbl79lUgjfaD/LTyUEw2Wg/gdF5OhjzpKMVugZQngO+RNdwYaNj+Fs+kWBWfp4VXPMHA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': - resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + '@rolldown/binding-win32-x64-msvc@1.2.9': + resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.45': - resolution: {integrity: sha512-Le9ulGCrD8ggInzWw/k2J8QcbPz7eGIOWqfJ2L+1R0Opm7n6J37s2hiDWlh6LJN0Lk9L5sUzMvRHKW7UxBZsQA==} - - '@rolldown/pluginutils@1.0.0-rc.17': - resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} @@ -925,8 +827,9 @@ packages: '@speed-highlight/core@1.2.23': resolution: {integrity: sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==} - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@supabase/postgrest-js@2.116.0': + resolution: {integrity: sha512-kGpVZTDHxFTJS3tu+rU0iTAZ+4U0bcLVjxwCk8f3gRhjw3qdCZjTBlgYvc4kGH2XccmAzbkKwXL/mrNHMGSc+A==} + engines: {node: '>=22.0.0'} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -934,9 +837,198 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@vue/compiler-core@3.5.18': + resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} + + '@vue/compiler-dom@3.5.18': + resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==} + + '@vue/compiler-sfc@3.5.18': + resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==} + + '@vue/compiler-ssr@3.5.18': + resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==} + + '@vue/devtools-api@7.7.10': + resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==} + + '@vue/devtools-kit@7.7.10': + resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==} + + '@vue/devtools-shared@7.7.10': + resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==} + + '@vue/reactivity@3.5.18': + resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==} + + '@vue/runtime-core@3.5.18': + resolution: {integrity: sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==} + + '@vue/runtime-dom@3.5.18': + resolution: {integrity: sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==} + + '@vue/server-renderer@3.5.18': + resolution: {integrity: sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==} + peerDependencies: + vue: 3.5.18 + + '@vue/shared@3.5.18': + resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} + '@vue/shared@3.5.25': resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + '@vueuse/core@14.1.0': + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.1.0': + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + + '@vueuse/shared@14.1.0': + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + peerDependencies: + vue: ^3.5.0 + + '@yuku-codegen/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==} + cpu: [arm64] + os: [win32] + + '@yuku-codegen/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.8.7': + resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -948,10 +1040,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - ast-kit@2.2.0: - resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} - engines: {node: '>=20.19.0'} - birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} @@ -970,10 +1058,6 @@ packages: magicast: optional: true - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -1001,18 +1085,19 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + copy-anything@4.1.1: + resolution: {integrity: sha512-AoT6Imdr98feSpFfmFwTFN73ccdr7uFPf27cBCgYvyyRyn1BzLRxMvrHNmwXO5LJMddRy4Rdhw2b1h7vSMKsEw==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} @@ -1036,17 +1121,13 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} - dts-resolver@2.1.3: - resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} - engines: {node: '>=20.19.0'} + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} peerDependencies: oxc-resolver: '>=11.0.0' peerDependenciesMeta: @@ -1057,6 +1138,10 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -1065,6 +1150,9 @@ packages: engines: {node: '>=18'} hasBin: true + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + exsolve@1.1.1: resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} @@ -1094,8 +1182,9 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - get-tsconfig@4.14.2: - resolution: {integrity: sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==} + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} giget@3.3.1: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} @@ -1104,6 +1193,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hookable@6.1.2: + resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==} + human-id@4.2.0: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true @@ -1114,6 +1206,10 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + import-without-cache@0.4.1: + resolution: {integrity: sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A==} + engines: {node: ^22.18.0 || >=24.0.0} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1150,15 +1246,13 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-yaml@4.3.1: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -1176,8 +1270,13 @@ packages: resolution: {integrity: sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA==} engines: {node: '>=22.0.0'} - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -1206,6 +1305,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} @@ -1216,9 +1318,22 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -1245,32 +1360,30 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - rolldown-plugin-dts@0.17.8: - resolution: {integrity: sha512-76EEBlhF00yeY6M7VpMkWKI4r9WjuoMiOGey7j4D6zf3m0BR+ZrrY9hvSXdueJ3ljxSLq4DJBKFpX/X9+L7EKw==} - engines: {node: '>=20.19.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rolldown-plugin-dts@0.27.14: + resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} + engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: - '@ts-macro/tsc': ^0.3.6 - '@typescript/native-preview': '>=7.0.0-dev.20250601.1' - rolldown: ^1.0.0-beta.44 - typescript: ^5.0.0 - vue-tsc: ~3.1.0 + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + rolldown: ^1.0.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 peerDependenciesMeta: - '@ts-macro/tsc': - optional: true '@typescript/native-preview': optional: true + '@volar/typescript': + optional: true typescript: optional: true vue-tsc: optional: true - rolldown@1.0.0-beta.45: - resolution: {integrity: sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rolldown@1.0.0-rc.17: - resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + rolldown@1.2.9: + resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1316,6 +1429,14 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -1332,31 +1453,47 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - tsdown@0.15.11: - resolution: {integrity: sha512-7k2OglWWt6LzvJKwEf1izbGvETvVfPYRBr9JgEYVRnz/R9LeJSp+B51FUMO46wUeEGtZ1jA3E3PtWWLlq3iygA==} - engines: {node: '>=20.19.0'} + tsdown@0.22.13: + resolution: {integrity: sha512-XaYFhtiKRUvTpXv/YAehsHdbEb3LN/iMlzjSINbjlaATtXN2zVPKox2STKhcyFPlh++8Zg7suNN27E679IfAUA==} + engines: {node: ^22.18.0 || >=24.11.0} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 - publint: ^0.3.0 - typescript: ^5.0.0 - unplugin-lightningcss: ^0.4.0 + '@tsdown/css': 0.22.13 + '@tsdown/exe': 0.22.13 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 unplugin-unused: ^0.5.0 + unrun: '*' peerDependenciesMeta: '@arethetypeswrong/core': optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true publint: optional: true - typescript: + tsx: optional: true - unplugin-lightningcss: + typescript: optional: true unplugin-unused: optional: true + unrun: + optional: true tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1365,9 +1502,6 @@ packages: unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} - unconfig@7.5.0: - resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} - undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} @@ -1375,14 +1509,16 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - unrun@0.2.39: - resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==} - engines: {node: '>=20.19.0'} - hasBin: true + verkit@0.1.2: + resolution: {integrity: sha512-WqkT8n3hqizuCu71W3bUzf5fjBmkbXcudsehe/NbxA8PgqoKnSOY5K0Ba2ckg1qaRaSpSz7as/n9K1R9JXjQKg==} + engines: {node: '>=18.12.0'} + + vue@3.5.18: + resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==} peerDependencies: - synckit: ^0.11.11 + typescript: '*' peerDependenciesMeta: - synckit: + typescript: optional: true which@2.0.2: @@ -1432,19 +1568,44 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + yuku-ast@0.8.7: + resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==} + + yuku-codegen@0.8.7: + resolution: {integrity: sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==} + + yuku-parser@0.8.7: + resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + + zod-class@0.0.18: + resolution: {integrity: sha512-wWWa6u56ualID4zf0DGgDhdDjYI7lP3XphIFBam+sgodMZOrad8lxAFchqBGq1uuGPAel/hIjHITLc5D+TowXg==} + peerDependencies: + zod: ^3 + + zod-config@1.4.0: + resolution: {integrity: sha512-1+SlrNzoWeid0/t+7hvxmKVK8gVYTiiWjmXYodEdUTADNrmMnvClmkzs2ht0HfZsmDgOv1SYHFwrvyiwsN+3xA==} + engines: {node: '>=14.0.0'} + peerDependencies: + dotenv: '>=15' + json5: '>=2' + smol-toml: ^1.x + yaml: ^2.x + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + dotenv: + optional: true + json5: + optional: true + smol-toml: + optional: true + yaml: + optional: true + zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} snapshots: - '@babel/generator@7.29.8': - dependencies: - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} @@ -1601,27 +1762,11 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true @@ -1856,24 +2001,35 @@ snapshots: '@img/sharp-win32-x64@0.35.2': optional: true - '@inkcre/ui-web@https://codeload.github.com/InKCre/ui/tar.gz/4eceec4c60345a52a08545555ebce9ab95053beb#path:/packages/web': + '@inkcre/core@https://codeload.github.com/InKCre/client-web/tar.gz/f29a407ae02980b221e9419bb38421aa95805771#path:/packages/core(@vueuse/core@14.1.0(vue@3.5.18(typescript@5.9.3)))(dotenv@17.4.2)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.18(typescript@5.9.3)))(vue@3.5.18(typescript@5.9.3))(yaml@2.9.0)': dependencies: - '@vue/shared': 3.5.25 + '@supabase/postgrest-js': 2.116.0 + '@vueuse/core': 14.1.0(vue@3.5.18(typescript@5.9.3)) + dayjs: 1.11.23 + jose: 5.10.0 + pinia: 3.0.3(typescript@5.9.3)(vue@3.5.18(typescript@5.9.3)) + semver: 7.8.5 + vue: 3.5.18(typescript@5.9.3) + zod: 4.1.12 + zod-class: 0.0.18(zod@4.1.12) + zod-config: 1.4.0(dotenv@17.4.2)(yaml@2.9.0)(zod@4.1.12) + transitivePeerDependencies: + - dotenv + - json5 + - smol-toml + - yaml - '@jridgewell/gen-mapping@0.3.13': + '@inkcre/ui-web@https://codeload.github.com/InKCre/ui/tar.gz/4eceec4c60345a52a08545555ebce9ab95053beb#path:/packages/web(@vueuse/core@14.1.0(vue@3.5.18(typescript@5.9.3)))(dayjs@1.11.23)(vue@3.5.18(typescript@5.9.3))': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@vue/shared': 3.5.25 + '@vueuse/core': 14.1.0(vue@3.5.18(typescript@5.9.3)) + dayjs: 1.11.23 + vue: 3.5.18(typescript@5.9.3) '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -1913,23 +2069,7 @@ snapshots: '@module-federation/sdk@0.21.6': {} - '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.11.3 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.127.0': {} - - '@oxc-project/types@0.95.0': {} + '@oxc-project/types@0.150.0': {} '@parcel/watcher-android-arm64@2.6.0': optional: true @@ -2006,120 +2146,227 @@ snapshots: dependencies: quansync: 1.0.0 - '@rolldown/binding-android-arm64@1.0.0-beta.45': + '@rolldown/binding-android-arm-eabi@1.2.9': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.17': + '@rolldown/binding-android-arm64@1.2.9': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.45': + '@rolldown/binding-darwin-arm64@1.2.9': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + '@rolldown/binding-darwin-x64@1.2.9': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.45': + '@rolldown/binding-freebsd-x64@1.2.9': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.17': + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.45': + '@rolldown/binding-linux-arm64-gnu@1.2.9': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + '@rolldown/binding-linux-arm64-musl@1.2.9': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.45': + '@rolldown/binding-linux-ppc64-gnu@1.2.9': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + '@rolldown/binding-linux-s390x-gnu@1.2.9': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.45': + '@rolldown/binding-linux-x64-gnu@1.2.9': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + '@rolldown/binding-linux-x64-musl@1.2.9': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.45': + '@rolldown/binding-openharmony-arm64@1.2.9': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + '@rolldown/binding-win32-arm64-msvc@1.2.9': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + '@rolldown/binding-win32-x64-msvc@1.2.9': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + '@rolldown/pluginutils@1.0.1': {} + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.23': {} + + '@supabase/postgrest-js@2.116.0': + dependencies: + tslib: 2.8.1 + + '@types/json-schema@7.0.15': {} + + '@types/semver@7.7.1': {} + + '@types/web-bluetooth@0.0.21': {} + + '@vue/compiler-core@3.5.18': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.18 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.18': + dependencies: + '@vue/compiler-core': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/compiler-sfc@3.5.18': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.18 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.18': + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/devtools-api@7.7.10': + dependencies: + '@vue/devtools-kit': 7.7.10 + + '@vue/devtools-kit@7.7.10': + dependencies: + '@vue/devtools-shared': 7.7.10 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.10': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.18': + dependencies: + '@vue/shared': 3.5.18 + + '@vue/runtime-core@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/runtime-dom@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/runtime-core': 3.5.18 + '@vue/shared': 3.5.18 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.18(vue@3.5.18(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + vue: 3.5.18(typescript@5.9.3) + + '@vue/shared@3.5.18': {} + + '@vue/shared@3.5.25': {} + + '@vueuse/core@14.1.0(vue@3.5.18(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.1.0 + '@vueuse/shared': 14.1.0(vue@3.5.18(typescript@5.9.3)) + vue: 3.5.18(typescript@5.9.3) + + '@vueuse/metadata@14.1.0': {} + + '@vueuse/shared@14.1.0(vue@3.5.18(typescript@5.9.3))': + dependencies: + vue: 3.5.18(typescript@5.9.3) + + '@yuku-codegen/binding-android-arm64@0.8.7': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.45': + '@yuku-codegen/binding-darwin-arm64@0.8.7': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + '@yuku-codegen/binding-darwin-x64@0.8.7': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.45': + '@yuku-codegen/binding-freebsd-x64@0.8.7': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.45': + '@yuku-codegen/binding-linux-arm-musl@0.8.7': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)': - dependencies: - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.45': + '@yuku-codegen/binding-linux-x64-musl@0.8.7': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + '@yuku-codegen/binding-win32-arm64@0.8.7': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.45': + '@yuku-codegen/binding-win32-x64@0.8.7': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.45': + '@yuku-parser/binding-android-arm64@0.8.7': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + '@yuku-parser/binding-darwin-arm64@0.8.7': optional: true - '@rolldown/pluginutils@1.0.0-beta.45': {} + '@yuku-parser/binding-darwin-x64@0.8.7': + optional: true - '@rolldown/pluginutils@1.0.0-rc.17': {} + '@yuku-parser/binding-freebsd-x64@0.8.7': + optional: true - '@sindresorhus/is@7.2.0': {} + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + optional: true - '@speed-highlight/core@1.2.23': {} + '@yuku-parser/binding-linux-arm-musl@0.8.7': + optional: true - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': optional: true - '@types/json-schema@7.0.15': {} + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + optional: true - '@types/semver@7.7.1': {} + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + optional: true - '@vue/shared@3.5.25': {} + '@yuku-parser/binding-linux-x64-musl@0.8.7': + optional: true + + '@yuku-parser/binding-win32-arm64@0.8.7': + optional: true + + '@yuku-parser/binding-win32-x64@0.8.7': + optional: true + + '@yuku-toolchain/types@0.8.7': {} ansi-colors@4.1.3: {} @@ -2127,11 +2374,6 @@ snapshots: argparse@2.0.1: {} - ast-kit@2.2.0: - dependencies: - '@babel/parser': 7.29.8 - pathe: 2.0.3 - birpc@2.9.0: {} blake3-wasm@2.1.5: {} @@ -2155,8 +2397,6 @@ snapshots: pkg-types: 2.3.1 rc9: 3.0.1 - cac@6.7.14: {} - cac@7.0.0: {} chokidar@4.0.3: @@ -2175,15 +2415,17 @@ snapshots: cookie@1.1.1: {} + copy-anything@4.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3: - dependencies: - ms: 2.1.3 + csstype@3.2.3: {} + + dayjs@1.11.23: {} default-browser-id@5.0.1: {} @@ -2200,14 +2442,14 @@ snapshots: detect-libc@2.1.2: {} - diff@8.0.4: {} - dotenv@17.4.2: {} - dts-resolver@2.1.3: {} + dts-resolver@3.0.0: {} empathic@2.0.1: {} + entities@4.5.0: {} + error-stack-parser-es@1.0.5: {} esbuild@0.28.1: @@ -2239,6 +2481,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + estree-walker@2.0.2: {} + exsolve@1.1.1: {} fast-string-truncated-width@3.0.3: {} @@ -2262,7 +2506,7 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-tsconfig@4.14.2: + get-tsconfig@5.0.0-beta.5: dependencies: resolve-pkg-maps: 1.0.0 @@ -2270,12 +2514,16 @@ snapshots: hookable@5.5.3: {} + hookable@6.1.2: {} + human-id@4.2.0: {} immutable@5.1.9: {} import-meta-resolve@4.2.0: {} + import-without-cache@0.4.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: @@ -2302,12 +2550,12 @@ snapshots: jju@1.4.0: {} + jose@5.10.0: {} + js-yaml@4.3.1: dependencies: argparse: 2.0.1 - jsesc@3.1.0: {} - jsonc-parser@3.3.1: {} kleur@4.1.5: {} @@ -2333,7 +2581,9 @@ snapshots: - bufferutil - utf-8-validate - ms@2.1.3: {} + mitt@3.0.1: {} + + nanoid@3.3.19: {} node-addon-api@7.1.1: optional: true @@ -2359,18 +2609,33 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@1.0.0: {} + perfect-debounce@2.1.0: {} picocolors@1.1.1: {} picomatch@4.0.5: {} + pinia@3.0.3(typescript@5.9.3)(vue@3.5.18(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.10 + vue: 3.5.18(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + pkg-types@2.3.1: dependencies: confbox: 0.2.4 exsolve: 1.1.1 pathe: 2.0.3 + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + powershell-utils@0.1.0: {} prettier@3.6.2: {} @@ -2388,66 +2653,42 @@ snapshots: resolve-pkg-maps@1.0.0: {} - rolldown-plugin-dts@0.17.8(rolldown@1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3))(typescript@5.9.3): + rfdc@1.4.1: {} + + rolldown-plugin-dts@0.27.14(rolldown@1.2.9)(typescript@5.9.3): dependencies: - '@babel/generator': 7.29.8 - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 - ast-kit: 2.2.0 - birpc: 2.9.0 - dts-resolver: 2.1.3 - get-tsconfig: 4.14.2 - magic-string: 0.30.21 + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 obug: 2.1.4 - rolldown: 1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) + rolldown: 1.2.9 + yuku-ast: 0.8.7 + yuku-codegen: 0.8.7 + yuku-parser: 0.8.7 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - rolldown@1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3): + rolldown@1.2.9: dependencies: - '@oxc-project/types': 0.95.0 - '@rolldown/pluginutils': 1.0.0-beta.45 + '@oxc-project/types': 0.150.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.45 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.45 - '@rolldown/binding-darwin-x64': 1.0.0-beta.45 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.45 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.45 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.45 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.45 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.45 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.45 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.45 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.45 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.45 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.45 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - rolldown@1.0.0-rc.17: - dependencies: - '@oxc-project/types': 0.127.0 - '@rolldown/pluginutils': 1.0.0-rc.17 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.17 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 - '@rolldown/binding-darwin-x64': 1.0.0-rc.17 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + '@rolldown/binding-android-arm-eabi': 1.2.9 + '@rolldown/binding-android-arm64': 1.2.9 + '@rolldown/binding-darwin-arm64': 1.2.9 + '@rolldown/binding-darwin-x64': 1.2.9 + '@rolldown/binding-freebsd-x64': 1.2.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.9 + '@rolldown/binding-linux-arm64-gnu': 1.2.9 + '@rolldown/binding-linux-arm64-musl': 1.2.9 + '@rolldown/binding-linux-ppc64-gnu': 1.2.9 + '@rolldown/binding-linux-s390x-gnu': 1.2.9 + '@rolldown/binding-linux-x64-gnu': 1.2.9 + '@rolldown/binding-linux-x64-musl': 1.2.9 + '@rolldown/binding-openharmony-arm64': 1.2.9 + '@rolldown/binding-win32-arm64-msvc': 1.2.9 + '@rolldown/binding-win32-x64-msvc': 1.2.9 run-applescript@7.1.0: {} @@ -2507,6 +2748,12 @@ snapshots: source-map-js@1.2.1: {} + speakingurl@14.0.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.1.1 + supports-color@10.2.2: {} tinyexec@1.3.0: {} @@ -2518,37 +2765,34 @@ snapshots: tree-kill@1.2.2: {} - tsdown@0.15.11(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(typescript@5.9.3): + tsdown@0.22.13(typescript@5.9.3): dependencies: ansis: 4.3.1 - cac: 6.7.14 - chokidar: 4.0.3 - debug: 4.4.3 - diff: 8.0.4 + cac: 7.0.0 + defu: 6.1.7 empathic: 2.0.1 - hookable: 5.5.3 - rolldown: 1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3) - rolldown-plugin-dts: 0.17.8(rolldown@1.0.0-beta.45(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3))(typescript@5.9.3) - semver: 7.8.5 + hookable: 6.1.2 + import-without-cache: 0.4.1 + obug: 2.1.4 + picomatch: 4.0.5 + rolldown: 1.2.9 + rolldown-plugin-dts: 0.27.14(rolldown@1.2.9)(typescript@5.9.3) tinyexec: 1.3.0 tinyglobby: 0.2.17 tree-kill: 1.2.2 - unconfig: 7.5.0 - unrun: 0.2.39 + unconfig-core: 7.5.0 + verkit: 0.1.2 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@ts-macro/tsc' - '@typescript/native-preview' + - '@volar/typescript' - oxc-resolver - - supports-color - - synckit - vue-tsc - tslib@2.8.1: - optional: true + tslib@2.8.1: {} + + type-fest@4.41.0: {} typescript@5.9.3: {} @@ -2557,23 +2801,23 @@ snapshots: '@quansync/fs': 1.0.0 quansync: 1.0.0 - unconfig@7.5.0: - dependencies: - '@quansync/fs': 1.0.0 - defu: 6.1.7 - jiti: 2.7.0 - quansync: 1.0.0 - unconfig-core: 7.5.0 - undici@7.29.0: {} unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 - unrun@0.2.39: + verkit@0.1.2: {} + + vue@3.5.18(typescript@5.9.3): dependencies: - rolldown: 1.0.0-rc.17 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-sfc': 3.5.18 + '@vue/runtime-dom': 3.5.18 + '@vue/server-renderer': 3.5.18(vue@3.5.18(typescript@5.9.3)) + '@vue/shared': 3.5.18 + optionalDependencies: + typescript: 5.9.3 which@2.0.2: dependencies: @@ -2625,4 +2869,55 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 + yuku-ast@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + + yuku-codegen@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-x64': 0.8.7 + '@yuku-codegen/binding-freebsd-x64': 0.8.7 + '@yuku-codegen/binding-linux-arm-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm-musl': 0.8.7 + '@yuku-codegen/binding-linux-arm64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm64-musl': 0.8.7 + '@yuku-codegen/binding-linux-x64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-x64-musl': 0.8.7 + '@yuku-codegen/binding-win32-arm64': 0.8.7 + '@yuku-codegen/binding-win32-x64': 0.8.7 + + yuku-parser@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + yuku-ast: 0.8.7 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.8.7 + '@yuku-parser/binding-darwin-arm64': 0.8.7 + '@yuku-parser/binding-darwin-x64': 0.8.7 + '@yuku-parser/binding-freebsd-x64': 0.8.7 + '@yuku-parser/binding-linux-arm-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm-musl': 0.8.7 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm64-musl': 0.8.7 + '@yuku-parser/binding-linux-x64-gnu': 0.8.7 + '@yuku-parser/binding-linux-x64-musl': 0.8.7 + '@yuku-parser/binding-win32-arm64': 0.8.7 + '@yuku-parser/binding-win32-x64': 0.8.7 + + zod-class@0.0.18(zod@4.1.12): + dependencies: + type-fest: 4.41.0 + zod: 4.1.12 + + zod-config@1.4.0(dotenv@17.4.2)(yaml@2.9.0)(zod@4.1.12): + dependencies: + zod: 4.1.12 + optionalDependencies: + dotenv: 17.4.2 + yaml: 2.9.0 + zod@4.1.12: {} diff --git a/pyproject.toml b/pyproject.toml index 860ac4a..349d7cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ license = "AGPL-3.0-only" authors = [{ name = "InKCre" }] dependencies = [ "fastapi>=0.139.2,<0.142", - "inkcre-extension-toolkit>=0.2,<0.3", + "inkcre-extension-toolkit>=0.3,<0.4", + "publicsuffixlist>=1,<2", "packaging>=25,<27", "python-multipart>=0.0.22,<0.1", "jinja2<4,>=3.1.6", diff --git a/runtimes/client-web/check-boundaries.mjs b/runtimes/client-web/check-boundaries.mjs new file mode 100644 index 0000000..e7546c7 --- /dev/null +++ b/runtimes/client-web/check-boundaries.mjs @@ -0,0 +1,172 @@ +import assert from 'node:assert/strict' +import { createServer } from 'node:http' +import { once } from 'node:events' +import { configStore, PeerManager, PeerOutcomeUnknown } from '@inkcre/core' +import { + EXTENSION_MANAGEMENT_CAPABILITY, + getExtensionDocumentation, + listAdvertisedExtensionManagementPeers, + manageExtensionOnPeer, + RegistryDocumentationError, +} from './dist/index.js' + +// Exercise the built package and real SDK over HTTP, without a deployment or real credentials. +const target = '11111111-1111-4111-8111-111111111111' +const current = '22222222-2222-4222-8222-222222222222' +const secret = 'boundary-check-secret' +const installed = { + name: 'inkcre/memos', + version: '0.2.0', + enabled: [target], + nickname: 'Memos', + config: {}, + config_schema: null, +} +let origin +let managementStatus = 200 +let managementBody = installed +let dropManagement = false +let docStatus = 200 +let docBody +let dropDocumentation = false +const requests = [] +const server = createServer(async (request, response) => { + const url = new URL(request.url, origin) + const chunks = [] + for await (const chunk of request) chunks.push(chunk) + requests.push({ url, headers: request.headers, body: Buffer.concat(chunks).toString() }) + response.setHeader('Content-Type', 'application/json') + if (url.pathname === '/peers') { + const peer = (id, capabilities) => ({ + id, + name: id, + capabilities, + lease_expires_at: '2099-01-01T00:00:00Z', + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }) + const advertisement = { + id: EXTENSION_MANAGEMENT_CAPABILITY, + inbound: { + protocol: 'core.peer.protocol.http.v1', + parameters: { method: 'POST', url: `${origin}/manage` }, + }, + } + const peers = [ + peer(target, [advertisement]), + peer(current, [{ ...advertisement, inbound: { protocol: 'unsupported', parameters: {} } }]), + peer('33333333-3333-4333-8333-333333333333', [ + { ...advertisement, id: `${EXTENSION_MANAGEMENT_CAPABILITY}.other` }, + ]), + peer('44444444-4444-4444-8444-444444444444', [{ id: EXTENSION_MANAGEMENT_CAPABILITY }]), + ] + response.end( + JSON.stringify(url.searchParams.getAll('id').includes(`eq.${target}`) ? [peers[0]] : peers), + ) + } else if (url.pathname === '/manage') { + if (dropManagement) return request.socket.destroy() + response.statusCode = managementStatus + response.end(JSON.stringify(managementBody)) + } else { + if (dropDocumentation) return request.socket.destroy() + response.statusCode = docStatus + response.end(JSON.stringify(docBody)) + } +}) +server.listen(0, '127.0.0.1') +await once(server, 'listening') +origin = `http://127.0.0.1:${server.address().port}` + +try { + configStore.metaConfig = { + INKCRE_PGREST_URL: origin, + INKCRE_PEER_ID: current, + INKCRE_JWT_SECRET: 'isolated-check-signing-secret-at-least-32-characters', + } + PeerManager.setupBuiltinOutbounds() + assert.deepEqual( + (await listAdvertisedExtensionManagementPeers()).map(({ id }) => id), + [target, current], + ) + assert.equal(requests.at(-1).url.searchParams.get('lease_expires_at'), 'gt.now') + for (const command of [ + { action: 'install', extension: installed.name, version: installed.version }, + { action: 'enable', extension: installed.name }, + { action: 'disable', extension: installed.name }, + { action: 'patch_config', extension: installed.name, patch: { personal_access_token: secret } }, + ]) { + assert.deepEqual(await manageExtensionOnPeer(target, command), installed) + assert.ok(requests.at(-2).url.searchParams.getAll('id').includes(`eq.${target}`)) + assert.deepEqual(JSON.parse(requests.at(-1).body), command) + } + managementStatus = 422 + managementBody = { detail: secret } + await assert.rejects( + manageExtensionOnPeer(target, { action: 'enable', extension: installed.name }), + (error) => /HTTP 422/.test(error.message) && !JSON.stringify(error).includes(secret), + ) + managementStatus = 200 + managementBody = { ...installed, nickname: { [secret]: true } } + await assert.rejects( + manageExtensionOnPeer(target, { action: 'enable', extension: installed.name }), + (error) => !`${error.message}${JSON.stringify(error)}`.includes(secret), + ) + dropManagement = true + const before = requests.filter(({ url }) => url.pathname === '/manage').length + await assert.rejects( + manageExtensionOnPeer(target, { action: 'enable', extension: installed.name }), + PeerOutcomeUnknown, + ) + assert.equal(requests.filter(({ url }) => url.pathname === '/manage').length, before + 1) + await assert.rejects( + manageExtensionOnPeer('', { action: 'enable', extension: installed.name }), + TypeError, + ) + + docBody = { + name: installed.name, + version: installed.version, + state: 'yanked', + sets: ['global', 'python', 'module-federation'].map((scope) => ({ + scope, + entry_url: `https://docs.example.test/${scope}/#connect`, + snapshot_id: 'a'.repeat(32), + content_sha256: 'b'.repeat(64), + snapshot_url: 'https://snapshot.example.test/', + source_repository: 'https://example.test/source', + source_revision: 'test', + updated_at: '2026-01-01T00:00:00Z', + etag: 'test', + })), + } + const readDocs = () => getExtensionDocumentation(origin, installed.name, installed.version) + assert.deepEqual( + await readDocs(), + docBody.sets.map(({ scope, entry_url }) => ({ scope, entry_url })), + ) + assert.equal( + requests.at(-1).url.pathname, + '/v1/extensions/inkcre/memos/releases/0.2.0/documentation', + ) + assert.equal(requests.at(-1).headers.authorization, undefined) + for (const unsafe of ['javascript:alert(1)', `https://user:${secret}@docs.example.test/`]) { + docBody.sets[0].entry_url = unsafe + await assert.rejects(readDocs(), RegistryDocumentationError) + } + docBody.sets = [] + assert.deepEqual(await readDocs(), []) + docBody.version = '0.3.0' + await assert.rejects(readDocs(), RegistryDocumentationError) + docStatus = 404 + assert.equal(await readDocs(), null) + docStatus = 503 + await assert.rejects(readDocs(), RegistryDocumentationError) + dropDocumentation = true + await assert.rejects(readDocs(), RegistryDocumentationError) + console.log('Built Runtime / real Core SDK HTTP boundaries passed.') +} finally { + server.closeAllConnections() + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ) +} diff --git a/runtimes/client-web/package.json b/runtimes/client-web/package.json index 2926851..56270b5 100644 --- a/runtimes/client-web/package.json +++ b/runtimes/client-web/package.json @@ -16,10 +16,12 @@ } }, "scripts": { - "build": "tsdown", + "prepare:sdk": "node prepare-sdk.mjs", + "build": "pnpm prepare:sdk && tsdown --platform neutral", + "check:boundaries": "pnpm build && node check-boundaries.mjs", "check:bindings": "pnpm generate:bindings && git diff --exit-code -- src/generated", "generate:bindings": "openapi-ts -f openapi-ts.config.ts && prettier --write src/generated", - "type-check": "tsc --noEmit", + "type-check": "pnpm prepare:sdk && tsc --noEmit", "pack": "pnpm pack" }, "dependencies": { @@ -27,13 +29,17 @@ "zod": "4.1.12" }, "peerDependencies": { - "@inkcre/core": ">=0.1.2 <0.2.0", + "@inkcre/core": ">=0.3.0 <0.4.0", "@module-federation/runtime": ">=0.21.4 <0.23.0" }, "devDependencies": { + "@inkcre/core": "github:InKCre/client-web#f29a407ae02980b221e9419bb38421aa95805771&path:/packages/core", "@module-federation/runtime": "0.21.6", "@types/semver": "7.7.1", - "tsdown": "0.15.11", - "typescript": "5.9.3" + "@vueuse/core": "14.1.0", + "pinia": "3.0.3", + "tsdown": "0.22.13", + "typescript": "5.9.3", + "vue": "3.5.18" } } diff --git a/runtimes/client-web/prepare-sdk.mjs b/runtimes/client-web/prepare-sdk.mjs new file mode 100644 index 0000000..c868b7e --- /dev/null +++ b/runtimes/client-web/prepare-sdk.mjs @@ -0,0 +1,79 @@ +import { execFileSync } from 'node:child_process' +import { + cpSync, + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + symlinkSync, + unlinkSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +// The SDK has no published build yet. Build its pinned source with this repository's +// frozen tools and dependencies, without installing the producer workspace or editing the store. +const root = dirname(fileURLToPath(import.meta.url)) +const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) +const revision = /^github:InKCre\/client-web#([a-f0-9]{40})&path:\/packages\/core$/.exec( + manifest.devDependencies['@inkcre/core'], +)?.[1] +if (!revision) + throw new Error('Core SDK development dependency must pin one full client-web Git revision.') +const dependency = join(root, 'node_modules/@inkcre/core') +const installed = realpathSync(dependency) +const output = join(root, `node_modules/.inkcre-core-sdk-${revision}`) +if ( + !lstatSync(dependency).isSymbolicLink() || + (existsSync(output) && lstatSync(output).isSymbolicLink()) +) { + throw new Error( + 'SDK preparation requires a workspace dependency link and a private build directory.', + ) +} +if (installed !== output) { + const checkout = mkdtempSync(join(tmpdir(), 'inkcre-core-sdk-')) + const run = (command, args, cwd = checkout) => + execFileSync(command, args, { cwd, stdio: 'inherit' }) + try { + run('git', ['init', '--quiet']) + run('git', [ + 'fetch', + '--quiet', + '--depth=1', + 'https://github.com/InKCre/client-web.git', + revision, + ]) + run('git', ['checkout', '--quiet', 'FETCH_HEAD', '--', 'packages/core']) + const source = join(checkout, 'packages/core') + // Build outside node_modules: Node 22 does not strip types from configs inside it. + symlinkSync(dirname(dirname(installed)), join(source, 'node_modules'), 'dir') + symlinkSync(join(root, 'node_modules'), join(checkout, 'packages/node_modules'), 'dir') + run(join(root, 'node_modules/.bin/tsdown'), [], source) + mkdirSync(output, { recursive: true }) + for (const entry of ['package.json', 'dist']) { + cpSync(join(source, entry), join(output, entry), { recursive: true }) + } + const dependencies = join(output, 'node_modules') + if (lstatSync(dependencies, { throwIfNoEntry: false })) unlinkSync(dependencies) + // SDK runtime dependencies live beside the scoped pnpm package. Build tools resolve + // from the enclosing Runtime workspace node_modules, all under this repository's lock. + symlinkSync(dirname(dirname(installed)), dependencies, 'dir') + } finally { + rmSync(checkout, { recursive: true, force: true }) + } + unlinkSync(dependency) + symlinkSync(output, dependency, 'dir') +} +if ( + realpathSync(dependency) !== output || + !existsSync(join(output, 'dist/index.d.ts')) || + !existsSync(join(output, 'dist/index.js')) +) { + throw new Error('Core SDK preparation did not resolve to the isolated real build.') +} +console.log(`Core SDK ready from client-web ${revision}.`) diff --git a/runtimes/client-web/src/documentation.ts b/runtimes/client-web/src/documentation.ts new file mode 100644 index 0000000..dfd2089 --- /dev/null +++ b/runtimes/client-web/src/documentation.ts @@ -0,0 +1,55 @@ +import { createClient } from './generated/client' +import { getDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGet } from './generated/sdk.gen' +import type { DocumentationRecord } from './generated/types.gen' +import { zReleaseDocumentation } from './generated/zod.gen' +import { RegistryDocumentationError } from './errors' +import { assertCoordinate, registryOrigin } from './registry' + +export type ExtensionDocumentationLink = Pick + +/** Discover exact installed Release links without loading documentation or checking Host compatibility. + * A 404 returns null; a readable Release without documentation returns an empty array. + */ +export async function getExtensionDocumentation( + origin: string, + name: string, + version: string, +): Promise { + assertCoordinate(name, version) + const [namespace, localName] = name.split('/') as [string, string] + const result = await getDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGet({ + client: createClient({ baseUrl: registryOrigin(origin).origin }), + path: { namespace, name: localName, version }, + credentials: 'omit', + headers: { Accept: 'application/json' }, + }) + if (result.response?.status === 404) return null + if (!result.response?.ok) { + throw new RegistryDocumentationError( + result.response + ? `Extension documentation request failed with HTTP ${result.response.status}.` + : 'Extension documentation request could not reach the Registry.', + ) + } + const parsed = zReleaseDocumentation.safeParse(result.data) + if ( + !parsed.success || + parsed.data.name !== name || + parsed.data.version !== version || + !['published', 'yanked'].includes(parsed.data.state) + ) { + throw new RegistryDocumentationError('Registry returned invalid exact Release documentation.') + } + return parsed.data.sets.map(({ scope, entry_url }) => { + let url: URL + try { + url = new URL(entry_url) + } catch { + throw new RegistryDocumentationError('Registry returned an invalid documentation URL.') + } + if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) { + throw new RegistryDocumentationError('Registry returned an unsafe documentation URL.') + } + return { scope, entry_url: url.href } + }) +} diff --git a/runtimes/client-web/src/errors.ts b/runtimes/client-web/src/errors.ts index ccc0ce4..0daec21 100644 --- a/runtimes/client-web/src/errors.ts +++ b/runtimes/client-web/src/errors.ts @@ -6,6 +6,10 @@ export class RegistryReleaseError extends WebExtensionRuntimeError { override name = 'RegistryReleaseError' } +export class RegistryDocumentationError extends WebExtensionRuntimeError { + override name = 'RegistryDocumentationError' +} + export class HostSdkCompatibilityError extends WebExtensionRuntimeError { override name = 'HostSdkCompatibilityError' } diff --git a/runtimes/client-web/src/generated/index.ts b/runtimes/client-web/src/generated/index.ts index 9bbd789..95dbc37 100644 --- a/runtimes/client-web/src/generated/index.ts +++ b/runtimes/client-web/src/generated/index.ts @@ -1,6 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts export { + documentationHostingV1DocumentationHostingGet, + getDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGet, getExtensionV1ExtensionsNamespaceNameGet, getReleaseV1ExtensionsNamespaceNameReleasesVersionGet, legacyUploadLegacyPost, @@ -9,19 +11,34 @@ export { moduleFederationFileExtensionsNamespaceNameVersionModuleFederationRelativePathGet, type Options, prepareReleaseV1ExtensionsNamespaceNameReleasesPost, + publisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGet, publisherWorkspaceV1PublisherGet, publishReleaseV1ExtensionsNamespaceNameReleasesVersionPublishPost, pythonFilePackagesProjectProjectVersionFilenameGet, simpleProjectSimpleProjectGet, simpleRootSimpleGet, unyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPost, + uploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePost, uploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPost, yankReleaseV1ExtensionsNamespaceNameReleasesVersionYankPost, } from './sdk.gen' export type { ClientOptions, + DocumentationHosting, + DocumentationHostingV1DocumentationHostingGetData, + DocumentationHostingV1DocumentationHostingGetError, + DocumentationHostingV1DocumentationHostingGetErrors, + DocumentationHostingV1DocumentationHostingGetResponse, + DocumentationHostingV1DocumentationHostingGetResponses, + DocumentationReceipt, + DocumentationRecord, ExtensionRecord, ExtensionSummary, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetData, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetError, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponse, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, GetExtensionV1ExtensionsNamespaceNameGetData, GetExtensionV1ExtensionsNamespaceNameGetError, GetExtensionV1ExtensionsNamespaceNameGetErrors, @@ -55,6 +72,11 @@ export type { PrepareReleaseV1ExtensionsNamespaceNameReleasesPostErrors, PrepareReleaseV1ExtensionsNamespaceNameReleasesPostResponse, PrepareReleaseV1ExtensionsNamespaceNameReleasesPostResponses, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetData, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetError, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponse, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, PublisherRelease, PublisherWorkspace, PublisherWorkspaceV1PublisherGetData, @@ -74,6 +96,8 @@ export type { PythonFilePackagesProjectProjectVersionFilenameGetError, PythonFilePackagesProjectProjectVersionFilenameGetErrors, PythonFilePackagesProjectProjectVersionFilenameGetResponses, + RegistryError, + ReleaseDocumentation, ReleaseRecord, SimpleProjectSimpleProjectGetData, SimpleProjectSimpleProjectGetError, @@ -86,6 +110,11 @@ export type { UnyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPostErrors, UnyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPostResponse, UnyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPostResponses, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostData, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostError, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponse, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses, UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostData, UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostError, UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostErrors, diff --git a/runtimes/client-web/src/generated/sdk.gen.ts b/runtimes/client-web/src/generated/sdk.gen.ts index 039a9cf..2357249 100644 --- a/runtimes/client-web/src/generated/sdk.gen.ts +++ b/runtimes/client-web/src/generated/sdk.gen.ts @@ -1,8 +1,21 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, ClientMeta, Options as Options2, RequestResult, TDataShape } from './client' +import { + type Client, + type ClientMeta, + formDataBodySerializer, + type Options as Options2, + type RequestResult, + type TDataShape, +} from './client' import { client } from './client.gen' import type { + DocumentationHostingV1DocumentationHostingGetData, + DocumentationHostingV1DocumentationHostingGetErrors, + DocumentationHostingV1DocumentationHostingGetResponses, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetData, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, GetExtensionV1ExtensionsNamespaceNameGetData, GetExtensionV1ExtensionsNamespaceNameGetErrors, GetExtensionV1ExtensionsNamespaceNameGetResponses, @@ -22,6 +35,9 @@ import type { PrepareReleaseV1ExtensionsNamespaceNameReleasesPostData, PrepareReleaseV1ExtensionsNamespaceNameReleasesPostErrors, PrepareReleaseV1ExtensionsNamespaceNameReleasesPostResponses, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetData, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, PublisherWorkspaceV1PublisherGetData, PublisherWorkspaceV1PublisherGetErrors, PublisherWorkspaceV1PublisherGetResponses, @@ -39,6 +55,9 @@ import type { UnyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPostData, UnyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPostErrors, UnyankReleaseV1ExtensionsNamespaceNameReleasesVersionUnyankPostResponses, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostData, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses, UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostData, UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostErrors, UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostResponses, @@ -157,6 +176,22 @@ export const simpleProjectSimpleProjectGet = ({ url: '/simple/{project}/', ...options }) +/** + * Documentation Hosting + */ +export const documentationHostingV1DocumentationHostingGet = ( + options?: Options, +): RequestResult< + DocumentationHostingV1DocumentationHostingGetResponses, + DocumentationHostingV1DocumentationHostingGetErrors, + ThrowOnError +> => + (options?.client ?? client).get< + DocumentationHostingV1DocumentationHostingGetResponses, + DocumentationHostingV1DocumentationHostingGetErrors, + ThrowOnError + >({ url: '/v1/documentation-hosting', ...options }) + /** * List Extensions */ @@ -227,6 +262,56 @@ export const getReleaseV1ExtensionsNamespaceNameReleasesVersionGet = < ThrowOnError >({ url: '/v1/extensions/{namespace}/{name}/releases/{version}', ...options }) +/** + * Get Documentation + */ +export const getDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGet = < + ThrowOnError extends boolean = false, +>( + options: Options< + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetData, + ThrowOnError + >, +): RequestResult< + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + ThrowOnError +> => + (options.client ?? client).get< + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + ThrowOnError + >({ url: '/v1/extensions/{namespace}/{name}/releases/{version}/documentation', ...options }) + +/** + * Upload Documentation + */ +export const uploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePost = < + ThrowOnError extends boolean = false, +>( + options: Options< + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostData, + ThrowOnError + >, +): RequestResult< + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors, + ThrowOnError +> => + (options.client ?? client).post< + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses, + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors, + ThrowOnError + >({ + ...formDataBodySerializer, + url: '/v1/extensions/{namespace}/{name}/releases/{version}/documentation/{scope}', + ...options, + headers: { + 'Content-Type': null, + ...options.headers, + }, + }) + /** * Upload Module Federation */ @@ -330,3 +415,26 @@ export const publisherWorkspaceV1PublisherGet = ({ url: '/v1/publisher', ...options }) + +/** + * Publisher Documentation + */ +export const publisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGet = + ( + options: Options< + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetData, + ThrowOnError + >, + ): RequestResult< + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + ThrowOnError + > => + (options.client ?? client).get< + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses, + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors, + ThrowOnError + >({ + url: '/v1/publisher/extensions/{namespace}/{name}/releases/{version}/documentation', + ...options, + }) diff --git a/runtimes/client-web/src/generated/types.gen.ts b/runtimes/client-web/src/generated/types.gen.ts index 5e67a13..5ec1340 100644 --- a/runtimes/client-web/src/generated/types.gen.ts +++ b/runtimes/client-web/src/generated/types.gen.ts @@ -4,6 +4,120 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } +/** + * DocumentationHosting + */ +export type DocumentationHosting = { + /** + * Origin Template + */ + origin_template: string +} + +/** + * DocumentationReceipt + */ +export type DocumentationReceipt = { + /** + * Build Id + */ + build_id?: string | null + /** + * Committed At + */ + committed_at: string + /** + * Content Sha256 + */ + content_sha256: string + /** + * Entry + */ + entry?: string + /** + * Name + */ + name: string + /** + * Scope + */ + scope: 'global' | 'python' | 'module-federation' + /** + * Snapshot Etag + */ + snapshot_etag: string + /** + * Snapshot Id + */ + snapshot_id: string + /** + * Snapshot Url + */ + snapshot_url: string + /** + * Source Repository + */ + source_repository: string + /** + * Source Revision + */ + source_revision: string + /** + * Version + */ + version: string +} + +/** + * DocumentationRecord + */ +export type DocumentationRecord = { + /** + * Build Id + */ + build_id?: string | null + /** + * Content Sha256 + */ + content_sha256: string + /** + * Entry + */ + entry?: string + /** + * Entry Url + */ + entry_url: string + /** + * Etag + */ + etag: string + /** + * Scope + */ + scope: 'global' | 'python' | 'module-federation' + /** + * Snapshot Id + */ + snapshot_id: string + /** + * Snapshot Url + */ + snapshot_url: string + /** + * Source Repository + */ + source_repository: string + /** + * Source Revision + */ + source_revision: string + /** + * Updated At + */ + updated_at: string +} + /** * ExtensionRecord */ @@ -232,6 +346,38 @@ export type PythonEntryPoint = { object: string } +/** + * RegistryError + */ +export type RegistryError = { + /** + * Detail + */ + detail: string +} + +/** + * ReleaseDocumentation + */ +export type ReleaseDocumentation = { + /** + * Name + */ + name: string + /** + * Sets + */ + sets: Array + /** + * State + */ + state: 'preparing' | 'published' | 'yanked' | 'blocked' + /** + * Version + */ + version: string +} + /** * ReleaseRecord */ @@ -468,6 +614,33 @@ export type SimpleProjectSimpleProjectGetResponses = { 200: unknown } +export type DocumentationHostingV1DocumentationHostingGetData = { + body?: never + path?: never + query?: never + url: '/v1/documentation-hosting' +} + +export type DocumentationHostingV1DocumentationHostingGetErrors = { + /** + * Documentation content hosting is not configured. + */ + 503: RegistryError +} + +export type DocumentationHostingV1DocumentationHostingGetError = + DocumentationHostingV1DocumentationHostingGetErrors[keyof DocumentationHostingV1DocumentationHostingGetErrors] + +export type DocumentationHostingV1DocumentationHostingGetResponses = { + /** + * Successful Response + */ + 200: DocumentationHosting +} + +export type DocumentationHostingV1DocumentationHostingGetResponse = + DocumentationHostingV1DocumentationHostingGetResponses[keyof DocumentationHostingV1DocumentationHostingGetResponses] + export type ListExtensionsV1ExtensionsGetData = { body?: never path?: never @@ -605,6 +778,157 @@ export type GetReleaseV1ExtensionsNamespaceNameReleasesVersionGetResponses = { export type GetReleaseV1ExtensionsNamespaceNameReleasesVersionGetResponse = GetReleaseV1ExtensionsNamespaceNameReleasesVersionGetResponses[keyof GetReleaseV1ExtensionsNamespaceNameReleasesVersionGetResponses] +export type GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetData = { + body?: never + path: { + /** + * Namespace + */ + namespace: string + /** + * Name + */ + name: string + /** + * Version + */ + version: string + } + query?: never + url: '/v1/extensions/{namespace}/{name}/releases/{version}/documentation' +} + +export type GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors = { + /** + * Release or publicly readable documentation does not exist. + */ + 404: RegistryError + /** + * Validation Error + */ + 422: HttpValidationError + /** + * The Release is operator-blocked, including for its publisher. + */ + 451: RegistryError + /** + * Documentation content hosting is not configured. + */ + 503: RegistryError +} + +export type GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetError = + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors[keyof GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetErrors] + +export type GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses = { + /** + * Successful Response + */ + 200: ReleaseDocumentation +} + +export type GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponse = + GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses[keyof GetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponses] + +export type UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostData = + { + body: { + content: Blob | File + /** + * JSON-encoded DocumentationPublication; send as a text form field, not a file. + */ + metadata: string + } + headers?: { + /** + * Authorization + */ + authorization?: string | null + } + path: { + /** + * Namespace + */ + namespace: string + /** + * Name + */ + name: string + /** + * Version + */ + version: string + /** + * Scope + */ + scope: 'global' | 'python' | 'module-federation' + } + query?: never + url: '/v1/extensions/{namespace}/{name}/releases/{version}/documentation/{scope}' + } + +export type UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors = + { + /** + * Malformed conditional headers, multipart fields, metadata, or static ZIP. + */ + 400: RegistryError + /** + * Publisher credential is missing or invalid. + */ + 401: RegistryError + /** + * Publisher does not own this namespace. + */ + 403: RegistryError + /** + * Release or publicly readable documentation does not exist. + */ + 404: RegistryError + /** + * Publication identity conflict, stale expected_etag, or missing association. + */ + 409: RegistryError + /** + * A non-chunked Content-Length is required. + */ + 411: RegistryError + /** + * The complete multipart request exceeds 20 MiB. + */ + 413: RegistryError + /** + * The request must use multipart/form-data. + */ + 415: RegistryError + /** + * Validation Error + */ + 422: HttpValidationError + /** + * The Release is operator-blocked, including for its publisher. + */ + 451: RegistryError + /** + * Documentation content hosting is not configured. + */ + 503: RegistryError + } + +export type UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostError = + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors[keyof UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostErrors] + +export type UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses = + { + /** + * Historical commit confirmed; current may differ. + */ + 200: DocumentationReceipt + } + +export type UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponse = + UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses[keyof UploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponses] + export type UploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostData = { body?: never @@ -832,3 +1156,72 @@ export type PublisherWorkspaceV1PublisherGetResponses = { export type PublisherWorkspaceV1PublisherGetResponse = PublisherWorkspaceV1PublisherGetResponses[keyof PublisherWorkspaceV1PublisherGetResponses] + +export type PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetData = + { + body?: never + headers?: { + /** + * Authorization + */ + authorization?: string | null + } + path: { + /** + * Namespace + */ + namespace: string + /** + * Name + */ + name: string + /** + * Version + */ + version: string + } + query?: never + url: '/v1/publisher/extensions/{namespace}/{name}/releases/{version}/documentation' + } + +export type PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors = + { + /** + * Publisher credential is missing or invalid. + */ + 401: RegistryError + /** + * Publisher does not own this namespace. + */ + 403: RegistryError + /** + * Release or publicly readable documentation does not exist. + */ + 404: RegistryError + /** + * Validation Error + */ + 422: HttpValidationError + /** + * The Release is operator-blocked, including for its publisher. + */ + 451: RegistryError + /** + * Documentation content hosting is not configured. + */ + 503: RegistryError + } + +export type PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetError = + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors[keyof PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetErrors] + +export type PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses = + { + /** + * Successful Response + */ + 200: ReleaseDocumentation + } + +export type PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponse = + PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses[keyof PublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponses] diff --git a/runtimes/client-web/src/generated/zod.gen.ts b/runtimes/client-web/src/generated/zod.gen.ts index 7208fc2..d06916d 100644 --- a/runtimes/client-web/src/generated/zod.gen.ts +++ b/runtimes/client-web/src/generated/zod.gen.ts @@ -2,6 +2,58 @@ import * as z from 'zod' +/** + * DocumentationHosting + */ +export const zDocumentationHosting = z.object({ + origin_template: z.string(), +}) + +/** + * DocumentationReceipt + */ +export const zDocumentationReceipt = z.object({ + build_id: z.string().max(256).nullish(), + committed_at: z.string(), + content_sha256: z.string().regex(/^[0-9a-f]{64}$/), + entry: z.string().min(1).max(768).optional().default('index.html'), + name: z + .string() + .min(3) + .max(129) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + scope: z.enum(['global', 'python', 'module-federation']), + snapshot_etag: z.string(), + snapshot_id: z.string().regex(/^[0-9a-f]{32}$/), + snapshot_url: z.string(), + source_repository: z.string().min(1).max(2048), + source_revision: z.string().min(1).max(256), + version: z + .string() + .min(5) + .max(128) + .regex( + /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$/, + ), +}) + +/** + * DocumentationRecord + */ +export const zDocumentationRecord = z.object({ + build_id: z.string().max(256).nullish(), + content_sha256: z.string().regex(/^[0-9a-f]{64}$/), + entry: z.string().min(1).max(768).optional().default('index.html'), + entry_url: z.string(), + etag: z.string(), + scope: z.enum(['global', 'python', 'module-federation']), + snapshot_id: z.string().regex(/^[0-9a-f]{32}$/), + snapshot_url: z.string(), + source_repository: z.string().min(1).max(2048), + source_revision: z.string().min(1).max(256), + updated_at: z.string(), +}) + /** * ExtensionSummary */ @@ -137,6 +189,33 @@ export const zPublisherWorkspace = z.object({ releases: z.array(zPublisherRelease), }) +/** + * RegistryError + */ +export const zRegistryError = z.object({ + detail: z.string(), +}) + +/** + * ReleaseDocumentation + */ +export const zReleaseDocumentation = z.object({ + name: z + .string() + .min(3) + .max(129) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + sets: z.array(zDocumentationRecord), + state: z.enum(['preparing', 'published', 'yanked', 'blocked']), + version: z + .string() + .min(5) + .max(128) + .regex( + /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$/, + ), +}) + /** * ReleaseRecord */ @@ -240,6 +319,11 @@ export const zSimpleProjectSimpleProjectGetPath = z.object({ project: z.string(), }) +/** + * Successful Response + */ +export const zDocumentationHostingV1DocumentationHostingGetResponse = zDocumentationHosting + /** * Response List Extensions V1 Extensions Get * @@ -314,6 +398,72 @@ export const zGetReleaseV1ExtensionsNamespaceNameReleasesVersionGetPath = z.obje */ export const zGetReleaseV1ExtensionsNamespaceNameReleasesVersionGetResponse = zReleaseRecord +export const zGetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetPath = + z.object({ + namespace: z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + name: z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + version: z + .string() + .min(5) + .max(128) + .regex( + /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$/, + ), + }) + +/** + * Successful Response + */ +export const zGetDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationGetResponse = + zReleaseDocumentation + +export const zUploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostBody = + z.object({ + content: z.string(), + metadata: z.string(), + }) + +export const zUploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostHeaders = + z.object({ + authorization: z.string().nullish(), + }) + +export const zUploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostPath = + z.object({ + namespace: z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + name: z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + version: z + .string() + .min(5) + .max(128) + .regex( + /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$/, + ), + scope: z.enum(['global', 'python', 'module-federation']), + }) + +/** + * Historical commit confirmed; current may differ. + */ +export const zUploadDocumentationV1ExtensionsNamespaceNameReleasesVersionDocumentationScopePostResponse = + zDocumentationReceipt + export const zUploadModuleFederationV1ExtensionsNamespaceNameReleasesVersionModuleFederationPostHeaders = z.object({ authorization: z.string().nullish(), @@ -453,3 +603,35 @@ export const zPublisherWorkspaceV1PublisherGetQuery = z.object({ * Successful Response */ export const zPublisherWorkspaceV1PublisherGetResponse = zPublisherWorkspace + +export const zPublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetHeaders = + z.object({ + authorization: z.string().nullish(), + }) + +export const zPublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetPath = + z.object({ + namespace: z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + name: z + .string() + .min(1) + .max(64) + .regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/), + version: z + .string() + .min(5) + .max(128) + .regex( + /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$/, + ), + }) + +/** + * Successful Response + */ +export const zPublisherDocumentationV1PublisherExtensionsNamespaceNameReleasesVersionDocumentationGetResponse = + zReleaseDocumentation diff --git a/runtimes/client-web/src/index.ts b/runtimes/client-web/src/index.ts index 011d811..7309b83 100644 --- a/runtimes/client-web/src/index.ts +++ b/runtimes/client-web/src/index.ts @@ -1,5 +1,8 @@ export * from './errors' +export * from './documentation' export * from './manager' export * from './module' -export * from './registry' +export * from './peer-management' +export { RegistryReleaseReader } from './registry' +export type { HostSdkIdentity, RegistryReleaseReaderOptions } from './registry' export type { ReleaseRecord, ModuleFederationDistribution } from './generated/types.gen' diff --git a/runtimes/client-web/src/inkcre-core.d.ts b/runtimes/client-web/src/inkcre-core.d.ts deleted file mode 100644 index 0b833bd..0000000 --- a/runtimes/client-web/src/inkcre-core.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare module '@inkcre/core' { - export interface ExtensionInstallInput { - readonly name: string - readonly version: string - readonly nickname: string - } - - export class ExtensionModel { - readonly name: string - readonly version: string - readonly nickname: string - readonly enabled: string[] - - static list(): Promise - static get(name: string): Promise - static install(input: ExtensionInstallInput): Promise - - changeVersion(version: string, nickname: string): Promise - updateConfig(config: Record): Promise - uninstall(): Promise - enablePeer(peerId: string): Promise - disablePeer(peerId: string): Promise - } -} diff --git a/runtimes/client-web/src/peer-management.ts b/runtimes/client-web/src/peer-management.ts new file mode 100644 index 0000000..59d5e86 --- /dev/null +++ b/runtimes/client-web/src/peer-management.ts @@ -0,0 +1,55 @@ +import { + InstalledExtensionSchema, + PeerManager, + PeerProtocolResponseSchema, + type InstalledExtension, + type JsonValue, + type Peer, +} from '@inkcre/core' +import { WebExtensionRuntimeError } from './errors' + +export const EXTENSION_MANAGEMENT_CAPABILITY = 'core.extension.management.v1' + +export type ExtensionManagementCommand = + | { action: 'install'; extension: string; version: string } + | { action: 'enable' | 'disable'; extension: string } + | { action: 'patch_config'; extension: string; patch: Record } + +/** Live advertisements are discovery candidates, not proof of a usable outbound route. */ +export async function listAdvertisedExtensionManagementPeers(): Promise { + return (await PeerManager.listLive()).filter((peer) => { + try { + return peer.capabilitySnapshot().some(({ id }) => id === EXTENSION_MANAGEMENT_CAPABILITY) + } catch { + return false + } + }) +} + +/** Send once to the selected Peer. PeerManager retains transport failure classification. */ +export async function manageExtensionOnPeer( + peerId: string, + command: ExtensionManagementCommand, +): Promise { + if (!peerId) throw new TypeError('Extension management requires an exact Peer ID.') + const result = await PeerManager.delegate( + EXTENSION_MANAGEMENT_CAPABILITY, + { body: command }, + peerId, + ) + const response = PeerProtocolResponseSchema.safeParse(result) + if (!response.success) { + throw new WebExtensionRuntimeError('Extension management Peer returned an invalid response.') + } + if (response.data.status !== 200) { + throw new WebExtensionRuntimeError( + `Extension management Peer returned HTTP ${response.data.status}.`, + ) + } + const extension = InstalledExtensionSchema.safeParse(response.data.body) + // Validation issues and remote error bodies can contain configuration credentials. + if (!extension.success || extension.data.name !== command.extension) { + throw new WebExtensionRuntimeError('Extension management Peer returned an invalid Extension.') + } + return extension.data +} diff --git a/runtimes/client-web/src/registry.ts b/runtimes/client-web/src/registry.ts index 167d382..9686ed8 100644 --- a/runtimes/client-web/src/registry.ts +++ b/runtimes/client-web/src/registry.ts @@ -92,7 +92,8 @@ export class RegistryReleaseReader { } } -function registryOrigin(value: string): URL { +/** Validate an origin already selected by the Host's Registry origin resolver. */ +export function registryOrigin(value: string): URL { const origin = new URL(value) if ( !['http:', 'https:'].includes(origin.protocol) || @@ -107,7 +108,7 @@ function registryOrigin(value: string): URL { return origin } -function assertCoordinate(name: string, version: string): void { +export function assertCoordinate(name: string, version: string): void { if (!EXTENSION_NAME.test(name)) throw new RegistryReleaseError('Invalid Extension name.') if (validSemVer(version) !== version || version.includes('+')) { throw new RegistryReleaseError( diff --git a/runtimes/core-py/src/inkcre_extension_runtime_core_py/generated/registry.py b/runtimes/core-py/src/inkcre_extension_runtime_core_py/generated/registry.py index b36bf7c..94f992a 100644 --- a/runtimes/core-py/src/inkcre_extension_runtime_core_py/generated/registry.py +++ b/runtimes/core-py/src/inkcre_extension_runtime_core_py/generated/registry.py @@ -9,6 +9,62 @@ from pydantic import BaseModel, ConfigDict, Field, conint, constr +class DocumentationHosting(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + origin_template: str = Field(..., title="Origin Template") + + +class Scope(Enum): + global_ = "global" + python = "python" + module_federation = "module-federation" + + +class DocumentationReceipt(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + build_id: constr(max_length=256) | None = Field(None, title="Build Id") + committed_at: str = Field(..., title="Committed At") + content_sha256: constr(pattern=r"^[0-9a-f]{64}$") = Field(..., title="Content Sha256") + entry: constr(min_length=1, max_length=768) | None = Field("index.html", title="Entry") + name: constr( + pattern=r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + min_length=3, + max_length=129, + ) = Field(..., title="Name") + scope: Scope = Field(..., title="Scope") + snapshot_etag: str = Field(..., title="Snapshot Etag") + snapshot_id: constr(pattern=r"^[0-9a-f]{32}$") = Field(..., title="Snapshot Id") + snapshot_url: str = Field(..., title="Snapshot Url") + source_repository: constr(min_length=1, max_length=2048) = Field(..., title="Source Repository") + source_revision: constr(min_length=1, max_length=256) = Field(..., title="Source Revision") + version: constr( + pattern=r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + min_length=5, + max_length=128, + ) = Field(..., title="Version") + + +class DocumentationRecord(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + build_id: constr(max_length=256) | None = Field(None, title="Build Id") + content_sha256: constr(pattern=r"^[0-9a-f]{64}$") = Field(..., title="Content Sha256") + entry: constr(min_length=1, max_length=768) | None = Field("index.html", title="Entry") + entry_url: str = Field(..., title="Entry Url") + etag: str = Field(..., title="Etag") + scope: Scope = Field(..., title="Scope") + snapshot_id: constr(pattern=r"^[0-9a-f]{32}$") = Field(..., title="Snapshot Id") + snapshot_url: str = Field(..., title="Snapshot Url") + source_repository: constr(min_length=1, max_length=2048) = Field(..., title="Source Repository") + source_revision: constr(min_length=1, max_length=256) = Field(..., title="Source Revision") + updated_at: str = Field(..., title="Updated At") + + class ExtensionSummary(BaseModel): model_config = ConfigDict( extra="forbid", @@ -69,6 +125,31 @@ class PythonEntryPoint(BaseModel): ) = Field(..., title="Object") +class RegistryError(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + detail: str = Field(..., title="Detail") + + +class ReleaseDocumentation(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + name: constr( + pattern=r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$", + min_length=3, + max_length=129, + ) = Field(..., title="Name") + sets: list[DocumentationRecord] = Field(..., title="Sets") + state: State = Field(..., title="State") + version: constr( + pattern=r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9][0-9]*)|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*)?$", + min_length=5, + max_length=128, + ) = Field(..., title="Version") + + class ValidationError(BaseModel): ctx: dict[str, Any] | None = Field(None, title="Context") input: Any | None = Field(None, title="Input") diff --git a/scripts/check_documentation.py b/scripts/check_documentation.py new file mode 100644 index 0000000..b88c8fd --- /dev/null +++ b/scripts/check_documentation.py @@ -0,0 +1,383 @@ +"""Documentation's public HTTP, storage atomicity, and lifecycle acceptance boundary.""" + +from __future__ import annotations + +import asyncio +import io +import json +import os +import secrets +import stat +import zipfile +from unittest.mock import patch + +import httpx +from inkcre_extension_toolkit.client import ( + DocumentationOutcomeUnknown, + RegistryClient, + RegistryHTTPError, +) +from inkcre_extension_toolkit.documentation import inspect_documentation +from inkcre_extension_toolkit.generated.documentation import DocumentationUpload +from tortoise.exceptions import IntegrityError +from tortoise.transactions import in_transaction + +from inkcre_extension_registry.service import database as db +from inkcre_extension_registry.service.settings import Settings + + +def check_content_sites() -> None: + """Configuration must enforce the promised cookie-site isolation before serving HTML.""" + for management, content, accepted in ( + ("https://registry.example.com", "https://{snapshot}.docs.example.com", False), + ("https://registry.example.co.uk", "https://{snapshot}.docs.example.co.uk", False), + ("https://registry.example.com", "https://{snapshot}.exampleusercontent.net", True), + ("https://registry.team.github.io", "https://{snapshot}.docs.team.github.io", False), + ("https://registry.team.github.io", "https://{snapshot}.other.github.io", True), + ("http://localhost", "http://{snapshot}.docs.localhost", True), + ("https://registry.example.com", "http://{snapshot}.docs.localhost", False), + ): + with patch.dict( + os.environ, PUBLIC_ORIGIN=management, DOCUMENTATION_ORIGIN_TEMPLATE=content + ): + try: + Settings.from_env() + except ValueError: + assert not accepted, (management, content) + else: + assert accepted, (management, content) + with patch.dict( + os.environ, + PUBLIC_ORIGIN="https://registry.example.com", + DOCUMENTATION_ORIGIN_TEMPLATE="https://{snapshot}.EXAMPLEUSERCONTENT.NET", + ): + settings = Settings.from_env() + identity = "a" * 32 + assert settings.documentation_snapshot(f"{identity}.exampleusercontent.net") == identity + + +def archive(files: dict[str, str]) -> bytes: + output = io.BytesIO() + with zipfile.ZipFile(output, "w") as bundle: + for name, content in files.items(): + bundle.writestr(name, content) + return output.getvalue() + + +async def check_documentation( + client: httpx.AsyncClient, token: str, other_token: str, artifacts +) -> None: + check_content_sites() + authorization = {"Authorization": f"Bearer {token}"} + base = "/v1/extensions/check/documentation/releases/1.0.0" + docs = base + "/documentation" + entry_url = "/documentation/check/documentation/1.0.0/global/" + private = docs.replace("/v1/", "/v1/publisher/", 1) + + async def request(method, path, expected=200, **kwargs): + response = await client.request(method, path, **kwargs) + assert response.status_code == expected, ( + method, + path, + response.status_code, + response.text[:800], + ) + return response + + await request( + "POST", + base.rsplit("/", 1)[0], + headers=authorization, + json={ + "nickname": "Documentation acceptance", + "version": "1.0.0", + "module_federation": { + "source_repository": "https://example.invalid/repo", + "source_revision": "revision", + "host_sdk": "@inkcre/core", + "host_sdk_version": "^1.0.0", + }, + }, + ) + files = { + "index.html": 'Guide', + "assets/main.css": "body { color: green; }", + "guide/index.html": "Directory guide", + "clean.html": "Clean URL", + "search.json": '{"guide":"find me"}', + } + zipped = archive(files) + + def metadata(content=zipped): + return { + "snapshot_id": secrets.token_hex(16), + "content_sha256": inspect_documentation(content).digest, + "source_repository": "https://example.invalid/repo", + "source_revision": "revision", + "entry": "index.html", + "build_id": "check", + "expected_etag": None, + } + + async def upload(payload, content=zipped, expected=200, headers=None, scope="global"): + return await request( + "POST", + docs + "/" + scope, + expected, + headers={**authorization, **(headers or {})}, + data={"metadata": json.dumps(payload)}, + files={"content": ("docs.zip", content)}, + ) + + first = metadata() + await upload(first, expected=401, headers={"Authorization": ""}) + await upload(first, expected=403, headers={"Authorization": f"Bearer {other_token}"}) + await upload( + {key: value for key, value in first.items() if key != "expected_etag"}, expected=400 + ) + await upload(first, expected=409, scope="python") + first_response = await upload(first) + assert "etag" not in first_response.headers + first_record = first_response.json() + assert (await upload(first)).json() == first_record + assert (await request("GET", private, headers=authorization)).json()["sets"][0][ + "snapshot_id" + ] == first["snapshot_id"] + await request("GET", docs, 404) + await request("GET", entry_url, 404) + await request("GET", first_record["snapshot_url"], 404) + await request("POST", base + "/publish", 409, headers=authorization) + await request( + "POST", + base + "/module-federation", + headers=authorization, + files={ + "content": ( + "mf.zip", + archive( + { + "mf-manifest.json": json.dumps( + { + "metaData": { + "publicPath": "./", + "remoteEntry": {"name": "remoteEntry.js"}, + } + } + ), + "remoteEntry.js": "export const ok = true", + } + ), + ) + }, + ) + await request("POST", base + "/publish", headers=authorization) + native_before = (await request("GET", base)).json() + public = (await request("GET", docs)).json() + assert public["sets"][0]["snapshot_id"] == first["snapshot_id"] + root = first_record["snapshot_url"] + entry = await request("GET", entry_url, 307) + assert entry.headers["location"] == root + for path, content in files.items(): + response = await request("GET", root + path) + assert response.text == content + assert response.headers["cache-control"] == "public, no-cache" + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["origin-agent-cluster"] == "?1" + assert "set-cookie" not in response.headers + await request("GET", root + path, 304, headers={"If-None-Match": response.headers["etag"]}) + head = await request("HEAD", root + path) + assert head.content == b"" and int(head.headers["content-length"]) == len(content.encode()) + assert (await request("GET", root)).text == files["index.html"] + await request("GET", root + "guide", 308) + assert (await request("GET", root + "guide/")).text == files["guide/index.html"] + assert (await request("GET", root + "clean")).text == files["clean.html"] + await request("GET", root + "missing", 404) + await request("GET", root + "v1/publisher", 404, headers=authorization) + await request( + "POST", + root + "v1/extensions/check/documentation/releases/1.0.0/documentation/global", + 405, + headers=authorization, + ) + await request("GET", "http://unknown.localhost/v1/extensions", 421) + + # A failed staging operation cannot move the pointer. Neither can a stale editor. + changed_zip = archive({**files, "index.html": "Corrected page"}) + second = {**metadata(changed_zip), "expected_etag": first_record["snapshot_etag"]} + + async def fail_put(*args, **kwargs): + raise OSError("simulated storage failure") + + with patch.object(artifacts, "put", fail_put): + try: + await upload(second, changed_zip) + except OSError: + pass + else: + raise AssertionError("failed storage upload unexpectedly succeeded") + assert (await request("GET", docs)).json() == public + second_record = (await upload(second, changed_zip)).json() + assert second_record["snapshot_url"] != root + assert (await request("GET", root)).text == files["index.html"] + assert (await request("GET", second_record["snapshot_url"])).text == "Corrected page" + await upload({**metadata(), "expected_etag": first_record["snapshot_etag"]}, expected=409) + before_replay = (await request("GET", docs)).json() + assert (await upload(first)).json() == first_record + assert (await request("GET", docs)).json() == before_replay + for change in ( + {"source_revision": "changed"}, + {"entry": "clean.html"}, + {"expected_etag": second_record["snapshot_etag"]}, + ): + await upload({**first, **change}, expected=409) + await upload(first, expected=409, scope="module-federation") + await upload( + { + **metadata(), + "snapshot_id": first["snapshot_id"], + "expected_etag": second_record["snapshot_etag"], + }, + expected=409, + ) + + # Two editors observing the same pointer cannot both win. + async def contender(): + return await client.post( + docs + "/global", + headers=authorization, + data={ + "metadata": json.dumps( + {**metadata(), "expected_etag": second_record["snapshot_etag"]} + ) + }, + files={"content": ("docs.zip", zipped)}, + ) + + outcomes = await asyncio.gather(contender(), contender()) + assert sorted(item.status_code for item in outcomes) == [200, 409] + current = next(item.json() for item in outcomes if item.status_code == 200) + assert (await request("GET", base)).json() == native_before + await request("POST", base + "/yank", headers=authorization) + warning = await request("GET", entry_url) + assert "withdrawn" in warning.text and current["snapshot_url"] in warning.text + repeated = {**metadata(), "expected_etag": current["snapshot_etag"]} + duplicates = await asyncio.gather(upload(repeated), upload(repeated)) + assert duplicates[0].json() == duplicates[1].json() + assert await db.DocumentationSnapshot.filter(id=repeated["snapshot_id"]).count() == 1 + await request("GET", root) + + row = await db.Release.get(extension_id="check/documentation", version="1.0.0") + try: + async with in_transaction(): + await db.DocumentationSet.filter(release=row).update(scope="invalid") + except IntegrityError: + pass + else: + raise AssertionError("database admitted an invalid documentation scope") + # Bypass publish to prove the database protects pointer ownership itself. + other_release = await db.Release.create(extension_id=row.extension_id, version="2.0.0") + for values in ({"scope": "python"}, {"release_id": other_release.id}): + try: + async with in_transaction(): + await db.DocumentationSet.filter(release=row).update(**values) + except IntegrityError: + pass + else: + raise AssertionError(f"database admitted a cross-owner documentation pointer: {values}") + # The reverse direction must also be protected while the pointer exists. + current_set = await db.DocumentationSet.get(release=row, scope="global").select_related( + "snapshot" + ) + try: + async with in_transaction(): + await db.DocumentationSnapshot.filter(id=current_set.snapshot.id).update(scope="python") + except IntegrityError: + pass + else: + raise AssertionError("database allowed a referenced snapshot to change ownership") + await db.Release.filter(id=row.id).update(state="blocked") + for path in ( + docs, + private, + entry_url, + root, + root + "assets/main.css", + second_record["snapshot_url"], + ): + for method in ("GET", "HEAD") if path.startswith(root) else ("GET",): + response = await request( + method, path, 451, headers={**authorization, "If-None-Match": "*"} + ) + assert response.headers["cache-control"] == "no-store" + await upload(metadata(), expected=451) + await upload(first, expected=451) + + # Inspect raw ZIP names, not normalized extraction output; no files touch disk. + for bad in ( + "../escape.html", + "/absolute.html", + "a//b.html", + "a/%2e.html", + ".env", + "_headers", + "script.php", + "a\\b.html", + ): + try: + inspect_documentation(archive({"index.html": "ok", bad: "bad"})) + except ValueError: + pass + else: + raise AssertionError(f"unsafe documentation member admitted: {bad}") + output = io.BytesIO() + with zipfile.ZipFile(output, "w") as zipped_link: + link = zipfile.ZipInfo("index.html") + link.external_attr = (stat.S_IFLNK | 0o777) << 16 + zipped_link.writestr(link, "outside") + try: + inspect_documentation(output.getvalue()) + except ValueError: + pass + else: + raise AssertionError("documentation symlink admitted") + + # A finite retry resends the candidate, never queries or adopts current. + payload = DocumentationUpload.model_validate( + {key: value for key, value in first.items() if key != "expected_etag"} + ) + requests: list[httpx.Request] = [] + + def recover(request: httpx.Request) -> httpx.Response: + requests.append(request) + if len(requests) == 1: + raise httpx.ReadTimeout("lost response", request=request) + return httpx.Response(200, json=first_record) + + with RegistryClient( + "http://localhost", token=token, transport=httpx.MockTransport(recover) + ) as toolkit: + recovered = toolkit.upload_documentation( + "check", "documentation", "1.0.0", "global", payload, zipped + ) + assert recovered.snapshot_id == first["snapshot_id"] + assert [item.method for item in requests] == ["POST", "POST"] + + for unavailable in (503, 409, 200): + attempts = 0 + + def fail(request: httpx.Request, status=unavailable) -> httpx.Response: + nonlocal attempts + attempts += 1 + return httpx.Response(status, json={"detail": "unavailable"}) + + with RegistryClient("http://localhost", transport=httpx.MockTransport(fail)) as toolkit: + try: + toolkit.upload_documentation( + "check", "documentation", "1.0.0", "global", payload, zipped + ) + except DocumentationOutcomeUnknown: + assert unavailable in {503, 200} and attempts == 2 + except RegistryHTTPError: + assert unavailable == 409 and attempts == 1 + else: + raise AssertionError("a missing receipt must not be reported as success") diff --git a/scripts/check_packages.py b/scripts/check_packages.py new file mode 100644 index 0000000..bb0b878 --- /dev/null +++ b/scripts/check_packages.py @@ -0,0 +1,76 @@ +"""Install the built Registry and Toolkit wheels without workspace/editable imports.""" + +from __future__ import annotations + +import email +import os +import subprocess +import tempfile +import tomllib +import venv +import zipfile +from pathlib import Path + +from packaging.requirements import Requirement + +ROOT = Path(__file__).resolve().parents[1] + + +def main() -> None: + wheels = [] + for directory in (ROOT, ROOT / "toolkit"): + project = tomllib.loads((directory / "pyproject.toml").read_text())["project"] + wheels.append( + ROOT + / "dist" + / f"{project['name'].replace('-', '_')}-{project['version']}-py3-none-any.whl" + ) + with zipfile.ZipFile(wheels[0]) as archive: + metadata_path = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + metadata = email.message_from_bytes(archive.read(metadata_path)) + requirement = next( + item + for value in metadata.get_all("Requires-Dist", []) + if (item := Requirement(value)).name == "inkcre-extension-toolkit" + ) + # 0.2.1 lacks documentation admission, even if the workspace import succeeds. + assert "0.2.1" not in requirement.specifier, ( + "Registry wheel admits Toolkit without documentation API" + ) + with tempfile.TemporaryDirectory(prefix="registry-wheel-check-") as temporary: + environment = Path(temporary) + venv.EnvBuilder(with_pip=True, symlinks=os.name != "nt").create(environment) + python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + subprocess.run( + [ + str(python), + "-I", + "-m", + "pip", + "install", + "--disable-pip-version-check", + *map(str, wheels), + ], + cwd=environment, + check=True, + ) + subprocess.run([str(python), "-I", "-m", "pip", "check"], cwd=environment, check=True) + subprocess.run( + [ + str(python), + "-I", + "-c", + "from inkcre_extension_registry.service.app import create_app; " + "from inkcre_extension_toolkit.documentation import inspect_documentation; " + "assert create_app().openapi()['paths']['/v1/documentation-hosting']", + ], + cwd=environment, + check=True, + ) + print("Registry and Toolkit wheel metadata, isolated installation and service import passed.") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 251fb0d..9c863ab 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -21,6 +21,7 @@ import httpx import psycopg from check_d1_import import check_import +from check_documentation import check_documentation from moto.server import ThreadedMotoServer from registry_database import configure_runtime_login from tortoise import Tortoise @@ -355,6 +356,7 @@ async def conflicting_upload(): denied = await request(method, path, 451, headers={"If-None-Match": "*"}) assert denied.headers["cache-control"] == "no-store" assert "check-extension" not in (await request("GET", "/simple/")).text + await check_documentation(client, token, other_token, artifacts) await db.Credential.all().update(disabled=True) await request("GET", "/v1/publisher", 401, private=True) @@ -397,6 +399,7 @@ async def main() -> None: os.environ.update( DATABASE_URL=url, PUBLIC_ORIGIN="http://localhost", + DOCUMENTATION_ORIGIN_TEMPLATE="http://{snapshot}.docs.localhost", S3_ENDPOINT_URL=f"http://{host}:{port}", S3_BUCKET="registry-check", AWS_ACCESS_KEY_ID="testing", @@ -430,6 +433,8 @@ async def main() -> None: db.ModuleFederationDistribution, db.PythonDistribution, db.PythonFile, + db.DocumentationSnapshot, + db.DocumentationSet, ) ): await model.all().delete() @@ -437,6 +442,8 @@ async def main() -> None: print("Registry PostgreSQL migrations, transactions, HTTP and S3 checks passed.") finally: for model in ( + db.DocumentationSet, + db.DocumentationSnapshot, db.PythonFile, db.PythonDistribution, db.ModuleFederationDistribution, diff --git a/scripts/generate_contracts.py b/scripts/generate_contracts.py index 4e8212c..b66398d 100644 --- a/scripts/generate_contracts.py +++ b/scripts/generate_contracts.py @@ -11,6 +11,7 @@ from inkcre_extension_toolkit.preview import PreviewInventory from inkcre_extension_registry.contracts.models import ( + DocumentationContracts, ExtensionRecord, InstalledExtension, PrepareReleaseRequest, @@ -30,6 +31,7 @@ def _encoded(value: Any) -> bytes: def generated_contracts() -> dict[Path, bytes]: models = { + "documentation.schema.json": DocumentationContracts.model_json_schema(mode="serialization"), "extension.schema.json": ExtensionRecord.model_json_schema(mode="serialization"), "prepare-release.schema.json": PrepareReleaseRequest.model_json_schema( mode="serialization" @@ -44,7 +46,8 @@ def generated_contracts() -> dict[Path, bytes]: outputs[CONTRACTS / "openapi.json"] = _encoded(create_app().openapi()) outputs[CONTRACTS / "revision.json"] = _encoded( { - "contract_revision": 2, + "contract_revision": 3, + "documentation_scopes": ["global", "module-federation", "python"], "distribution_kinds": ["module_federation", "python"], "extension_version": "strict-semver-without-build-metadata", "python_upload_filetypes": ["bdist_wheel"], @@ -95,6 +98,10 @@ def _generate_python_binding(schema: bytes, output: Path) -> bytes: def generated_python_bindings() -> dict[Path, bytes]: return { + TOOLKIT_GENERATED / "documentation.py": _generate_python_binding( + _encoded(DocumentationContracts.model_json_schema(mode="serialization")), + TOOLKIT_GENERATED / "documentation.py", + ), TOOLKIT_GENERATED / "contracts.py": _generate_python_binding( _encoded(PythonConsumerContracts.model_json_schema(mode="serialization")), TOOLKIT_GENERATED / "contracts.py", diff --git a/src/inkcre_extension_registry/contracts/models.py b/src/inkcre_extension_registry/contracts/models.py index 6a4298d..fa5976b 100644 --- a/src/inkcre_extension_registry/contracts/models.py +++ b/src/inkcre_extension_registry/contracts/models.py @@ -122,6 +122,62 @@ class ContractModel(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) +DocumentationScope = Literal["global", "python", "module-federation"] + + +class RegistryError(ContractModel): + detail: str + + +class DocumentationUpload(ContractModel): + snapshot_id: str = Field(pattern=r"^[0-9a-f]{32}$") + content_sha256: str = Field(pattern=r"^[0-9a-f]{64}$") + entry: str = Field(default="index.html", min_length=1, max_length=768) + source_repository: str = Field(min_length=1, max_length=2048) + source_revision: str = Field(min_length=1, max_length=256) + build_id: str | None = Field(default=None, max_length=256) + + +class DocumentationRecord(DocumentationUpload): + scope: DocumentationScope + etag: str + entry_url: str + snapshot_url: str + updated_at: str + + +class DocumentationPublication(DocumentationUpload): + expected_etag: str | None = Field(pattern=r'^"[0-9a-f]{64}"$') + + +class DocumentationReceipt(DocumentationUpload): + name: CanonicalExtensionName + version: StrictSemVer + scope: DocumentationScope + snapshot_etag: str + snapshot_url: str + committed_at: str + + +class ReleaseDocumentation(ContractModel): + name: CanonicalExtensionName + version: StrictSemVer + state: ReleaseState + sets: list[DocumentationRecord] + + +class DocumentationHosting(ContractModel): + origin_template: str + + +class DocumentationContracts(ContractModel): + upload: DocumentationUpload + publication: DocumentationPublication + receipt: DocumentationReceipt + release: ReleaseDocumentation + hosting: DocumentationHosting + + class PythonEntryPoint(ContractModel): group: str = Field(min_length=1, max_length=128, pattern=ENTRY_GROUP_PATTERN_TEXT) name: str = Field(min_length=1, max_length=128, pattern=ENTRY_NAME_PATTERN_TEXT) diff --git a/src/inkcre_extension_registry/migrations/0003_auto_20260921_0025.py b/src/inkcre_extension_registry/migrations/0003_auto_20260921_0025.py new file mode 100644 index 0000000..37ac132 --- /dev/null +++ b/src/inkcre_extension_registry/migrations/0003_auto_20260921_0025.py @@ -0,0 +1,146 @@ +import functools +from json import dumps, loads + +from tortoise import fields, migrations +from tortoise.fields.base import OnDelete +from tortoise.migrations import operations as ops +from tortoise.migrations.constraints import CheckConstraint + + +class Migration(migrations.Migration): + dependencies = [("models", "0002_runtime_access")] + + initial = False + + operations = [ + ops.CreateModel( + name="DocumentationSnapshot", + fields=[ + ( + "id", + fields.CharField(primary_key=True, unique=True, db_index=True, max_length=32), + ), + ( + "release", + fields.ForeignKeyField( + "models.Release", + source_field="release_id", + db_constraint=True, + to_field="id", + on_delete=OnDelete.RESTRICT, + ), + ), + ("scope", fields.CharField(max_length=32)), + ("content_sha256", fields.CharField(max_length=64)), + ("entry", fields.TextField(unique=False)), + ("source_repository", fields.TextField(unique=False)), + ("source_revision", fields.TextField(unique=False)), + ("build_id", fields.TextField(null=True, unique=False)), + ( + "files", + fields.JSONField( + encoder=functools.partial(dumps, separators=(",", ":")), decoder=loads + ), + ), + ("etag", fields.CharField(max_length=64)), + ("created_at", fields.DatetimeField(auto_now=False, auto_now_add=True)), + ], + options={ + "table": "documentation_snapshots", + "app": "models", + "constraints": [ + CheckConstraint(check="id ~ '^[0-9a-f]{32}$'", name="docs_snapshot_id"), + CheckConstraint( + check="scope IN ('global', 'python', 'module-federation')", + name="docs_snapshot_scope", + ), + CheckConstraint( + check="content_sha256 ~ '^[0-9a-f]{64}$' AND etag ~ '^[0-9a-f]{64}$'", + name="docs_snapshot_hash", + ), + CheckConstraint( + check="jsonb_typeof(files) = 'object'", name="docs_snapshot_files" + ), + ], + "pk_attr": "id", + }, + bases=["Model"], + ), + ops.CreateModel( + name="DocumentationSet", + fields=[ + ( + "id", + fields.BigIntField( + generated=True, primary_key=True, unique=True, db_index=True + ), + ), + ( + "release", + fields.ForeignKeyField( + "models.Release", + source_field="release_id", + db_constraint=True, + to_field="id", + on_delete=OnDelete.RESTRICT, + ), + ), + ("scope", fields.CharField(max_length=32)), + ( + "snapshot", + fields.ForeignKeyField( + "models.DocumentationSnapshot", + source_field="snapshot_id", + db_constraint=True, + to_field="id", + on_delete=OnDelete.RESTRICT, + ), + ), + ("updated_at", fields.DatetimeField(auto_now=True, auto_now_add=False)), + ], + options={ + "table": "documentation_sets", + "app": "models", + "unique_together": (("release_id", "scope"),), + "constraints": [ + CheckConstraint( + check="scope IN ('global', 'python', 'module-federation')", + name="docs_set_scope", + ) + ], + "pk_attr": "id", + }, + bases=["Model"], + ), + ops.AddConstraint( + model_name="DocumentationSnapshot", + constraint=CheckConstraint("id ~ '^[0-9a-f]{32}$'", "docs_snapshot_id"), + ), + ops.AddConstraint( + model_name="DocumentationSnapshot", + constraint=CheckConstraint( + "scope IN ('global', 'python', 'module-federation')", "docs_snapshot_scope" + ), + ), + ops.AddConstraint( + model_name="DocumentationSnapshot", + constraint=CheckConstraint( + "content_sha256 ~ '^[0-9a-f]{64}$' AND etag ~ '^[0-9a-f]{64}$'", + "docs_snapshot_hash", + ), + ), + ops.AddConstraint( + model_name="DocumentationSnapshot", + constraint=CheckConstraint("jsonb_typeof(files) = 'object'", "docs_snapshot_files"), + ), + ops.AddConstraint( + model_name="DocumentationSet", + constraint=CheckConstraint( + "scope IN ('global', 'python', 'module-federation')", "docs_set_scope" + ), + ), + ops.RunSQL( + "GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE documentation_snapshots, documentation_sets TO registry_app" + ), + ops.RunSQL("GRANT USAGE ON SEQUENCE documentation_sets_id_seq TO registry_app"), + ] diff --git a/src/inkcre_extension_registry/migrations/0004_documentation_ownership.py b/src/inkcre_extension_registry/migrations/0004_documentation_ownership.py new file mode 100644 index 0000000..8c23dd3 --- /dev/null +++ b/src/inkcre_extension_registry/migrations/0004_documentation_ownership.py @@ -0,0 +1,24 @@ +"""Keep the current pointer attached to its snapshot's exact Release and scope.""" + +from tortoise import migrations +from tortoise.migrations import operations as ops +from tortoise.migrations.constraints import UniqueConstraint + + +class Migration(migrations.Migration): + dependencies = [("models", "0003_auto_20260921_0025")] + + operations = [ + ops.AddConstraint( + model_name="DocumentationSnapshot", + constraint=UniqueConstraint(("id", "release_id", "scope"), "docs_snapshot_owner"), + ), + # Tortoise relations model single-column FKs; PostgreSQL owns this + # cross-row invariant in addition to the ORM's navigable snapshot FK. + ops.RunSQL( + "ALTER TABLE documentation_sets ADD CONSTRAINT docs_set_snapshot_owner " + "FOREIGN KEY (snapshot_id, release_id, scope) " + "REFERENCES documentation_snapshots (id, release_id, scope) ON DELETE RESTRICT", + "ALTER TABLE documentation_sets DROP CONSTRAINT docs_set_snapshot_owner", + ), + ] diff --git a/src/inkcre_extension_registry/service/app.py b/src/inkcre_extension_registry/service/app.py index 48c2209..0fe5140 100644 --- a/src/inkcre_extension_registry/service/app.py +++ b/src/inkcre_extension_registry/service/app.py @@ -7,26 +7,36 @@ import mimetypes import os import re -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, suppress from time import perf_counter from typing import Annotated, Any +from urllib.parse import urlparse +import anyio from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request, Response, status from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse +from inkcre_extension_toolkit.documentation import document_path, inspect_documentation from packaging.version import InvalidVersion from packaging.version import Version as Pep440Version +from pydantic import ValidationError from starlette.datastructures import UploadFile from tortoise.contrib.fastapi import RegisterTortoise from .. import __version__ from ..contracts.models import ( + DocumentationHosting, + DocumentationPublication, + DocumentationReceipt, + DocumentationScope, ExtensionRecord, ExtensionSummary, PrepareReleaseRequest, PublisherWorkspace, PythonEntryPoint, + RegistryError, RegistrySegment, + ReleaseDocumentation, ReleaseRecord, StrictSemVer, YankRequest, @@ -34,6 +44,7 @@ validate_segment, validate_version, ) +from . import documentation from .module_federation import ( ModuleFederationValidationError, inspect_module_federation_snapshot, @@ -64,21 +75,48 @@ root_json, ) from .storage import ArtifactStore -from .ui import SCRIPT, extension_catalog_html, extension_detail_html, html_response, page +from .ui import ( + SCRIPT, + extension_catalog_html, + extension_detail_html, + html_response, + page, + select_release, +) MAX_UPLOAD_BYTES = 20 * 1024 * 1024 logger = logging.getLogger(__name__) BEARER_PATTERN = re.compile(r"^Bearer ([A-Za-z0-9._~-]{24,512})$") UPLOAD_PATH_PATTERN = re.compile( - r"^/legacy/$|^/v1/extensions/[^/]+/[^/]+/releases/[^/]+/module-federation$" + r"^/legacy/$|^/v1/extensions/[^/]+/[^/]+/releases/[^/]+/(module-federation|documentation/[^/]+)$" ) +DOCUMENTATION_ERRORS = { + code: {"model": RegistryError, "description": description} + for code, description in { + 400: "Malformed conditional headers, multipart fields, metadata, or static ZIP.", + 401: "Publisher credential is missing or invalid.", + 403: "Publisher does not own this namespace.", + 404: "Release or publicly readable documentation does not exist.", + 409: "Publication identity conflict, stale expected_etag, or missing association.", + 411: "A non-chunked Content-Length is required.", + 413: "The complete multipart request exceeds 20 MiB.", + 415: "The request must use multipart/form-data.", + 451: "The Release is operator-blocked, including for its publisher.", + 503: "Documentation content hosting is not configured.", + }.items() +} + def _repository(request: Request) -> RegistryRepository: return request.app.state.repository +def _documentation(request: Request) -> documentation.DocumentationRepository: + return request.app.state.documentation + + def _credential_from_authorization(authorization: str | None) -> str | None: bearer = BEARER_PATTERN.fullmatch(authorization or "") if bearer is not None: @@ -144,8 +182,21 @@ async def _form(request: Request) -> Any: content_type = request.headers.get("content-type", "") if not content_type.lower().startswith("multipart/form-data"): raise HTTPException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, "multipart/form-data required") + received = 0 + + async def limited_receive(): + nonlocal received + message = await request.receive() + received += len(message.get("body", b"")) + if received > MAX_UPLOAD_BYTES: + raise HTTPException(413, "multipart upload exceeds 20 MiB") + return message + try: - return await request.form(max_files=1, max_fields=64, max_part_size=MAX_UPLOAD_BYTES) + bounded = Request(request.scope, receive=limited_receive) + return await bounded.form(max_files=1, max_fields=64, max_part_size=MAX_UPLOAD_BYTES) + except HTTPException: + raise except Exception as error: raise HTTPException(status.HTTP_400_BAD_REQUEST, "invalid multipart upload") from error @@ -200,6 +251,7 @@ async def lifespan(app: FastAPI): async with RegisterTortoise(app, config=database_config(settings.database_url)): app.state.settings = settings app.state.repository = RegistryRepository(artifacts) + app.state.documentation = documentation.DocumentationRepository(artifacts) yield finally: await artifacts.close() @@ -215,13 +267,13 @@ def create_app() -> FastAPI: app.add_middleware( CORSMiddleware, allow_origins=["*"], - allow_methods=["GET", "HEAD", "OPTIONS", "POST"], - allow_headers=["Authorization", "Content-Type"], + allow_methods=["GET", "HEAD", "OPTIONS", "POST", "PUT"], + allow_headers=["Authorization", "Content-Type", "If-Match", "If-None-Match"], ) @app.middleware("http") async def bound_uploads(request: Request, call_next: Any) -> Response: - if request.method == "POST" and UPLOAD_PATH_PATTERN.fullmatch(request.url.path): + if request.method in {"POST", "PUT"} and UPLOAD_PATH_PATTERN.fullmatch(request.url.path): content_length = request.headers.get("content-length") if ( content_length is None @@ -251,6 +303,260 @@ async def bound_uploads(request: Request, call_next: Any) -> Response: response.headers["Cache-Control"] = "no-store" return response + @app.middleware("http") + async def content_origin(request: Request, call_next: Any) -> Response: + settings: Settings = request.app.state.settings + if settings.documentation_origin_template is None: + return await call_next(request) + authority = request.headers.get("host", "").lower() + snapshot_id = settings.documentation_snapshot(authority) + if snapshot_id is None: + if authority != urlparse(settings.public_origin).netloc.lower(): + return JSONResponse( + {"detail": "unrecognized Registry host"}, + status_code=421, + headers={"Cache-Control": "no-store"}, + ) + return await call_next(request) + # This branch never enters the management router, even for /v1/*. + try: + if request.method not in {"GET", "HEAD"}: + raise HTTPException( + 405, "content origins are read-only", headers={"Allow": "GET, HEAD"} + ) + snapshot = await _documentation(request).public_snapshot(snapshot_id) + path = request.url.path.removeprefix("/") + if path: + document_path(path.removesuffix("/")) + target, redirect = documentation.static_path(snapshot, path) + if redirect: + response = RedirectResponse( + documentation.snapshot_link(settings, snapshot_id, path + "/"), status_code=308 + ) + response.headers["Cache-Control"] = "no-store" + else: + file = snapshot.files[target] + response = await _public_response( + request, + PublicObject( + documentation.object_key(snapshot.content_sha256, target), + file["media_type"], + file["sha256"], + ), + ) + except (RegistryBlockedError, RegistryNotFoundError) as error: + mapped = _map_repository_error(error) + response = JSONResponse( + {"detail": mapped.detail}, + status_code=mapped.status_code, + headers={"Cache-Control": "no-store"}, + ) + except ValueError: + response = JSONResponse( + {"detail": "invalid documentation path"}, + status_code=404, + headers={"Cache-Control": "no-store"}, + ) + except HTTPException as error: + response = JSONResponse( + {"detail": error.detail}, + status_code=error.status_code, + headers={"Cache-Control": "no-store", **(error.headers or {})}, + ) + response.headers["Origin-Agent-Cluster"] = "?1" + response.headers["Referrer-Policy"] = "no-referrer" + response.headers["X-Content-Type-Options"] = "nosniff" + return response + + def documentation_settings(request: Request) -> Settings: + settings = request.app.state.settings + if settings.documentation_origin_template is None: + raise HTTPException(503, "documentation content hosting is not configured") + return settings + + @app.get( + "/v1/documentation-hosting", + response_model=DocumentationHosting, + responses={503: DOCUMENTATION_ERRORS[503]}, + ) + async def documentation_hosting(request: Request, response: Response) -> DocumentationHosting: + settings = documentation_settings(request) + response.headers["Cache-Control"] = "no-store" + assert settings.documentation_origin_template is not None + return DocumentationHosting(origin_template=settings.documentation_origin_template) + + async def read_documentation( + namespace: str, + name: str, + version: str, + request: Request, + response: Response, + *, + public: bool, + ) -> ReleaseDocumentation: + extension = _validate_identity(namespace, name, version) + settings = documentation_settings(request) + response.headers["Cache-Control"] = "no-store" + try: + return await _documentation(request).discover( + extension, version, settings, public=public + ) + except (RegistryBlockedError, RegistryNotFoundError) as error: + raise _map_repository_error(error) from error + + @app.get( + "/v1/extensions/{namespace}/{name}/releases/{version}/documentation", + response_model=ReleaseDocumentation, + responses={code: DOCUMENTATION_ERRORS[code] for code in (404, 451, 503)}, + ) + async def get_documentation( + namespace: RegistrySegment, + name: RegistrySegment, + version: StrictSemVer, + request: Request, + response: Response, + ) -> ReleaseDocumentation: + return await read_documentation(namespace, name, version, request, response, public=True) + + @app.get( + "/v1/publisher/extensions/{namespace}/{name}/releases/{version}/documentation", + response_model=ReleaseDocumentation, + responses={code: DOCUMENTATION_ERRORS[code] for code in (401, 403, 404, 451, 503)}, + ) + async def publisher_documentation( + namespace: RegistrySegment, + name: RegistrySegment, + version: StrictSemVer, + request: Request, + response: Response, + _publisher: Annotated[str, Depends(_publisher_namespace)], + ) -> ReleaseDocumentation: + return await read_documentation(namespace, name, version, request, response, public=False) + + @app.post( + "/v1/extensions/{namespace}/{name}/releases/{version}/documentation/{scope}", + response_model=DocumentationReceipt, + responses={ + **DOCUMENTATION_ERRORS, + 200: { + "description": "Historical commit confirmed; current may differ.", + }, + }, + openapi_extra={ + "requestBody": { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["metadata", "content"], + "properties": { + "metadata": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": DocumentationPublication.model_json_schema(), + "description": ( + "JSON-encoded DocumentationPublication; " + "send as a text form field, not a file." + ), + }, + "content": {"type": "string", "format": "binary"}, + }, + } + } + }, + } + }, + ) + async def upload_documentation( + namespace: RegistrySegment, + name: RegistrySegment, + version: StrictSemVer, + scope: DocumentationScope, + request: Request, + response: Response, + _publisher: Annotated[str, Depends(_publisher_namespace)], + ) -> DocumentationReceipt: + extension = _validate_identity(namespace, name, version) + settings = documentation_settings(request) + form = await _form(request) + try: + upload = form.get("content") + if not isinstance(upload, UploadFile): + raise HTTPException(400, "content ZIP file is required") + try: + payload = DocumentationPublication.model_validate_json( + _form_string(form, "metadata") or "" + ) + bundle = await anyio.to_thread.run_sync( + inspect_documentation, await upload.read(), payload.entry + ) + except (ValueError, ValidationError) as error: + raise HTTPException(400, str(error)) from error + try: + result = await _documentation(request).publish( + extension, + version, + scope, + payload, + bundle, + settings, + ) + except documentation.DocumentationPreconditionError as error: + raise HTTPException(409, str(error)) from error + except (RegistryConflictError, RegistryStateError, RegistryNotFoundError) as error: + raise _map_repository_error(error) from error + finally: + await form.close() + response.headers["Cache-Control"] = "no-store" + return result + + @app.api_route( + "/documentation/{namespace}/{name}/{version}/{scope}/{path:path}", + methods=["GET", "HEAD"], + include_in_schema=False, + ) + async def documentation_entry( + namespace: RegistrySegment, + name: RegistrySegment, + version: StrictSemVer, + scope: DocumentationScope, + path: str, + request: Request, + ) -> Response: + settings = documentation_settings(request) + extension = _validate_identity(namespace, name, version) + try: + found = await _documentation(request).discover(extension, version, settings) + selected = next((item for item in found.sets if item.scope == scope), None) + if selected is None: + raise RegistryNotFoundError("documentation scope does not exist") + if path: + document_path(path.removesuffix("/")) + snapshot = await _documentation(request).public_snapshot(selected.snapshot_id) + documentation.static_path(snapshot, path) + except ValueError as error: + raise HTTPException(404, "invalid documentation path") from error + except (RegistryBlockedError, RegistryNotFoundError) as error: + raise _map_repository_error(error) from error + target = documentation.snapshot_link(settings, selected.snapshot_id, path) + if found.state == "yanked": + return html_response( + page( + "documentation-yanked.html", + title="Withdrawn release documentation", + version=version, + name=extension, + target=target, + noindex=True, + ) + ) + return RedirectResponse( + target, + status_code=307, + headers={"Cache-Control": "no-store", "Referrer-Policy": "no-referrer"}, + ) + @app.get("/livez") async def livez() -> dict[str, str]: return { @@ -289,7 +595,14 @@ async def extension_detail( ) -> HTMLResponse: extension_name = _validate_identity(namespace, name) extension = await _repository(request).get_extension(extension_name) - document = extension_detail_html(extension, version) if extension else None + selected = select_release(extension, version) if extension else None + hosted = None + if selected is not None and request.app.state.settings.documentation_origin_template: + with suppress(RegistryBlockedError, RegistryNotFoundError): + hosted = await _documentation(request).discover( + extension_name, selected.version, request.app.state.settings + ) + document = extension_detail_html(extension, version, hosted) if extension else None if document is None: return html_response( page( diff --git a/src/inkcre_extension_registry/service/database.py b/src/inkcre_extension_registry/service/database.py index f707fa6..1d46d9a 100644 --- a/src/inkcre_extension_registry/service/database.py +++ b/src/inkcre_extension_registry/service/database.py @@ -3,7 +3,7 @@ from typing import ClassVar from tortoise import fields -from tortoise.migrations.constraints import CheckConstraint +from tortoise.migrations.constraints import CheckConstraint, UniqueConstraint from tortoise.models import Model @@ -57,6 +57,7 @@ class Meta(Model.Meta): class Release(Model): id = fields.BigIntField(primary_key=True) + extension_id: str extension: fields.ForeignKeyRelation[Extension] = fields.ForeignKeyField( "models.Extension", related_name="releases", @@ -173,3 +174,53 @@ class Meta(Model.Meta): "mf_complete_snapshot", ), ] + + +class DocumentationSnapshot(Model): + id = fields.CharField(max_length=32, primary_key=True) + release: fields.ForeignKeyRelation[Release] = fields.ForeignKeyField( + "models.Release", on_delete=fields.RESTRICT + ) + scope = fields.CharField(max_length=32) + content_sha256 = fields.CharField(max_length=64) + entry = fields.TextField() + source_repository = fields.TextField() + source_revision = fields.TextField() + build_id = fields.TextField(null=True) + files = fields.JSONField() + etag = fields.CharField(max_length=64) + created_at = fields.DatetimeField(auto_now_add=True) + + class Meta(Model.Meta): + table = "documentation_snapshots" + constraints: ClassVar = [ + UniqueConstraint(("id", "release_id", "scope"), "docs_snapshot_owner"), + CheckConstraint("id ~ '^[0-9a-f]{32}$'", "docs_snapshot_id"), + CheckConstraint( + "scope IN ('global', 'python', 'module-federation')", "docs_snapshot_scope" + ), + CheckConstraint( + "content_sha256 ~ '^[0-9a-f]{64}$' AND etag ~ '^[0-9a-f]{64}$'", + "docs_snapshot_hash", + ), + CheckConstraint("jsonb_typeof(files) = 'object'", "docs_snapshot_files"), + ] + + +class DocumentationSet(Model): + id = fields.BigIntField(primary_key=True) + release: fields.ForeignKeyRelation[Release] = fields.ForeignKeyField( + "models.Release", on_delete=fields.RESTRICT + ) + scope = fields.CharField(max_length=32) + snapshot: fields.ForeignKeyRelation[DocumentationSnapshot] = fields.ForeignKeyField( + "models.DocumentationSnapshot", on_delete=fields.RESTRICT + ) + updated_at = fields.DatetimeField(auto_now=True) + + class Meta(Model.Meta): + table = "documentation_sets" + unique_together = (("release_id", "scope"),) + constraints: ClassVar = [ + CheckConstraint("scope IN ('global', 'python', 'module-federation')", "docs_set_scope"), + ] diff --git a/src/inkcre_extension_registry/service/documentation.py b/src/inkcre_extension_registry/service/documentation.py new file mode 100644 index 0000000..f1e28c6 --- /dev/null +++ b/src/inkcre_extension_registry/service/documentation.py @@ -0,0 +1,259 @@ +"""Documentation snapshot persistence and lifecycle; HTTP routing is in app.py.""" + +from __future__ import annotations + +import hashlib +import json +from typing import cast +from urllib.parse import quote + +import anyio +from inkcre_extension_toolkit.documentation import DocumentationBundle +from tortoise.exceptions import IntegrityError +from tortoise.transactions import in_transaction + +from ..contracts.models import ( + DocumentationPublication, + DocumentationReceipt, + DocumentationRecord, + DocumentationScope, + ReleaseDocumentation, + ReleaseState, +) +from . import database as db +from .repository import ( + RegistryBlockedError, + RegistryConflictError, + RegistryNotFoundError, + RegistryStateError, +) +from .settings import Settings +from .storage import ArtifactStore + + +class DocumentationPreconditionError(RuntimeError): + pass + + +def readable(release: db.Release, *, public: bool = True) -> None: + if release.state == "blocked": + raise RegistryBlockedError("documentation Release is operator-blocked") + if public and release.state == "preparing": + raise RegistryNotFoundError("public documentation does not exist") + + +def record( + row: db.DocumentationSet, release: db.Release, settings: Settings +) -> DocumentationRecord: + snapshot = row.snapshot + return DocumentationRecord( + snapshot_id=snapshot.id, + content_sha256=snapshot.content_sha256, + entry=snapshot.entry, + source_repository=snapshot.source_repository, + source_revision=snapshot.source_revision, + build_id=snapshot.build_id, + scope=cast(DocumentationScope, row.scope), + etag=f'"{snapshot.etag}"', + entry_url=f"{settings.public_origin}/documentation/{release.extension_id}/{release.version}/{row.scope}/", + snapshot_url=settings.documentation_origin(snapshot.id) + "/", + updated_at=row.updated_at.isoformat(), + ) + + +def object_key(digest: str, path: str) -> str: + return f"documentation/{digest}/{path}" + + +def static_path(snapshot: db.DocumentationSnapshot, path: str) -> tuple[str, bool]: + """Exact files win; support directory indexes and clean HTML links, never SPA fallback.""" + if not path: + return snapshot.entry, False + if path in snapshot.files: + return path, False + directory = path.rstrip("/") + "/index.html" + if directory in snapshot.files: + return directory, not path.endswith("/") + if not path.endswith("/") and path + ".html" in snapshot.files: + return path + ".html", False + raise RegistryNotFoundError("documentation file does not exist") + + +def snapshot_link(settings: Settings, snapshot_id: str, path: str) -> str: + return settings.documentation_origin(snapshot_id) + "/" + quote(path, safe="/") + + +def receipt( + snapshot: db.DocumentationSnapshot, release: db.Release, settings: Settings +) -> DocumentationReceipt: + return DocumentationReceipt( + name=release.extension_id, + version=release.version, + scope=cast(DocumentationScope, snapshot.scope), + snapshot_id=snapshot.id, + content_sha256=snapshot.content_sha256, + entry=snapshot.entry, + source_repository=snapshot.source_repository, + source_revision=snapshot.source_revision, + build_id=snapshot.build_id, + snapshot_etag=f'"{snapshot.etag}"', + snapshot_url=settings.documentation_origin(snapshot.id) + "/", + committed_at=snapshot.created_at.isoformat(), + ) + + +class DocumentationRepository: + """Own documentation queries, atomic pointer replacement, and bounded object staging.""" + + def __init__(self, artifacts: ArtifactStore) -> None: + self._artifacts = artifacts + self._capacity = anyio.CapacityLimiter(4) + + async def discover( + self, extension: str, version: str, settings: Settings, *, public: bool = True + ) -> ReleaseDocumentation: + release = await db.Release.filter(extension_id=extension, version=version).first() + if release is None: + raise RegistryNotFoundError("Release does not exist") + readable(release, public=public) + rows = ( + await db.DocumentationSet.filter(release=release) + .select_related("snapshot") + .order_by("scope") + ) + return ReleaseDocumentation( + name=extension, + version=version, + state=cast(ReleaseState, release.state), + sets=[record(row, release, settings) for row in rows], + ) + + async def public_snapshot(self, snapshot_id: str) -> db.DocumentationSnapshot: + snapshot = ( + await db.DocumentationSnapshot.filter(id=snapshot_id).select_related("release").first() + ) + if snapshot is None: + raise RegistryNotFoundError("documentation snapshot does not exist") + readable(snapshot.release) + return snapshot + + async def publish( + self, + extension: str, + version: str, + scope: DocumentationScope, + payload: DocumentationPublication, + bundle: DocumentationBundle, + settings: Settings, + ) -> DocumentationReceipt: + if bundle.digest != payload.content_sha256: + raise RegistryConflictError("documentation content digest does not match metadata") + metadata = payload.model_dump(mode="json") + etag = hashlib.sha256( + json.dumps( + {**metadata, "name": extension, "version": version, "scope": scope}, + sort_keys=True, + separators=(",", ":"), + ).encode() + ).hexdigest() + + async def check( + release: db.Release, + ) -> db.DocumentationSet | db.DocumentationSnapshot | None: + readable(release, public=False) + association = { + "python": db.PythonDistribution, + "module-federation": db.ModuleFederationDistribution, + }.get(scope) + if association is not None and not await association.filter(release=release).exists(): + raise RegistryStateError( + "documentation scope requires its Distribution association" + ) + previous = await db.DocumentationSnapshot.filter(id=payload.snapshot_id).first() + if previous is not None: + # The ETag fingerprints target, metadata and original precondition. + # Confirming this commit never reactivates its historical snapshot. + if previous.etag != etag: + raise RegistryConflictError( + "snapshot identity belongs to a different publication" + ) + return previous + current = ( + await db.DocumentationSet.filter(release=release, scope=scope) + .select_related("snapshot") + .first() + ) + if (payload.expected_etag is None and current is not None) or ( + payload.expected_etag is not None + and (current is None or payload.expected_etag != f'"{current.snapshot.etag}"') + ): + raise DocumentationPreconditionError( + "documentation changed; read it before replacing" + ) + return current + + release = await db.Release.filter(extension_id=extension, version=version).first() + if release is None: + raise RegistryNotFoundError("Release does not exist") + existing = await check(release) + if isinstance(existing, db.DocumentationSnapshot): + return receipt(existing, release, settings) + paths = iter(bundle.files) + failure: Exception | None = None + + async def stage() -> None: + nonlocal failure + try: + for path in paths: + async with self._capacity: + key = object_key(bundle.digest, path) + stored = await self._artifacts.head(key) + if stored is None: + await self._artifacts.put( + key, + bundle.files[path], + content_type=bundle.manifest[path]["media_type"], + ) + elif stored.size != len(bundle.files[path]): + raise RegistryConflictError( + "documentation staging object size conflict" + ) + except Exception as error: + failure = failure or error + group.cancel_scope.cancel() + + async with anyio.create_task_group() as group: + for _ in range(min(4, len(bundle.files))): + group.start_soon(stage) + if failure is not None: + raise failure + try: + async with in_transaction(): + release = await db.Release.filter(id=release.id).select_for_update().get() + current = await check(release) + if isinstance(current, db.DocumentationSnapshot): + return receipt(current, release, settings) + snapshot = await db.DocumentationSnapshot.create( + id=payload.snapshot_id, + release=release, + scope=scope, + **{ + key: value + for key, value in metadata.items() + if key not in {"snapshot_id", "expected_etag"} + }, + files=bundle.manifest, + etag=etag, + ) + if current is None: + current = await db.DocumentationSet.create( + release=release, scope=scope, snapshot=snapshot + ) + else: + current.snapshot = snapshot + await current.save(update_fields=["snapshot_id", "updated_at"]) + except IntegrityError as error: + raise RegistryConflictError( + "documentation snapshot address is already bound" + ) from error + return receipt(snapshot, release, settings) diff --git a/src/inkcre_extension_registry/service/settings.py b/src/inkcre_extension_registry/service/settings.py index a282fd9..4172392 100644 --- a/src/inkcre_extension_registry/service/settings.py +++ b/src/inkcre_extension_registry/service/settings.py @@ -3,10 +3,13 @@ from __future__ import annotations import os +import re import ssl from dataclasses import dataclass, field from urllib.parse import parse_qs, unquote, urlparse +from publicsuffixlist import PublicSuffixList + def database_config(url: str) -> dict: parsed = urlparse(url) @@ -58,6 +61,23 @@ class Settings: public_origin: str s3_endpoint_url: str s3_bucket: str + documentation_origin_template: str | None = None + + def documentation_origin(self, snapshot_id: str) -> str: + if self.documentation_origin_template is None: + raise ValueError("documentation content origin is not configured") + if not re.fullmatch(r"[0-9a-f]{32}", snapshot_id): + raise ValueError("invalid documentation snapshot identity") + return self.documentation_origin_template.replace("{snapshot}", snapshot_id) + + def documentation_snapshot(self, authority: str) -> str | None: + if self.documentation_origin_template is None: + return None + template = urlparse(self.documentation_origin_template).netloc.lower() + match = re.fullmatch( + re.escape(template).replace(r"\{snapshot\}", "([0-9a-f]{32})"), authority.lower() + ) + return match.group(1) if match else None @classmethod def from_env(cls) -> Settings: @@ -79,9 +99,47 @@ def from_env(cls) -> Settings: or parsed.params ): raise ValueError("PUBLIC_ORIGIN must be an absolute HTTPS origin") + documentation_origin = os.environ.get("DOCUMENTATION_ORIGIN_TEMPLATE") + if documentation_origin is not None: + content = urlparse(documentation_origin) + local_content = content.scheme == "http" and (content.hostname or "").endswith( + ".localhost" + ) + if ( + documentation_origin.count("{snapshot}") != 1 + or not (content.hostname or "").startswith("{snapshot}.") + or (content.scheme != "https" and not local_content) + or content.username is not None + or content.password is not None + or content.path + or content.query + or content.fragment + or content.params + or "{" in documentation_origin.replace("{snapshot}", "") + or "}" in documentation_origin.replace("{snapshot}", "") + ): + raise ValueError( + "DOCUMENTATION_ORIGIN_TEMPLATE must be a separate wildcard HTTPS origin " + "with a leading {snapshot} label" + ) + # Author HTML/JS must not share the management site's cookie domain. + # Use the packaged ICANN + private PSL, never a startup network fetch. + if not (local_http and local_content): + psl = PublicSuffixList(accept_unknown=False, only_icann=False) + management_host = (parsed.hostname or "").encode("idna").decode("ascii") + content_host = (content.hostname or "").removeprefix("{snapshot}.") + content_host = content_host.encode("idna").decode("ascii") + management_site = psl.privatesuffix(management_host) + content_site = psl.privatesuffix(content_host) + if not management_site or not content_site or management_site == content_site: + raise ValueError( + "documentation and management must use different registrable domains " + "recognized by the packaged Public Suffix List" + ) return cls( database_url=os.environ["DATABASE_URL"], public_origin=origin, s3_endpoint_url=os.environ["S3_ENDPOINT_URL"], s3_bucket=os.environ["S3_BUCKET"], + documentation_origin_template=documentation_origin, ) diff --git a/src/inkcre_extension_registry/service/templates/detail.html b/src/inkcre_extension_registry/service/templates/detail.html index bdbf7f4..68ce83f 100644 --- a/src/inkcre_extension_registry/service/templates/detail.html +++ b/src/inkcre_extension_registry/service/templates/detail.html @@ -51,6 +51,21 @@

Web

Manifest ↗ {% endif %} + {% if documentation and documentation.sets %} +
+

Documentation

+

Guides published for this exact Extension release.

+ +
+ {% endif %}