Local CDN capability module

MPL-2.0 14 downloads
Updated 14 hours ago Repository
cdn

Run

wippy run wippy/cdn

Wippy CDN core

The public release of wippy/cdn is version 0.0.4. It requires wippy/facade >=0.6.45. Facade 0.6.45 selects Host 1.0.64 (npm 0.0.64) by default.

The shared core stores and serves validated revisions for the Iconify and font adapters. It owns saved source preferences and discovers installed CDN adapters. It contributes selected settings through Facade's generic configuration hook. The modules preserve online defaults and do not create an application route or navigation item when installed.

Host bindings

Install wippy/cdn 0.0.4, wippy/cdn-iconify 0.0.4 and wippy/cdn-fonts 0.0.3.

Bind the core database requirement database to app:db. The current implementation and its recovery queries support SQLite only. PostgreSQL support is unverified. Bind data_directory to one persistent, writable application-owned filesystem volume outside artifact-managed output. Keep the database and volume stable across restarts. The database stores committed content bytes, manifests, settings, and selections. The volume keeps manual source files and reconstructible serving data.

The process_host requirement defaults to app:processes. Each adapter's server requirement defaults to app:gateway. Bind management_router to the host's existing token-authenticated API router, normally app:api. Bind management_security_scope to an authorized host group, normally app.security:admin. The host must apply endpoint_firewall(access) to the full management scope, with deny rules taking precedence. The modules use the existing server; they do not add a listener.

Every management request must keep the caller actor and request scope. The scope must include the exact wippy.cdn:management_access marker policy. This module check is an additional check and does not replace host token authentication or endpoint_firewall(access). Requests without an actor receive 401. Requests without the marker, or with a denied action, receive 403.

Inspector shell

The core module builds a lazy web component named wippy-cdn-admin. It is announced for explicit loading and does not register globally. Its static files are served from /cdn/admin through the module-owned admin_server requirement, which defaults to app:gateway. The host loads the component with loadByTagName("wippy-cdn-admin") when the Inspector route opens. Build the bundle with make build-wc-admin or the checked-in make.bat build-wc-admin wrapper.

Installed module descriptors may include settings_ui: { schema_version: 1, tag_name: "..." }. The tag is validated as a custom-element name. Descriptors without valid panel metadata remain installed; the management discovery response reports bounded diagnostics and omits only the unavailable panel metadata.

The core shell owns shared CDN settings. Module-specific controls load only the selected installed module panel. Core stores source choices in the existing database and protects them through its existing management router and authorization group. Applications need no extra Facade CDN bindings, source contract or resolver shim. The standard wippy-cdn-admin element uses the AppConfig proxy and needs no API-base or public-base attributes. The Core source endpoint defaults to /api/v1/cdn-config/sources; source-settings-api remains an optional relative compatibility attribute. The shell compares the public Facade default Iconify provider with the expected source configuration. A saved choice does not reload already open application tabs.

Facade discovers the Core function marked wippy.facade.config_contributor and passes only declared static defaults and application overrides. Core uses installed adapters to compose selected local settings. The Iconify adapter owns its provider URL and default-provider merge. Explicit application Iconify settings remain authoritative. Import-map entries and scopes use local defaults, then static configuration, then application overrides. Facade has no dependency on Core and owns no CDN tables, routes or provider rules. Source preferences retain the existing app_cdn_source_preferences table and survive this ownership change without a data migration.

Persistent files and manual inputs

The configured data root contains these manual inputs:

<cdn-data-root>/
  iconify/library/<prefix>.json
  fonts/library/<family-id>/
    fontset.json
    files/<face-id>.woff2
    notices/license.txt

For an Iconify collection, write .json.part, close it, then publish the complete file as .json with a filesystem rename. The JSON must be a complete supported Iconify collection, and its prefix must match the filename. In-place edits are not the completion protocol.

For a font family, copy every WOFF2 face and the complete OFL-1.1 notice first. Write fontset.json last. The physical input manifest has only schemaVersion, id, metadata, and files at the top level. Each files entry has only path, size, and sha256. List every face as files/.woff2 and the notice as notices/license.txt. The face id is a lowercase letter or digit followed by lowercase letters, digits, or single hyphens; use the same id in the face path. The module maps each physical face to faces/.woff2 in the committed manifest. The committed manifest is internal output, not a manual input file.

