diff --git a/README.md b/README.md index 0c4171e0..a306df29 100644 --- a/README.md +++ b/README.md @@ -105,13 +105,15 @@ Individual extensions are licensed by their respective authors. ## Manifest API contract -The generated v2 client follows the API's manifest v1 contract: `module`, string licenses, optional `homepage_url`, package authors, and full FOSSBilling/PHP/module requirements. Author/edit/moderator forms preserve these fields; historical revision diffs also accept pre-manifest license objects and `website` fields. +The generated v2 client follows the API's manifest v1 contract: `module`, string licenses, optional `homepage_url`, package authors, and full FOSSBilling/PHP/module requirements. Author/edit/moderator forms preserve these fields. -Package icons are paths relative to the distribution root. For GitHub release assets matching the declared source repository, the site resolves them against the release tag. `FOSSBilling/modules` uses its `Ucfirst(id)/` package directory; other GitHub sources use the repository root. Unsupported sources omit relative icons. Legacy HTTP(S) icons continue to work. +Package preparation is queued. The API returns a private job ID; the release editor shows queued/processing status and saves that reference in browser storage so it can resume after a reload. A completed release joins the local draft and still needs review. Manifest corrections reuse stored source; new submissions and server-side corrections wait for the same job contract with a bounded timeout. -Prepared CDN icons use `https://extensions-assets.fossbilling.net/extensions//icons/.`. These images load directly without the site's image transformer. SVG remains vector; a prepared PNG icon offers its AVIF sibling through ``. Other image URLs use the existing image handling. +Authors upload ready-to-install distribution ZIPs. Icons use `icon.svg`, `icon.png`, `icon.webp` or `icon.avif` at the package root, without a manifest field. The API prepares display images in the public asset bucket during processing, including pending icons at unlisted UUID URLs. Existing HTTP(S) icon URLs remain supported for display. Package downloads and pending previews use permission-checked API routes. -API mutations and site mutations both purge the `extensions` and `developers` tags. The `developers` tag is currently a no-op reserved for future cached surfaces; developer pages are uncached. Deploy this site with the API's manifest migration and cache-tag change before enabling the updated modules publisher. Existing cached pages expire through their cache windows; deployment must coordinate the new contract and tag. Old pending revisions require resubmission using the manifest fields before approval. +Published icon URLs use `https://extensions-assets.fossbilling.net/extensions//icons/.`. The extension UUID is independent of its public identifier. SVG or AVIF/PNG variants share the processed icon's immutable asset UUID, so metadata-only edits retain the same icon URL. + +API mutations and site mutations purge the `extensions` and `developers` cache tags. The `developers` tag is currently a no-op; developer pages are uncached. Deploy and smoke-test the configured, package-enabled API before deploying this site or enabling the modules publisher. The Queue and Images bindings must exist before the API's automatic deployment, even when package mode is disabled. ## Directory Identity diff --git a/openapi/extensions-v2.json b/openapi/extensions-v2.json index cd61364c..b8bc2949 100644 --- a/openapi/extensions-v2.json +++ b/openapi/extensions-v2.json @@ -96,10 +96,6 @@ "minLength": 1, "maxLength": 1000 }, - "icon_url": { - "type": "string", - "maxLength": 2048 - }, "source": { "$ref": "#/components/schemas/Repository" }, @@ -114,6 +110,10 @@ "format": "uri", "description": "Null when a legacy URL exceeds the card length bound" }, + "icon_url": { + "type": "string", + "maxLength": 2048 + }, "id": { "type": "string", "format": "uuid" @@ -271,10 +271,6 @@ "minLength": 1, "maxLength": 1000 }, - "icon_url": { - "type": "string", - "maxLength": 2048 - }, "source": { "$ref": "#/components/schemas/Repository" }, @@ -288,6 +284,10 @@ "maxLength": 2048, "format": "uri", "description": "Null when a legacy URL exceeds the card length bound" + }, + "icon_url": { + "type": "string", + "maxLength": 2048 } }, "required": [ @@ -458,9 +458,6 @@ "type": "string", "minLength": 1 }, - "icon_url": { - "type": "string" - }, "version": { "type": "string", "minLength": 1, @@ -470,6 +467,9 @@ "type": "string", "minLength": 1 }, + "icon_url": { + "type": "string" + }, "id": { "type": "string", "format": "uuid" @@ -537,6 +537,18 @@ "maxLength": 2048, "format": "uri" }, + "is_latest": { + "type": "boolean", + "description": "Explicitly selected latest release; at most one release may be selected" + }, + "artifact_id": { + "type": "string", + "format": "uuid" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, "requires": { "$ref": "#/components/schemas/ManifestRequirements" } @@ -611,12 +623,6 @@ "maxLength": 1000, "pattern": "^[A-Za-z0-9.\\-+_:() |]+$" }, - "icon_url": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^(?!\\/)(?![a-zA-Z][a-zA-Z0-9+.-]*:)(?!.*\\.\\.)[^\\\\]+$" - }, "readme": { "type": "string", "minLength": 1, @@ -713,9 +719,6 @@ "type": "string", "minLength": 1 }, - "icon_url": { - "type": "string" - }, "version": { "type": "string", "minLength": 1, @@ -724,6 +727,9 @@ "download_url": { "type": "string", "minLength": 1 + }, + "icon_url": { + "type": "string" } } } @@ -859,21 +865,6 @@ } ] }, - "icon_url": { - "anyOf": [ - { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^(?!\\/)(?![a-zA-Z][a-zA-Z0-9+.-]*:)(?!.*\\.\\.)[^\\\\]+$" - }, - { - "type": "string", - "maxLength": 2048, - "format": "uri" - } - ] - }, "readme": { "type": "string", "minLength": 1, @@ -892,6 +883,10 @@ "maxLength": 2048, "format": "uri" }, + "icon_url": { + "type": "string", + "maxLength": 2048 + }, "website": { "type": "string", "maxLength": 2048, @@ -966,12 +961,6 @@ "maxLength": 1000, "pattern": "^[A-Za-z0-9.\\-+_:() |]+$" }, - "icon_url": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^(?!\\/)(?![a-zA-Z][a-zA-Z0-9+.-]*:)(?!.*\\.\\.)[^\\\\]+$" - }, "readme": { "type": "string", "minLength": 1, @@ -1081,12 +1070,6 @@ "maxLength": 1000, "pattern": "^[A-Za-z0-9.\\-+_:() |]+$" }, - "icon_url": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^(?!\\/)(?![a-zA-Z][a-zA-Z0-9+.-]*:)(?!.*\\.\\.)[^\\\\]+$" - }, "readme": { "type": "string", "minLength": 1, @@ -1676,12 +1659,6 @@ "maxLength": 1000, "pattern": "^[A-Za-z0-9.\\-+_:() |]+$" }, - "icon_url": { - "type": "string", - "minLength": 1, - "maxLength": 2048, - "pattern": "^(?!\\/)(?![a-zA-Z][a-zA-Z0-9+.-]*:)(?!.*\\.\\.)[^\\\\]+$" - }, "readme": { "type": "string", "minLength": 1, @@ -1818,6 +1795,185 @@ "required": ["developer_id"], "additionalProperties": false }, + "PackageJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "extension_identifier": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["queued", "processing", "ready", "failed"] + }, + "artifact": { + "anyOf": [ + { + "$ref": "#/components/schemas/PackageArtifact" + }, + { + "type": "null" + } + ] + }, + "error": { + "type": ["object", "null"], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": ["code", "message", "status"] + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "extension_identifier", + "status", + "artifact", + "error", + "created_at", + "updated_at" + ] + }, + "PackageArtifact": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "source_id": { + "type": "string", + "format": "uuid" + }, + "manifest": { + "$ref": "#/components/schemas/Manifest" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "download_url": { + "type": "string", + "format": "uri" + }, + "published_at": { + "type": ["string", "null"] + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "source_id", + "manifest", + "sha256", + "download_url", + "published_at", + "created_at" + ] + }, + "Manifest": { + "type": "object", + "properties": { + "manifest_version": { + "type": "number", + "enum": [1] + }, + "type": { + "type": "string", + "enum": [ + "module", + "theme", + "payment-gateway", + "server-manager", + "domain-registrar", + "translation" + ] + }, + "id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9_-]+$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "version": { + "type": "string", + "maxLength": 100, + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": ["name"], + "additionalProperties": false + }, + "license": { + "type": "string", + "minLength": 1, + "maxLength": 1000, + "pattern": "^[A-Za-z0-9.\\-+_:() |]+$" + }, + "homepage_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "requires": { + "$ref": "#/components/schemas/ManifestRequirements" + } + }, + "required": [ + "manifest_version", + "type", + "id", + "name", + "version", + "description", + "author", + "license", + "requires" + ], + "additionalProperties": false + }, "OwnedDeveloperProfile": { "anyOf": [ { @@ -5674,6 +5830,740 @@ } } }, + "/packages": { + "post": { + "tags": ["Packages"], + "summary": "Queue package preparation from a distribution ZIP or stored source", + "security": [ + { + "Bearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "manifest": { + "type": "string", + "minLength": 1, + "maxLength": 65536, + "description": "JSON manifest v1" + }, + "source": { + "type": "string", + "format": "binary" + }, + "source_id": { + "type": "string", + "format": "uuid" + } + }, + "required": ["manifest"], + "additionalProperties": false + } + } + } + }, + "responses": { + "202": { + "description": "Private processing job; identical requests reuse queued or completed work", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/PackageJob" + } + }, + "required": ["result"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Package not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Source version conflict, ownership changed, or storage quota exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "Upload exceeds size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Invalid manifest or source ZIP", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Write allowance exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Package storage is not configured or is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/package-jobs/{jobId}": { + "get": { + "tags": ["Packages"], + "summary": "Read an owning developer's private package-processing job", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "jobId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Package processing state and completed artifact", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/PackageJob" + } + }, + "required": ["result"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Package not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Source version conflict, ownership changed, or storage quota exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "Upload exceeds size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Invalid manifest or source ZIP", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Write allowance exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Package storage is not configured or is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/packages/cleanup": { + "post": { + "tags": ["Packages"], + "summary": "Reclaim abandoned local package objects older than one day", + "security": [ + { + "Bearer": [] + } + ], + "responses": { + "200": { + "description": "Cleanup completed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "deleted": { + "type": "number" + } + }, + "required": ["deleted"] + } + }, + "required": ["result"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Package not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Source version conflict, ownership changed, or storage quota exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "Upload exceeds size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Invalid manifest or source ZIP", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Write allowance exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Package storage is not configured or is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/packages/{packageId}": { + "get": { + "tags": ["Packages"], + "summary": "Read an approved package or an authorized private preview", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "packageId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Package manifest and checksum", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/PackageArtifact" + } + }, + "required": ["result"] + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Package not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Source version conflict, ownership changed, or storage quota exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "Upload exceeds size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Invalid manifest or source ZIP", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Write allowance exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Package storage is not configured or is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/packages/{packageId}/download": { + "get": { + "tags": ["Packages"], + "summary": "Download an approved package, or preview as owner/moderator", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "packageId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Verified immutable ZIP", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Package not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Source version conflict, ownership changed, or storage quota exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "Upload exceeds size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Invalid manifest or source ZIP", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Write allowance exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Package storage is not configured or is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/packages/{packageId}/icon": { + "get": { + "tags": ["Packages"], + "summary": "Read an approved package or an authorized private preview", + "security": [ + { + "Bearer": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "packageId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Sanitized SVG, optimized AVIF/PNG, or generic fallback", + "content": { + "image/svg+xml": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "image/avif": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "401": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "The bearer is valid but the account is inactive", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Package not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Source version conflict, ownership changed, or storage quota exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "413": { + "description": "Upload exceeds size limit", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Invalid manifest or source ZIP", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "429": { + "description": "Write allowance exhausted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Package storage is not configured or is unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/developers": { "get": { "tags": ["Developers"], diff --git a/src/components/ExtensionForm.astro b/src/components/ExtensionForm.astro index 2c3bb2b2..b39d0000 100644 --- a/src/components/ExtensionForm.astro +++ b/src/components/ExtensionForm.astro @@ -46,11 +46,6 @@ const isEdit = Boolean(extension); // Redisplay values: a failed submission's echo wins over the extension being // edited, so a rejected edit shows what the user typed. const shown = echo ?? extension; -// A release is required, same as a brand new extension, whenever there's -// nothing already published to carry through — an extension can reach this -// form with no releases yet (rejected and never resubmitted). -const requireRelease = !isEdit || extension!.releases.length === 0; - const existingSpdxId = shown?.license; // An existing license only lands in the custom bucket when it truly isn't // one of the ids this form offers — covers both genuinely custom/proprietary @@ -96,7 +91,7 @@ const otherSpdxIds = SPDX_LICENSE_IDS.filter((id) => !popularSpdxIds.has(id));
-
+
Publisher @@ -202,22 +197,6 @@ const otherSpdxIds = SPDX_LICENSE_IDS.filter((id) => !popularSpdxIds.has(id)); />
-
- -
- -
-
{correctionNote !== undefined && ( @@ -640,3 +628,15 @@ const otherSpdxIds = SPDX_LICENSE_IDS.filter((id) => !popularSpdxIds.has(id)); customInput.required = isOther; }); + + diff --git a/src/components/ExtensionHeader.astro b/src/components/ExtensionHeader.astro index 903f6127..b8a2d017 100644 --- a/src/components/ExtensionHeader.astro +++ b/src/components/ExtensionHeader.astro @@ -6,6 +6,7 @@ import { ArrowLeft, Download, Package, User } from '@lucide/astro'; import { getSignedImageUrl } from '@/lib/signed-image-url'; import { directoryAssetAvifUrl } from '@/lib/image-url'; import { extensionIconUrl } from '@/lib/extension-icon'; +import { releaseDownloadHref } from '@/lib/extension-package'; import { isSafeHttpUrl } from '@/lib/safe-url'; interface Props { id: string; @@ -19,23 +20,20 @@ const { id, content, developer, - versionLabel = 'Latest Version', + versionLabel = 'Latest Release', backHref = '/', backLabel = 'Extensions', } = Astro.props; const latest = latestRelease(content.releases ?? []); const version = latest?.tag ?? content.version; const iconUrl = await getSignedImageUrl( - extensionIconUrl({ - ...content, - identifier: id, - source: content.source ?? { type: 'custom', repo: '' }, - }), + extensionIconUrl(content), 'icon', Astro.locals.env.sessionSecret, ); -const downloadUrl = latest?.download_url ?? content.download_url; -const avifIconUrl = directoryAssetAvifUrl(iconUrl); +const downloadUrl = latest + ? new URL(releaseDownloadHref(latest), Astro.url).href + : content.download_url; ---
- {iconUrl ? ( - - {avifIconUrl && } - - - ) : ( + + + + +
-
diff --git a/src/components/ExtensionReleases.astro b/src/components/ExtensionReleases.astro index 560a164d..506e531a 100644 --- a/src/components/ExtensionReleases.astro +++ b/src/components/ExtensionReleases.astro @@ -1,7 +1,10 @@ --- +import { Copy } from '@lucide/astro'; +import ReleaseChecksum from './ReleaseChecksum.astro'; import { compatibilityLabel } from '@/lib/requirements'; import type { Release } from '@/types'; import { latestRelease, releasesDescending } from '@/lib/releases'; +import { releaseDownloadHref } from '@/lib/extension-package'; import { isSafeHttpUrl } from '@/lib/safe-url'; interface Props { releases: Release[]; @@ -13,13 +16,18 @@ const { releases, editable, disabledReason, showActions = true } = Astro.props; const latest = latestRelease(releases); --- +{editable && ( + +)}