The metadata.faces array declares every font file exactly once. Faces use style normal and fontDisplay swap. A static face uses a quoted single weight such as "400" or "700". A variable face uses a quoted range such as "100 900" and an axes.wght object with matching numeric min and max values. unicodeRange is a comma-separated CSS Unicode range declaration. The module validates its syntax, not which characters the font can render. Custom face ids and subsets are allowed when each face has a matching file and valid metadata.

The files inventory must match the files in the family directory: one entry per face and one notice entry, with no extras or duplicate paths. Record each exact byte size and lowercase SHA-256 digest. A distribution supports up to 511 faces plus one notice within the 512-file limit. Each face is limited to 20 MiB and the complete distribution to 100 MiB. The notice can be up to 1 MiB and must be UTF-8 text containing the SIL OPEN FONT LICENSE Version 1.1 marker.

The WOFF2 check validates bounded container and table-directory structure and checks variable-font metadata against the presence of an fvar table. Declared weight ranges must use supported numeric wght bounds. The validator does not decompress font tables to verify numeric axis bounds, static weight or glyph coverage for a declared Unicode range. The module copies input bytes to private staging, validates those copied bytes, and activates only a complete valid revision. Invalid or disappearing input does not replace the last committed revision or delete source files.

Input handling promises validated copied content. It does not promise to detect every link encoding. A link followed by the stock filesystem resource may supply bytes for the same validation process, but a link is never retained as an asset. Managed output uses fresh module-owned paths. The module can reconstruct content from the database if a serving file or index is missing.

Built-in assets

The Iconify adapter includes Tabler from @iconify/json 2.2.460, using the Tabler 3.41.1 collection and its MIT notice.

The font adapter includes Inter Variable and Outfit Variable with weights 100 to 900, plus JetBrains Mono Variable with weights 100 to 800. Each default has normal Latin and Latin-ext WOFF2 faces and an OFL-1.1 notice. Defaults remain available without acquisition. Successful explicit acquisition validates and activates its revision if the reviewed selection snapshot still matches. Installing a module does not change source preferences. The provider acquisition recipe remains the existing two-face variable Latin and Latin-ext recipe; manual families may contain other valid normal faces.

Management API

The host binds the management endpoints to its existing authenticated router. Paths below are relative to management_router.

| Method | Path | Purpose | | --- | --- | --- | | GET | /cdn/catalog/{kind} | List assets and selection tokens | | GET | /cdn/modules | List installed CDN adapters | | GET | /cdn/inputs/{kind} | List manual inputs and validation state | | GET | /cdn/health | Read discovery status | | GET | /cdn/revisions/{kind}/{id} | List retained revisions | | DELETE | /cdn/revisions/{kind}/{id}/{revision} | Delete an inactive revision | | DELETE | /cdn/assets/{kind}/{id} | Delete an eligible non-embedded asset and all its revisions | | GET | /cdn/settings | Read settings | | PATCH | /cdn/settings | Change settings | | PUT | /cdn/settings | Replace all three visible General settings fields | | GET | /cdn/operations/{operation_id} | Read an asynchronous operation | | GET | /cdn/unresolved/{kind}/{id} | Read unresolved icon names | | POST | /cdn/select | Select a revision | | POST | /cdn/rollback | Restore the previous revision | | POST | /cdn/deactivate | Stop serving a selected asset | | POST | /cdn/rescan | Request a manual input rescan | | POST | /cdn/iconify/{id}/import | Import validated SVG sources into a collection | | POST | /cdn/iconify/{id}/acquire | Acquire one exact Iconify package version | | POST | /cdn/fonts/{id}/acquire | Acquire one exact Fontsource Variable package version | | POST | /cdn/iconify/{id}/resolve | Review the latest supported Iconify version | | POST | /cdn/fonts/{id}/resolve | Review the latest supported Fontsource version | | POST | /cdn/fonts/{id}/uploads | Admit a browser font upload | | POST | /cdn/fonts/{id}/uploads/{candidate_id}/files/{index} | Stage one raw file with a request_id query parameter | | POST | /cdn/fonts/{id}/uploads/{candidate_id}/commit | Validate and activate a complete upload | | DELETE | /cdn/fonts/{id}/uploads/{candidate_id} | Cancel an incomplete upload |

Mutations return 202 with an operation_id. Poll GET /cdn/operations/{operation_id} until the operation reaches a terminal state. Catalog and revision listings accept limit and cursor. Use the selection_token from the catalog when an action must apply only to the selection snapshot that was reviewed. The General form uses PUT with all three visible fields. The existing PATCH route remains available for the Icons panel's live setting updates. CDN source choices use a separate complete-form PUT at /cdn-config/sources on the same management router.

DELETE /cdn/revisions/{kind}/{id}/{revision} requires a JSON body with expected_selection_token copied from that asset's catalog selection_token, for example { "expected_selection_token": "" }. The endpoint deletes inactive revisions only, and refuses the active and previous revisions. DELETE /cdn/assets/{kind}/{id} uses the same exact JSON body shape and selection token. Asset deletion is refused while its manual source remains in the library, and embedded assets cannot be deleted. When accepted, the module removes the selection and asynchronously removes every retained revision. Move or remove the manual source first. The operation response includes an operation_id; poll it until completion. Browser caches may keep immutable revision URLs, so reload affected documents after deletion or selection changes. Stable stylesheet URLs revalidate on use; revision-qualified CSS and WOFF2 files are immutable.

Select, rollback, and deactivate requests do not accept expected_selection_token. SVG import and acquisition endpoints have their own request fields. An acquisition request supplies version. To update an existing selection, also supply base_revision and expected_selection_token.

GET /cdn/settings returns a result with exactly schemaVersion set to 1, the boolean fields acquisition_enabled, icon_search_enabled, and local_serving_enabled, and retained_bytes_limit as an integer from 33554432 to 1073741824 inclusive. PATCH /cdn/settings accepts a nonempty subset of the four setting fields, without schemaVersion. It does not accept a generic limits object.

Acquisition reads the exact package metadata and archive from https://registry.npmjs.org, validates the allowed origin and redirects, checks registry SRI, then validates and copies only the files selected by the adapter recipe. It runs no package scripts. Reads, misses, rescans, startup, restart, and panel opening do not contact a provider.

Browser font uploads

The optional Fonts Inspector accepts normal WOFF2 files and an OFL-1.1 notice. File selection does not transfer bytes. Enter a family name and stable ID, review each face's static weight or variable range and Unicode coverage, then choose Upload fonts. The component creates the internal manifest.

All upload routes use the host's protected management router. Start with POST /cdn/fonts/{id}/uploads and JSON containing request_id, family, faces, and notice. Each face has id, weight, unicodeRange, size, sha256, and optional axes.wght bounds. The notice has size and sha256. Existing families also require the current base_revision and expected_selection_token from the catalog.

Send each face, followed by the notice, to POST /cdn/fonts/{id}/uploads/{candidate_id}/files/{index}?request_id= with Content-Type: application/octet-stream. Indices start at one. Commit through POST /cdn/fonts/{id}/uploads/{candidate_id}/commit, or cancel through DELETE /cdn/fonts/{id}/uploads/{candidate_id}; both take a JSON body containing request_id.

Generate and retain a separate UUID before each phase. Reuse it only for an identical retry of that phase. Poll the returned operation_id. A committed start or file operation confirms only that phase. A committed commit_candidate result with a valid revision and active set to true confirms activation. Read the current selection token from the refreshed catalog. Exact terminal replays can return HTTP 200.

Candidates are bound to their font family and browser-upload workflow. Failed validation or selection conflicts preserve the previous active revision. Cancellation removes staged bytes or blocks a late start; it does not undo an activated revision. Embedded families cannot be replaced by browser uploads.

The Inspector preserves operation IDs after uncertain responses. Reopen the pending upload to check its status or cancel it. File contents are not stored in the browser. Reloading an incomplete transfer may require cancellation and file selection again. Browser uploads use private staging rather than the user's manual library.

Limits and current status

Current implementation bounds include a 32 MiB Iconify collection, 100,000 icons and aliases combined, alias depth 32, 128 icon names per request, and search pages up to 100 entries. Font faces are limited to 20 MiB each and a font distribution to 100 MiB, with at most 512 distribution files. Manual library discovery has a 512 MiB byte limit and a 16-level path limit. The scan has separate shared budgets of 10,000 files and 10,000 directories across both library roots, with at most 20,000 child entries examined. The two library roots count toward the directory budget.

The retained_bytes_limit setting controls shared retained content. It counts each unique SHA-256 digest once across committed revisions and staged candidates. Its range is 32 MiB to 1 GiB. It does not limit database file size or total filesystem use. Individual revision deletion protects active and previous revisions. Whole-asset deletion removes all revisions. The revision DELETE endpoint can remove eligible inactive revisions and reclaim content that no longer has a committed or staged reference.

The current implementation prunes terminal operation records after 30 days. This is a routine implementation choice, not an approved numeric retention limit.

Offline use applies only to CDN assets that the host has selected for local serving. It does not mean that the rest of the application works without a network connection.