diff --git a/API_SUPPORT.json b/API_SUPPORT.json index 10678c1..1fd1313 100644 --- a/API_SUPPORT.json +++ b/API_SUPPORT.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sdkVersion": "1.0.0", + "sdkVersion": "1.1.0", "releaseStatus": "stable", "pluginPeerRange": ">=1.0.0 <2.0.0", "policy": "docs/SUPPORT_POLICY.md", @@ -399,6 +399,34 @@ ], "declarationsSha256": "40b8a3d52a39b6654336636ffa4367f5ccf26c0fa273f91cf2a852dc71d1c05b" }, + { + "entry": "./bundles", + "stability": "stable", + "availability": "optional", + "environments": [ + "node" + ], + "runtimeNames": [ + "BUNDLE_LIMITS", + "createPluginBundle", + "installPluginBundle", + "lockBundleDependency", + "verifyPluginBundle" + ], + "declarationNames": [ + "BUNDLE_LIMITS", + "BundleDependencyInput", + "BundleFile", + "BundleReceipt", + "LockedBundleDependency", + "PluginBundleInput", + "createPluginBundle", + "installPluginBundle", + "lockBundleDependency", + "verifyPluginBundle" + ], + "declarationsSha256": "6570662411ddce3aa530d826e17afaa64beb5f4a40880baf6dc596926b1548e8" + }, { "entry": "./conformance", "stability": "stable", @@ -907,6 +935,34 @@ ], "declarationsSha256": "aa142b1ad12968a67e8bca9881c919841a11b2d9e47bab29b323156eb2511cbf" }, + { + "entry": "./provenance", + "stability": "stable", + "availability": "optional", + "environments": [ + "node" + ], + "runtimeNames": [ + "canonicalProvenancePayload", + "createProvenanceStatement", + "signPluginProvenance", + "verifyPluginProvenance" + ], + "declarationNames": [ + "ProvenanceEnvelope", + "ProvenanceKey", + "ProvenancePolicy", + "ProvenanceReceipt", + "ProvenanceStatement", + "ProvenanceSubject", + "ProvenanceTrustRoot", + "canonicalProvenancePayload", + "createProvenanceStatement", + "signPluginProvenance", + "verifyPluginProvenance" + ], + "declarationsSha256": "92bc8a92565faf44935e0d85c49c602c7c9555e7384c7b0c0a6881cd7f9a138c" + }, { "entry": "./publishing", "stability": "stable", @@ -915,6 +971,7 @@ "node" ], "runtimeNames": [ + "inspectPluginArchiveBytes", "packPlugin", "validatePluginPackage", "verifyPluginArchive" @@ -924,11 +981,12 @@ "PackageReport", "PackedPlugin", "VerifiedPluginArchive", + "inspectPluginArchiveBytes", "packPlugin", "validatePluginPackage", "verifyPluginArchive" ], - "declarationsSha256": "0fbeb14838e64dd879e77c169d8db6f61222b4e6f62b04073089b6aceae0e7c5" + "declarationsSha256": "b75f8c11539663dee64f16066feba2e19cc9565e8ac1f2c4ada593c3cdafaaf9" }, { "entry": "./schemas", @@ -944,7 +1002,7 @@ "declarationNames": [ "SCHEMAS" ], - "declarationsSha256": "a25b1a2abe5e8f398cb39f954cedc77927e8a77a035dbafc570818f42ef1be97" + "declarationsSha256": "259961a4a30f11c661bafd71e3e8314012c13d8a179294af09558df5606e580a" }, { "entry": "./secrets", @@ -1138,6 +1196,54 @@ ], "declarationsSha256": "a501435b8d1b356ee7ec29e145956a90714a442eb9d5f05844276d7264c406a5" }, + { + "entry": "./updates", + "stability": "stable", + "availability": "optional", + "environments": [ + "node" + ], + "runtimeNames": [ + "createMemoryUpdateJournal", + "createPluginUpdateController", + "createPluginUpdatePlan", + "parsePluginUpdatePlan", + "reconcilePluginUpdateJournal" + ], + "declarationNames": [ + "PluginUpdateController", + "PluginUpdateJournal", + "PluginUpdatePlan", + "PluginUpdateRecord", + "UpdateArtifact", + "UpdateDescriptor", + "UpdateHost", + "UpdateMigration", + "createMemoryUpdateJournal", + "createPluginUpdateController", + "createPluginUpdatePlan", + "parsePluginUpdatePlan", + "reconcilePluginUpdateJournal" + ], + "declarationsSha256": "31b45e47fec6ee23f64762b1fe795dbcd684a8beb0b21d4bd3482caf0d9749be" + }, + { + "entry": "./updates-node", + "stability": "stable", + "availability": "optional", + "environments": [ + "node" + ], + "runtimeNames": [ + "createNodeUpdateJournal", + "inspectNodeUpdateJournal" + ], + "declarationNames": [ + "createNodeUpdateJournal", + "inspectNodeUpdateJournal" + ], + "declarationsSha256": "e06d7a3e787f5720687db685facebef42ae490a4ee83e8183c4897294386b7da" + }, { "entry": "./uploads", "stability": "stable", @@ -1236,6 +1342,32 @@ ], "declarationsSha256": "a04f718b4464f94d15742b7a7b05c759d0fda551224c847c955d3539d0061d13" }, + { + "entry": "./upstream", + "stability": "stable", + "availability": "optional", + "environments": [ + "node" + ], + "runtimeNames": [ + "createUpstreamCandidate", + "createUpstreamManifest", + "fetchUpstreamArtifact", + "inspectUpstreamManifest" + ], + "declarationNames": [ + "UpstreamCandidate", + "UpstreamFetch", + "UpstreamManifest", + "UpstreamSnapshot", + "UpstreamSource", + "createUpstreamCandidate", + "createUpstreamManifest", + "fetchUpstreamArtifact", + "inspectUpstreamManifest" + ], + "declarationsSha256": "7027bd158d8e2e105f3999f1137930468dde63d729c999eb6712c2bd69bc40fe" + }, { "entry": "./workspace-client", "stability": "stable", diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b1a83..8fab792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 1.1.0 — 2026-10-05 + +- Add exact dependency tree locks, deterministic offline bundles, LICENSE/NOTICE inventories and CycloneDX 1.6 SBOMs. Install only into a new directory, without scripts, downloads or permission inheritance. +- Add detached Ed25519 provenance using operator-owned keys, publisher/subject binding, explicit expiry/revocation freshness and unsigned compatibility policy. +- Add digest-bound update plans, exact grant/key approval, active-job version leases, settings conflict checks, separate candidate preparation and explicit rollback. +- Add bounded Node CAS journals with restart inspection and explicit recovery after uncertain activation; external side effects are not rolled back. +- Add explicit allowlisted upstream fetches, API/license/file comparisons and human-review candidate data; no automatic publication or monitoring. +- Extend the existing archive verifier with bounded in-memory inspection. Existing unsigned archive and 1.0 APIs remain compatible. +- Include independent installed examples, declarations and 10 additional schemas. Security audit and vulnerability validation remain deferred. + ## 1.0.0 — 2026-10-05 - Publish the RC API as the stable 1.x line with an exact SDK archive and new plugin diff --git a/PUBLIC_SURFACE.json b/PUBLIC_SURFACE.json index a8434cd..a95c79b 100644 --- a/PUBLIC_SURFACE.json +++ b/PUBLIC_SURFACE.json @@ -3,10 +3,14 @@ "description": "Explicitly reviewed files for the public DDS Plugin SDK package. Changes require publication and rights review.", "packageFiles": [ "API_SUPPORT.json", - "bin/dds-plugin.mjs", "CHANGELOG.md", "CONTRIBUTING.md", - "docs/api.md", + "LICENSE", + "NOTICE", + "PUBLIC_SURFACE.json", + "README.md", + "SECURITY.md", + "bin/dds-plugin.mjs", "docs/ARTIFACT_STORAGE.md", "docs/BINARY_ARTIFACTS.md", "docs/BROWSER_ARTIFACTS.md", @@ -18,37 +22,39 @@ "docs/DEVTOOLS.md", "docs/DIAGNOSTICS.md", "docs/FILE_REVISIONS.md", - "docs/host-contract.md", + "docs/JOBS.md", "docs/JOB_HISTORY.md", "docs/JOB_STORAGE.md", - "docs/JOBS.md", + "docs/LANGUAGE.md", "docs/LANGUAGE_ASSISTANCE.md", "docs/LANGUAGE_DISPLAY.md", "docs/LANGUAGE_EDITING.md", - "docs/LANGUAGE.md", "docs/LICENSING.md", "docs/LOCALIZATION.md", "docs/OBSERVATION.md", - "docs/performance.md", + "docs/PROJECTS.md", "docs/PROJECT_QUERIES.md", "docs/PROJECT_TOOLS.md", "docs/PROJECT_WATCH.md", - "docs/PROJECTS.md", "docs/PUBLIC_SCOPE.md", "docs/PUBLISHING.md", "docs/RC_ACCEPTANCE.md", "docs/RELEASE_1_0.md", + "docs/RELEASE_TOOLS.md", "docs/SECRETS.md", "docs/SETTINGS.md", "docs/SUPPORT_POLICY.md", - "docs/testing.md", "docs/THEMES.md", "docs/TRANSFER_QUEUE.md", "docs/UPLOADS.md", "docs/VERIFYING.md", + "docs/WORKSPACES.md", "docs/WORKSPACE_EDITS.md", + "docs/api.md", + "docs/host-contract.md", + "docs/performance.md", + "docs/testing.md", "docs/workspace-protocol.md", - "docs/WORKSPACES.md", "examples/binary-artifacts/run.mjs", "examples/browser-artifacts/browser.html", "examples/browser-artifacts/browser.mjs", @@ -63,10 +69,10 @@ "examples/browser-resume/worker.mjs", "examples/combined-workload/run.mjs", "examples/command-jobs/run.mjs", + "examples/configuration/LICENSE", "examples/configuration/browser.html", "examples/configuration/browser.mjs", "examples/configuration/catalog.mjs", - "examples/configuration/LICENSE", "examples/configuration/plugin.mjs", "examples/configuration/run.mjs", "examples/configuration/serve.mjs", @@ -75,8 +81,8 @@ "examples/conformance/browser.mjs", "examples/conformance/run.mjs", "examples/conformance/serve.mjs", - "examples/consent/prompt.mjs", "examples/consent/README.md", + "examples/consent/prompt.mjs", "examples/development-tools/run.mjs", "examples/durable-jobs/run.mjs", "examples/hello-diagnostics/plugin.mjs", @@ -91,9 +97,9 @@ "examples/language-assistance/LICENSE", "examples/language-assistance/plugin.mjs", "examples/language-assistance/run.mjs", + "examples/language-display/LICENSE", "examples/language-display/browser.html", "examples/language-display/browser.mjs", - "examples/language-display/LICENSE", "examples/language-display/plugin.mjs", "examples/language-display/run.mjs", "examples/language-display/serve.mjs", @@ -107,13 +113,15 @@ "examples/project-tools/run.mjs", "examples/project-tools/serve.mjs", "examples/project-watch/run.mjs", - "examples/publishable-plugin/dds-package.json", - "examples/publishable-plugin/disclosure.json", "examples/publishable-plugin/LICENSE", "examples/publishable-plugin/NOTICE", + "examples/publishable-plugin/README.md", + "examples/publishable-plugin/dds-package.json", + "examples/publishable-plugin/disclosure.json", "examples/publishable-plugin/plugin.json", "examples/publishable-plugin/plugin.mjs", - "examples/publishable-plugin/README.md", + "examples/release-tools/fixture.mjs", + "examples/release-tools/run.mjs", "examples/release-verification/README.md", "examples/release-verification/run.mjs", "examples/stored-artifacts/run.mjs", @@ -126,23 +134,19 @@ "examples/uploads/browser.mjs", "examples/uploads/run.mjs", "examples/uploads/serve.mjs", - "examples/user-workspace/client.mjs", "examples/user-workspace/LICENSE", + "examples/user-workspace/client.mjs", "examples/user-workspace/plugin.mjs", "examples/user-workspace/private-manifest.json", "examples/user-workspace/project/design.sv", "examples/user-workspace/server.mjs", "examples/user-workspace/tool.mjs", - "examples/workspace-edits/child.mjs", "examples/workspace-edits/LICENSE", + "examples/workspace-edits/child.mjs", "examples/workspace-edits/plugin.mjs", "examples/workspace-edits/run.mjs", "examples/workspace-observation/run.mjs", - "LICENSE", - "NOTICE", "package.json", - "PUBLIC_SURFACE.json", - "README.md", "schemas/artifact-sink-capabilities.schema.json", "schemas/artifact-storage-capabilities.schema.json", "schemas/artifact-transfer-receipt.schema.json", @@ -152,6 +156,7 @@ "schemas/binary-artifact-reference.schema.json", "schemas/binary-artifact.schema.json", "schemas/browser-artifact-checkpoint.schema.json", + "schemas/bundle-dependency.schema.json", "schemas/code-action.schema.json", "schemas/command.schema.json", "schemas/completion-item.schema.json", @@ -188,7 +193,10 @@ "schemas/localized-message.schema.json", "schemas/manifest.schema.json", "schemas/memory-job-snapshot.schema.json", + "schemas/plugin-bundle.schema.json", "schemas/plugin-plan.schema.json", + "schemas/plugin-update-plan.schema.json", + "schemas/plugin-update-record.schema.json", "schemas/project-entry.schema.json", "schemas/project-query-capabilities.schema.json", "schemas/project-query-options.schema.json", @@ -197,6 +205,9 @@ "schemas/project-watch-capabilities.schema.json", "schemas/project-watch-event.schema.json", "schemas/project-watch-options.schema.json", + "schemas/provenance-envelope.schema.json", + "schemas/provenance-receipt.schema.json", + "schemas/provenance-statement.schema.json", "schemas/secret-reference.schema.json", "schemas/secret-scope.schema.json", "schemas/semantic-legend.schema.json", @@ -220,6 +231,9 @@ "schemas/upload-reference.schema.json", "schemas/upload-spec.schema.json", "schemas/upload-status.schema.json", + "schemas/upstream-candidate.schema.json", + "schemas/upstream-manifest.schema.json", + "schemas/upstream-snapshot.schema.json", "schemas/workspace-conditional-file.schema.json", "schemas/workspace-edit-preview.schema.json", "schemas/workspace-edit-receipt.schema.json", @@ -229,7 +243,6 @@ "schemas/workspace-file-revision.schema.json", "schemas/workspace-project-capabilities.schema.json", "schemas/workspace-project-poll.schema.json", - "SECURITY.md", "src/accessibility.d.mts", "src/accessibility.mjs", "src/artifact-download-node.d.mts", @@ -249,6 +262,8 @@ "src/artifact-transfer.mjs", "src/artifacts.d.mts", "src/artifacts.mjs", + "src/bundles.d.mts", + "src/bundles.mjs", "src/configuration-operations.mjs", "src/configuration-values.mjs", "src/conformance-catalog.mjs", @@ -321,9 +336,13 @@ "src/project-watch-schemas.mjs", "src/project-watch.d.mts", "src/project-watch.mjs", + "src/provenance.d.mts", + "src/provenance.mjs", "src/publication-policy.mjs", "src/publishing.d.mts", "src/publishing.mjs", + "src/release-internals.mjs", + "src/release-schemas.mjs", "src/schemas.d.mts", "src/schemas.mjs", "src/secrets.d.mts", @@ -350,6 +369,10 @@ "src/transfer-queue.d.mts", "src/transfer-queue.mjs", "src/transfer-transient.mjs", + "src/updates-node.d.mts", + "src/updates-node.mjs", + "src/updates.d.mts", + "src/updates.mjs", "src/upload-files-node.mjs", "src/upload-schemas.mjs", "src/upload-source-node.mjs", @@ -360,6 +383,8 @@ "src/uploads-node.mjs", "src/uploads.d.mts", "src/uploads.mjs", + "src/upstream.d.mts", + "src/upstream.mjs", "src/version.mjs", "src/workspace-client.d.mts", "src/workspace-client.mjs", diff --git a/README.md b/README.md index e54e4ae..80ae970 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,12 @@ a small ESM package, TypeScript declarations and versioned contracts. Connect your own workspace and tools with the Node.js host and HTTP client. The SDK has **zero runtime dependencies**. -Version **1.0.0** is the stable API release. The +Version **1.1.0** adds [offline bundles, detached provenance, reviewed updates and upstream candidates](docs/RELEASE_TOOLS.md). +Run `npm run example:release-tools` for a complete independent installation, signature, +approval, active-job version switch, rollback and durable readback example. +These Node-only tools use operator-owned trust and explicit execution grants. + +Version 1.0 established the stable API release. The [API inventory](API_SUPPORT.json), [support and migration policy](docs/SUPPORT_POLICY.md) and [host conformance suite](docs/CONFORMANCE.md) make its scope reviewable. Run `npm run example:conformance`, `npm run example:conformance-browser` or @@ -57,7 +62,7 @@ application, private engines/services and production signing systems are outside this repository. Read the [public scope](docs/PUBLIC_SCOPE.md) and [compatibility table](docs/COMPATIBILITY.md) for exact boundaries. -Version **1.0.0** supports Node 22 and 24. Manifest v1 diagnostic plugins remain +Version **1.1.0** supports Node 22 and 24. Manifest v1 diagnostic plugins remain compatible. Manifest v2 adds commands, permission-scoped workspace and backend APIs, and open-source or proprietary distribution metadata. Themes export to the XML format already supported by DDS. The workspace host runs trusted plugins @@ -117,7 +122,7 @@ mkdir my-dds-plugin cd my-dds-plugin npm init -y npm pkg set type=module -npm install --ignore-scripts --save-exact https://github.com/Altifigence/dds-plugin-sdk/releases/download/v1.0.0/altifigence-dds-plugin-sdk-1.0.0.tgz +npm install --ignore-scripts --save-exact https://github.com/Altifigence/dds-plugin-sdk/releases/download/v1.1.0/altifigence-dds-plugin-sdk-1.1.0.tgz ``` Copy the example's `plugin.mjs` and `run.mjs` into this directory, then run: diff --git a/SECURITY.md b/SECURITY.md index d18ab49..f2beefc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security -The current stable API release is **1.0.0**. It retains the security fixes from +The current stable API release is **1.1.0**. It retains the security fixes from 0.3.2. Update workspace servers and clients together, using a current patched Node 22 or 24; do not use a pre-0.3.2 workspace host. Report an SDK @@ -59,7 +59,7 @@ protect storage, verify the current identity and implement revocation themselves ## 한국어 -현재 안정 API 릴리스는 **1.0.0**이며 0.3.2의 보안 수정을 유지합니다. workspace **서버와 클라이언트를 모두 +현재 안정 API 릴리스는 **1.1.0**이며 0.3.2의 보안 수정을 유지합니다. workspace **서버와 클라이언트를 모두 0.3.2 이상**으로 갱신하고 Node 22/24의 최신 보안 패치를 사용하세요. 클라이언트만 갱신하면 오래된 서버의 파일 검사와 요청 수신 제한은 바뀌지 않습니다. diff --git a/docs/RELEASE_TOOLS.md b/docs/RELEASE_TOOLS.md new file mode 100644 index 0000000..afb14de --- /dev/null +++ b/docs/RELEASE_TOOLS.md @@ -0,0 +1,160 @@ +# Bundles, provenance and reviewed updates + +SDK 1.1 adds five Node-only entry points: `/bundles`, `/provenance`, `/updates`, +`/updates-node` and `/upstream`. They are optional tools for a user-owned host. +They do not operate a registry, schedule checks, publish releases or grant plugin +permissions. Existing unsigned DDS `.tgz` archives still use `/publishing`. + +Run the complete working example after installing the SDK: + +```sh +node node_modules/@altifigence/dds-plugin-sdk/examples/release-tools/run.mjs +``` + +The example creates disposable files, an ephemeral signing key in memory and a +loopback HTTP fixture. It installs and executes an SDK-authored plugin with a +real dependency, approves an update while an old command runs, rolls back, and +reads its durable journal. It never contacts an external registry. + +## Deterministic offline bundles + +`lockBundleDependency` accepts a reviewed file tree, exact version, HTTPS source, +entry point, module type, exact dependency map, license/NOTICE paths and an +explicit redistribution decision. Files use canonical base64. The dependency +digest covers all these values and file contents under DDS canonical JSON v1. +It is a digest of the reviewed tree, not a claim that an upstream tarball has the +same hash. Keep the original upstream archive digest in your review evidence. + +`createPluginBundle({plugin, dependencies, packages})` embeds a DDS archive, +its release metadata and expected hash, plus the locked dependency graph and a +deterministic CycloneDX 1.6 SBOM. Exact package versions are global within a +bundle; cycles, conflicting versions, missing packages and unreachable packages +are rejected. This format supports reviewed ESM/CommonJS trees. Native addons, +postinstall downloads, arbitrary npm metadata, dependency export maps and +unreviewed npm dependency resolution are outside this format. + +`verifyPluginBundle(bytes, {expectedSha256})` checks a bounded byte snapshot and +returns inventory, source, licenses, SBOM and digests. Limits are 32 dependencies, +1,024 expanded files, 4 MiB per dependency file, 32 MiB expanded data and 48 MiB +encoded bundle. The existing root archive verifier has its own smaller limits. +Paths, case collisions, regular file types and file/directory conflicts are +checked before installation. The bundle is canonical UTF-8 JSON: sorted object +keys, preserved array order, finite JSON numbers and no duplicate object keys. +Reviewed input bytes are preserved; timestamps, absolute paths and platform +archive metadata are not generated into dependency files. + +```js +import {verifyPluginBundle, installPluginBundle} from '@altifigence/dds-plugin-sdk/bundles'; + +const receipt = verifyPluginBundle(bytes, {expectedSha256: approvedHash}); +await installPluginBundle(bytes, { + destination: newDirectory, + expectedSha256: receipt.sha256, + approved: true, +}); +``` + +Installation requires a new directory under an operator-controlled parent. It +does not invoke npm, run scripts, fetch files or execute the plugin. A failed +installation is reported with the destination and left for inspection. The +owning process must exclude concurrent filesystem writers; this is not an OS +sandbox. The SDK itself remains a separate host dependency. Dependencies gain no +grants from the bundle and receive no additional permissions from the installer. + +## Detached provenance and operator trust + +`createProvenanceStatement` binds a bundle's exact name/version/SHA-256 to its +publisher, source revision, issue time and expiry. `signPluginProvenance` uses a +caller-owned Ed25519 `KeyObject`. No private key is bundled with the SDK. The +signed bytes are `DDS-PROVENANCE-V1\n` followed by canonical statement JSON. +`canonicalProvenancePayload` lets another signer reproduce these bytes. + +`verifyPluginProvenance(bytes, envelope, policy)` takes operator-selected public +keys and publisher bindings, explicit current time, key validity/revocation, +revocation evidence time and maximum age, offline policy and unsigned policy. +Maximum accepted clock skew is five minutes; maximum revocation evidence age is +30 days. The chosen policy can be stricter. Offline verification uses supplied +evidence; it never obtains fresh revocation information itself. Both online and +offline verification reject evidence outside the supplied freshness budget. + +The receipt separates `checksum`, `signature`, `publisher`, `policy` and +`executionAuthorized: false`. A valid signature does not establish code safety, +approval to execute, grants, a sandbox or DDS product signing authority. A new +key becomes trusted only when the operator adds it. Rotation can overlap two +explicit roots; expiry and revocation are checked again before activation. +`envelope: null` requires `allowUnsigned: true` and still reports unsigned and +unverified publisher. It does not manufacture provenance for older releases. + +## Review, approve, activate and recover + +`createPluginUpdatePlan` binds old/new bundle bytes, version, API/capabilities, +grants, dependency sources/digests, declared tools, licenses, settings hashes, +migration identity, signing envelope and key. `parsePluginUpdatePlan` checks its +version and canonical digest. Each descriptor comes from the verified bundle. + +`createPluginUpdateController` owns a currently authorized host, a settings +snapshot and one journal. Its operator-supplied `getPolicy` returns fresh trust +evidence; `prepare` creates a separate candidate host after explicit approval. +The host ports execute trusted code and must enforce their granted authority. + +1. `stage` verifies and snapshots candidate bytes and next settings. +2. Review the returned plan, then `approve` its exact digest, grants and key ID. +3. `activate` rechecks bytes, current settings and trust before and after preparing + the candidate. It records the change with an exact journal revision. +4. `execute` retains the host/settings snapshot it began with and returns the + plugin bundle hash with the result. Under `wait`, active jobs defer the switch; + `retain-old` keeps the old host until its jobs settle. Neither policy silently + cancels jobs. Pass a caller-owned AbortSignal for explicit cancellation. + +Settings replacement invalidates staged approval. Preparation/migration failure +keeps the old host and settings. Concurrent updates fail with a conflict/busy +result. Preparation code can have external effects; neither failed activation +nor rollback undoes external writes, emails, processes or network requests. +The migration's `reversible` flag is review metadata, not an automatic undo +promise. Rollback stages the previous bundle/settings as another update and +requires fresh policy and approval. + +The Node journal lives in a dedicated directory outside the workspace. It has +one writer, append-only digest-checked records, exact revision CAS, file sync and +bounded storage (256 records, 128 KiB per envelope, 8 MiB total). Archive it under +operator control before starting a new journal at the limit. It does not promise +cross-process host/settings/filesystem transactions or power-loss durability on +every platform. A crash in `preparing`, an ambiguous write acknowledgement or an +unreadable tail requires host readback. `inspectNodeUpdateJournal` reports stored +state without replay; `reconcilePluginUpdateJournal` records an explicit, +approved readback of a known old/new digest and settings hash. A corrupt tail +must be inspected and repaired by the operator. `recoverStaleLock: true` only +recovers a same-machine writer that is demonstrably no longer alive. It never +breaks a live or unknown writer lock. + +## Upstream candidates + +`fetchUpstreamArtifact` runs only when called with `enabled: true`, an exact +source/version/digest, exact URL allowlist, offline flag, byte limit and deadline. +Redirect targets need their own allowlist entries. It sends no cookies or auth +headers, performs no implicit retry and returns distinct offline, unavailable, +unverified, digest-mismatch and verified results with observation time. Loopback +HTTP requires a separate explicit fixture option. A changed tag/digest requires +new review; it is not accepted as the old release. + +An adapter can publish a reviewed `upstream-manifest` with API names, exact source +revision, full license/NOTICE and file digests. `inspectUpstreamManifest` binds +that metadata to downloaded bytes. It does not inspect arbitrary upstream code +or infer license permissions. `createUpstreamCandidate` compares API/file/license +changes against an exact supported-version matrix and a developer-owned +functional check. A matrix match without that check remains `unverified`; +missing required APIs are `unsupported`. Even `supported` yields review data and +an expected-before/replacement source patch, with `reviewRequired: true` and no +automatic actions. The host owns test execution and its cancellation budget. + +## Release evidence and limits + +The package includes TypeScript declarations and versioned JSON Schemas for the +new contracts, plus functional tests for the reviewed input and lifecycle +rules. The release runs independent packed consumers and Windows/Linux × Node +22/24 CI. New tools are Node-only; existing browser functionality is unchanged. +Security audit and vulnerability validation remain deferred. Passing these +functional contracts does not complete that separate review. + +References: [Node crypto sign/verify](https://nodejs.org/docs/latest-v22.x/api/crypto.html#cryptosignalgorithm-data-key-callback), +[CycloneDX 1.6 schema](https://cyclonedx.org/schema/bom-1.6.schema.json). diff --git a/docs/SUPPORT_POLICY.md b/docs/SUPPORT_POLICY.md index 97d7b0b..0e52ce5 100644 --- a/docs/SUPPORT_POLICY.md +++ b/docs/SUPPORT_POLICY.md @@ -1,6 +1,6 @@ # SDK 1.x support and migration -`1.0.0` is the stable SDK line. Pin the installed archive; newly packed plugins +`1.1.0` is the current stable SDK release in the 1.x line. Pin the installed archive; newly packed plugins use `>=1.0.0 <2.0.0`. The policy below applies to this line. Security audit and vulnerability validation remain pending under the owner's 2026-10-05 deferral; stable API status does not constitute security or DDS product acceptance. @@ -27,7 +27,7 @@ experimental changes are announced in minor-release notes and require fresh acceptance. Its presence is not a cross-browser compatibility claim. Other browser ports also require their documented capability and permission checks. -No stable API is deprecated in 1.0.0. Future deprecations appear in declarations, +No stable API is deprecated in 1.1.0. Future deprecations appear in declarations, the changelog and migration guide for at least two minor releases and six months before removal in a new major. Runtime execution does not emit unsolicited warning logs or change grants. A security emergency is documented explicitly. diff --git a/docs/api.md b/docs/api.md index 7091424..3271358 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,4 +1,9 @@ -# API reference — 0.13.0 +# API reference — 1.1.0 + +SDK 1.1 adds Node-only [release tools](RELEASE_TOOLS.md): deterministic offline +bundles, detached provenance, reviewed updates and upstream comparison. Existing +1.0 stable APIs remain compatible. The complete export/type list is generated in +[API_SUPPORT.json](../API_SUPPORT.json). SDK 0.13 adds `/testing` scenario/clock/fault/trace helpers, `/diagnostics` local sessions and adapters, and `/devtools` feature templates plus `planPlugin()` and @@ -340,3 +345,7 @@ authorization, with no automatic rollback or retry. See [assistance](LANGUAGE_ASSISTANCE.md), [workspace edits](WORKSPACE_EDITS.md), [formatting/actions](LANGUAGE_EDITING.md) and [language display](LANGUAGE_DISPLAY.md) for complete contracts, limits and independent examples. + +## Release tools (1.1, Node only) + +See [release tools](RELEASE_TOOLS.md) for the complete bundle, Ed25519 provenance, approval/activation journal and upstream candidate APIs. The five entry points provide declarations and versioned schemas without adding SDK runtime dependencies. diff --git a/examples/release-tools/fixture.mjs b/examples/release-tools/fixture.mjs new file mode 100644 index 0000000..d8798db --- /dev/null +++ b/examples/release-tools/fixture.mjs @@ -0,0 +1,31 @@ +import {mkdir,readFile,writeFile} from 'node:fs/promises'; +import path from 'node:path'; +import {pathToFileURL} from 'node:url'; +import {createHash} from 'node:crypto'; +import {definePlugin,createPluginHost} from '@altifigence/dds-plugin-sdk'; +import {packPlugin} from '@altifigence/dds-plugin-sdk/publishing'; +import {createPluginBundle,lockBundleDependency,installPluginBundle,verifyPluginBundle} from '@altifigence/dds-plugin-sdk/bundles'; + +export const sha256=bytes=>createHash('sha256').update(bytes).digest('hex'); +export async function makeExampleBundle(directory,version='1.0.0',offset=1,permissions=[]){ + await mkdir(directory,{recursive:true}); + const license=await readFile(new URL('../../LICENSE',import.meta.url),'utf8'); + const manifest={manifestVersion:2,id:'release-tools-example',name:'Release Tools Example',publisher:'example',version,protocolVersion:1,entry:'./plugin.mjs',runtime:'workspace',capabilities:['commands'],permissions,supportedHosts:['workspace-host'],license:'Apache-2.0',source:{visibility:'open',licenseFile:'LICENSE'}}; + const code=`import {twice} from 'dds-example-math';\nexport function activate(context) { return context.registerCommand({id:'compute',title:'Compute'},async(input,{signal})=>{ if(input.delay) await new Promise(resolve=>setTimeout(resolve,input.delay)); signal.throwIfAborted(); return {value:twice(input.value)+${offset},version:${JSON.stringify(version)}}; }); }\n`; + const disclosure={schemaVersion:1,publisher:manifest.publisher,pluginId:manifest.id,pluginVersion:version,license:manifest.license,sourceVisibility:'open',supportUrl:'https://github.com/Altifigence/dds-plugin-sdk/issues',dataUse:'Only explicit invented example input.',backends:[]}; + const files={'plugin.json':JSON.stringify(manifest),'plugin.mjs':code,'LICENSE':license,'NOTICE':'SDK authored example. No third-party dependency code.','disclosure.json':JSON.stringify(disclosure)}; + await writeFile(path.join(directory,'dds-package.json'),JSON.stringify({schemaVersion:1,files:Object.keys(files)})); + for(const [name,content]of Object.entries(files))await writeFile(path.join(directory,name),content); + const packed=await packPlugin(directory,{out:path.join(directory,'dist')}); + const dependency=lockBundleDependency({name:'dds-example-math',version:'1.0.0',source:'https://github.com/Altifigence/dds-plugin-sdk',entry:'index.mjs',type:'module',dependencies:{},license:'Apache-2.0',licenseFile:'LICENSE',noticeFile:'NOTICE',redistributable:true,files:[{path:'index.mjs',data:Buffer.from('export const twice=value=>value*2;\n').toString('base64')},{path:'LICENSE',data:Buffer.from(license).toString('base64')},{path:'NOTICE',data:Buffer.from('SDK authored arithmetic example.').toString('base64')}]}); + const input={plugin:{archive:(await readFile(packed.archivePath)).toString('base64'),metadata:JSON.parse(await readFile(packed.metadataPath,'utf8')),sha256:packed.artifact.sha256},dependencies:{'dds-example-math':'1.0.0'},packages:[dependency]}; + return {bundle:createPluginBundle(input),input,manifest,packed}; +} +export async function loadExampleHost(bundle,destination,grants=[]){ + const receipt=verifyPluginBundle(bundle);await installPluginBundle(bundle,{destination,expectedSha256:receipt.sha256,approved:true}); + const manifest=JSON.parse(await readFile(path.join(destination,'plugin.json'),'utf8')); + const implementation=await import(pathToFileURL(path.join(destination,'plugin.mjs')).href); + const host=createPluginHost({hostId:'workspace-host',grants}); + try{await host.activate(definePlugin(manifest,implementation.activate));}catch(error){host.dispose();throw error;} + return {execute(input,{signal}={}){return host.executeCommand(manifest.id,'compute',input,{signal});},dispose(){host.dispose();}}; +} diff --git a/examples/release-tools/run.mjs b/examples/release-tools/run.mjs new file mode 100644 index 0000000..06f592c --- /dev/null +++ b/examples/release-tools/run.mjs @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import {mkdtemp,mkdir,realpath,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {generateKeyPairSync} from 'node:crypto'; +import {createServer} from 'node:http'; +import {once} from 'node:events'; +import {verifyPluginBundle,createPluginBundle} from '@altifigence/dds-plugin-sdk/bundles'; +import {createProvenanceStatement,signPluginProvenance,verifyPluginProvenance} from '@altifigence/dds-plugin-sdk/provenance'; +import {createPluginUpdateController} from '@altifigence/dds-plugin-sdk/updates'; +import {createNodeUpdateJournal,inspectNodeUpdateJournal} from '@altifigence/dds-plugin-sdk/updates-node'; +import {createUpstreamManifest,inspectUpstreamManifest,fetchUpstreamArtifact,createUpstreamCandidate} from '@altifigence/dds-plugin-sdk/upstream'; +import {makeExampleBundle,loadExampleHost,sha256} from './fixture.mjs'; + +const root=await realpath(await mkdtemp(path.join(tmpdir(),'dds-release-tools-')));let controller,journal,server; +try{ + const old=await makeExampleBundle(path.join(root,'old')),next=await makeExampleBundle(path.join(root,'next'),'1.1.0',2); + assert.deepEqual(createPluginBundle(old.input),old.bundle); + const receipt=verifyPluginBundle(next.bundle,{expectedSha256:sha256(next.bundle)});assert.equal(receipt.sbom.specVersion,'1.6'); + const {privateKey,publicKey}=generateKeyPairSync('ed25519'),now=Date.now(); + const sign=(bundle,version)=>signPluginProvenance(createProvenanceStatement({subject:{name:'@example/release-tools-example',version,sha256:sha256(bundle)},publisher:'example',source:{url:'https://github.com/Altifigence/dds-plugin-sdk',revision:'sdk-authored-example'},issuedAt:now-1000,expiresAt:now+60000}),{keyId:'ephemeral-example',privateKey}); + const current={bundle:old.bundle,envelope:sign(old.bundle,'1.0.0')},candidate={bundle:next.bundle,envelope:sign(next.bundle,'1.1.0')}; + const policy=(descriptor)=>({subject:{name:descriptor.name,version:descriptor.version,sha256:descriptor.bundleSha256},publisher:'example',trustRoots:[{keyId:'ephemeral-example',publisher:'example',publicKey,validFrom:now-2000,validUntil:now+120000,revokedAt:null}],now:Date.now(),revocations:{observedAt:now,revokedKeyIds:[]},maxRevocationAgeMs:120000,offline:true,allowUnsigned:false}); + const workspace=path.join(root,'workspace');await mkdir(workspace); + journal=await createNodeUpdateJournal({directory:path.join(root,'journal'),workspaceRoot:workspace});let installs=0; + controller=await createPluginUpdateController({initial:current,initialHost:await loadExampleHost(current.bundle,path.join(workspace,'initial')),settings:{revision:1},getPolicy:policy,prepare:(bundle,{grants})=>loadExampleHost(bundle,path.join(workspace,'install-'+(++installs)),grants),journal}); + const oldJob=controller.execute({value:5,delay:80}); + const plan=await controller.stage({candidate,nextSettings:{revision:2},migration:{id:'example-settings-v2',reversible:true},jobPolicy:'retain-old'}); + await assert.rejects(controller.activate({sha256:plan.sha256}),{code:'APPROVAL_REQUIRED'}); + await controller.approve({sha256:plan.sha256,grants:plan.candidate.grants,keyId:plan.candidate.keyId,approved:true}); + await controller.activate({sha256:plan.sha256}); + const latest=await controller.execute({value:5});assert.equal(latest.result.value,12);assert.equal(latest.pluginSha256,sha256(next.bundle)); + const first=await oldJob;assert.equal(first.result.value,11);assert.equal(first.pluginSha256,sha256(old.bundle)); + const rollback=await controller.stage({candidate:current,nextSettings:{revision:1},migration:{id:'restore-example-settings',reversible:true},jobPolicy:'wait'}); + await controller.approve({sha256:rollback.sha256,grants:rollback.candidate.grants,keyId:rollback.candidate.keyId,approved:true}); + const restored=await controller.activate({sha256:rollback.sha256});assert.equal(restored.externalEffectsUndone,false);assert.equal((await controller.execute({value:5})).result.value,11); + await controller.dispose();controller=null;await journal.close();journal=null; + const readback=await inspectNodeUpdateJournal({directory:path.join(root,'journal')});assert.equal(readback.state,'readable');assert.equal(readback.record.activeSha256,sha256(old.bundle)); + const manifest=version=>createUpstreamManifest({name:'example-upstream',version,revision:'fixture-'+version,api:['twice'],license:{name:'Apache-2.0',text:'SDK-authored fixture under Apache-2.0.',notice:'Example only.',redistributable:true},files:[{path:'index.mjs',sha256:sha256(Buffer.from('example '+version))}]}); + const before=Buffer.from(JSON.stringify(manifest('1.0.0'))),after=Buffer.from(JSON.stringify(manifest('1.1.0'))); + server=createServer((req,res)=>{const bytes=req.url==='/old'?before:after;res.writeHead(200,{'Content-Length':bytes.length});res.end(bytes);});server.listen(0,'127.0.0.1');await once(server,'listening'); + const base=`http://127.0.0.1:${server.address().port}`,oldSource={url:base+'/old',version:'1.0.0',sha256:sha256(before)},newSource={url:base+'/new',version:'1.1.0',sha256:sha256(after)}; + const fetched=await fetchUpstreamArtifact({source:newSource,allowedUrls:[newSource.url],enabled:true,offline:false,maxBytes:65536,timeoutMs:2000,allowLoopback:true});assert.equal(fetched.status,'verified'); + const update=await createUpstreamCandidate({current:inspectUpstreamManifest(before,oldSource,now),candidate:inspectUpstreamManifest(fetched.artifact,newSource,fetched.observedAt),support:{versions:['1.1.0'],requiredApi:['twice']},check:()=>({passed:true,detail:'Example arithmetic adapter fixture passed'})});assert.equal(update.compatibility,'supported');assert.equal(update.reviewRequired,true);assert.deepEqual(update.automaticActions,[]); + const signature=verifyPluginProvenance(next.bundle,candidate.envelope,policy(plan.candidate));assert.equal(signature.signature,'verified');assert.equal(signature.executionAuthorized,false); + console.log(JSON.stringify({version:'1.1.0',deterministicBundle:true,offlineInstall:true,sbom:'CycloneDX 1.6',signed:true,oldJobPinned:true,activated:true,rollback:true,journalReadback:true,upstreamCandidate:true,automaticActions:0})); +}finally{ + if(controller)await controller.dispose();if(journal)await journal.close();if(server){server.closeAllConnections();await new Promise(resolve=>server.close(resolve));} + assert.equal(path.dirname(root),await realpath(tmpdir()));assert.ok(path.basename(root).startsWith('dds-release-tools-'));await rm(root,{recursive:true}); +} diff --git a/package-lock.json b/package-lock.json index ca2b050..b3065e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@altifigence/dds-plugin-sdk", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@altifigence/dds-plugin-sdk", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0", "bin": { "dds-plugin": "bin/dds-plugin.mjs" diff --git a/package.json b/package.json index 11d6af7..034c188 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@altifigence/dds-plugin-sdk", - "version": "1.0.0", + "version": "1.1.0", "description": "Plugin, theme and user-owned workspace tools for Digital Design Studio", "type": "module", "license": "Apache-2.0", @@ -164,6 +164,26 @@ "./conformance-node": { "types": "./src/conformance-node.d.mts", "import": "./src/conformance-node.mjs" + }, + "./bundles": { + "types": "./src/bundles.d.mts", + "import": "./src/bundles.mjs" + }, + "./provenance": { + "types": "./src/provenance.d.mts", + "import": "./src/provenance.mjs" + }, + "./updates": { + "types": "./src/updates.d.mts", + "import": "./src/updates.mjs" + }, + "./updates-node": { + "types": "./src/updates-node.d.mts", + "import": "./src/updates-node.mjs" + }, + "./upstream": { + "types": "./src/upstream.d.mts", + "import": "./src/upstream.mjs" } }, "bin": { @@ -236,7 +256,8 @@ "example:conformance": "node examples/conformance/run.mjs", "example:conformance-browser": "node examples/conformance/serve.mjs", "example:combined": "node examples/combined-workload/run.mjs", - "benchmark:combined": "node scripts/benchmark-combined.mjs" + "benchmark:combined": "node scripts/benchmark-combined.mjs", + "example:release-tools": "node examples/release-tools/run.mjs" }, "devDependencies": { "typescript": "5.9.3" diff --git a/schemas/artifact-sink-capabilities.schema.json b/schemas/artifact-sink-capabilities.schema.json index 9746916..e5d9dea 100644 --- a/schemas/artifact-sink-capabilities.schema.json +++ b/schemas/artifact-sink-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/artifact-sink-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/artifact-sink-capabilities.schema.json", "title": "artifact-sink-capabilities", "type": "object", "properties": { diff --git a/schemas/artifact-storage-capabilities.schema.json b/schemas/artifact-storage-capabilities.schema.json index 4a56cb4..058264e 100644 --- a/schemas/artifact-storage-capabilities.schema.json +++ b/schemas/artifact-storage-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/artifact-storage-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/artifact-storage-capabilities.schema.json", "title": "artifact-storage-capabilities", "oneOf": [ { diff --git a/schemas/artifact-transfer-receipt.schema.json b/schemas/artifact-transfer-receipt.schema.json index 8043749..06313f9 100644 --- a/schemas/artifact-transfer-receipt.schema.json +++ b/schemas/artifact-transfer-receipt.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/artifact-transfer-receipt.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/artifact-transfer-receipt.schema.json", "title": "artifact-transfer-receipt", "type": "object", "properties": { diff --git a/schemas/binary-artifact-capabilities.schema.json b/schemas/binary-artifact-capabilities.schema.json index dc8bedf..87bfeb7 100644 --- a/schemas/binary-artifact-capabilities.schema.json +++ b/schemas/binary-artifact-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/binary-artifact-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/binary-artifact-capabilities.schema.json", "title": "binary-artifact-capabilities", "type": "object", "properties": { diff --git a/schemas/binary-artifact-chunk.schema.json b/schemas/binary-artifact-chunk.schema.json index 1807b13..c737186 100644 --- a/schemas/binary-artifact-chunk.schema.json +++ b/schemas/binary-artifact-chunk.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/binary-artifact-chunk.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/binary-artifact-chunk.schema.json", "title": "binary-artifact-chunk", "type": "object", "properties": { diff --git a/schemas/binary-artifact-list.schema.json b/schemas/binary-artifact-list.schema.json index 66cac39..e43451c 100644 --- a/schemas/binary-artifact-list.schema.json +++ b/schemas/binary-artifact-list.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/binary-artifact-list.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/binary-artifact-list.schema.json", "title": "binary-artifact-list", "type": "object", "properties": { diff --git a/schemas/binary-artifact-reference.schema.json b/schemas/binary-artifact-reference.schema.json index adecb93..e4a26e0 100644 --- a/schemas/binary-artifact-reference.schema.json +++ b/schemas/binary-artifact-reference.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/binary-artifact-reference.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/binary-artifact-reference.schema.json", "title": "binary-artifact-reference", "type": "object", "properties": { diff --git a/schemas/binary-artifact.schema.json b/schemas/binary-artifact.schema.json index 8eb04cc..7ab78c1 100644 --- a/schemas/binary-artifact.schema.json +++ b/schemas/binary-artifact.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/binary-artifact.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/binary-artifact.schema.json", "title": "binary-artifact", "type": "object", "properties": { diff --git a/schemas/browser-artifact-checkpoint.schema.json b/schemas/browser-artifact-checkpoint.schema.json index d838b11..63bcaf5 100644 --- a/schemas/browser-artifact-checkpoint.schema.json +++ b/schemas/browser-artifact-checkpoint.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/browser-artifact-checkpoint.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/browser-artifact-checkpoint.schema.json", "title": "browser-artifact-checkpoint", "type": "object", "properties": { diff --git a/schemas/bundle-dependency.schema.json b/schemas/bundle-dependency.schema.json new file mode 100644 index 0000000..84213b9 --- /dev/null +++ b/schemas/bundle-dependency.schema.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/bundle-dependency.schema.json", + "title": "bundle-dependency", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "entry": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "type": { + "enum": [ + "module", + "commonjs" + ] + }, + "dependencies": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + } + }, + "license": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "licenseFile": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "noticeFile": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + { + "type": "null" + } + ] + }, + "redistributable": { + "const": true + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "data": { + "type": "string", + "maxLength": 5592408 + } + }, + "required": [ + "path", + "data" + ], + "additionalProperties": false + }, + "maxItems": 1024 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "name", + "version", + "source", + "entry", + "type", + "dependencies", + "license", + "licenseFile", + "noticeFile", + "redistributable", + "files", + "sha256" + ], + "additionalProperties": false +} diff --git a/schemas/code-action.schema.json b/schemas/code-action.schema.json index c114d5a..4006eae 100644 --- a/schemas/code-action.schema.json +++ b/schemas/code-action.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/code-action.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/code-action.schema.json", "title": "code-action", "type": "object", "properties": { diff --git a/schemas/command.schema.json b/schemas/command.schema.json index 565c2c8..77c1693 100644 --- a/schemas/command.schema.json +++ b/schemas/command.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/command.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/command.schema.json", "title": "command", "type": "object", "properties": { diff --git a/schemas/completion-item.schema.json b/schemas/completion-item.schema.json index 2d06238..2eea20d 100644 --- a/schemas/completion-item.schema.json +++ b/schemas/completion-item.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/completion-item.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/completion-item.schema.json", "title": "completion-item", "type": "object", "properties": { diff --git a/schemas/conformance-report.schema.json b/schemas/conformance-report.schema.json index 88e109e..dfbd8b5 100644 --- a/schemas/conformance-report.schema.json +++ b/schemas/conformance-report.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/conformance-report.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/conformance-report.schema.json", "title": "conformance-report", "type": "object", "properties": { diff --git a/schemas/data-schema.schema.json b/schemas/data-schema.schema.json index 9805c19..fbd1c8b 100644 --- a/schemas/data-schema.schema.json +++ b/schemas/data-schema.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/data-schema.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/data-schema.schema.json", "title": "data-schema", "type": "object", "properties": { diff --git a/schemas/development-definition.schema.json b/schemas/development-definition.schema.json index 58142c7..b78ce2f 100644 --- a/schemas/development-definition.schema.json +++ b/schemas/development-definition.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/development-definition.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/development-definition.schema.json", "title": "development-definition", "type": "object", "properties": { @@ -20,18 +20,18 @@ "commands": { "type": "array", "items": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/command.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/command.schema.json" }, "maxItems": 64 }, "settings": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/settings-definition.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/settings-definition.schema.json" }, "catalog": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/locale-catalog.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/locale-catalog.schema.json" }, "theme": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/theme.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/theme.schema.json" } }, "required": [ diff --git a/schemas/diagnostic-report.schema.json b/schemas/diagnostic-report.schema.json index b7088ae..4bfd9e9 100644 --- a/schemas/diagnostic-report.schema.json +++ b/schemas/diagnostic-report.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/diagnostic-report.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/diagnostic-report.schema.json", "title": "diagnostic-report", "type": "object", "properties": { diff --git a/schemas/diagnostics-request.schema.json b/schemas/diagnostics-request.schema.json index a1e3a5c..bc5593e 100644 --- a/schemas/diagnostics-request.schema.json +++ b/schemas/diagnostics-request.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/diagnostics-request.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/diagnostics-request.schema.json", "title": "diagnostics-request", "type": "object", "properties": { diff --git a/schemas/diagnostics-result.schema.json b/schemas/diagnostics-result.schema.json index 5ed5b59..e780c5f 100644 --- a/schemas/diagnostics-result.schema.json +++ b/schemas/diagnostics-result.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/diagnostics-result.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/diagnostics-result.schema.json", "title": "diagnostics-result", "type": "object", "properties": { diff --git a/schemas/display-metadata.schema.json b/schemas/display-metadata.schema.json index 579d898..436459c 100644 --- a/schemas/display-metadata.schema.json +++ b/schemas/display-metadata.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/display-metadata.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/display-metadata.schema.json", "title": "display-metadata", "type": "object", "properties": { diff --git a/schemas/document-symbol-tree.schema.json b/schemas/document-symbol-tree.schema.json index fc590be..c5f45a1 100644 --- a/schemas/document-symbol-tree.schema.json +++ b/schemas/document-symbol-tree.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/document-symbol-tree.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/document-symbol-tree.schema.json", "title": "document-symbol-tree", "type": "array", "items": { diff --git a/schemas/error.schema.json b/schemas/error.schema.json index 994102e..f0a93c9 100644 --- a/schemas/error.schema.json +++ b/schemas/error.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/error.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/error.schema.json", "title": "error", "type": "object", "properties": { diff --git a/schemas/folding-ranges.schema.json b/schemas/folding-ranges.schema.json index 2853423..c138491 100644 --- a/schemas/folding-ranges.schema.json +++ b/schemas/folding-ranges.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/folding-ranges.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/folding-ranges.schema.json", "title": "folding-ranges", "type": "array", "items": { diff --git a/schemas/formatting-options.schema.json b/schemas/formatting-options.schema.json index d60704d..f2ba2c7 100644 --- a/schemas/formatting-options.schema.json +++ b/schemas/formatting-options.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/formatting-options.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/formatting-options.schema.json", "title": "formatting-options", "type": "object", "properties": { diff --git a/schemas/generation-receipt.schema.json b/schemas/generation-receipt.schema.json index 416e0ef..cb321a7 100644 --- a/schemas/generation-receipt.schema.json +++ b/schemas/generation-receipt.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/generation-receipt.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/generation-receipt.schema.json", "title": "generation-receipt", "type": "object", "properties": { diff --git a/schemas/generation-report.schema.json b/schemas/generation-report.schema.json index 15fe226..5a93bc8 100644 --- a/schemas/generation-report.schema.json +++ b/schemas/generation-report.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/generation-report.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/generation-report.schema.json", "title": "generation-report", "type": "object", "properties": { diff --git a/schemas/inlay-hints.schema.json b/schemas/inlay-hints.schema.json index 0c939d1..c82295e 100644 --- a/schemas/inlay-hints.schema.json +++ b/schemas/inlay-hints.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/inlay-hints.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/inlay-hints.schema.json", "title": "inlay-hints", "type": "array", "items": { diff --git a/schemas/job-artifact-content.schema.json b/schemas/job-artifact-content.schema.json index bcd8b12..210352c 100644 --- a/schemas/job-artifact-content.schema.json +++ b/schemas/job-artifact-content.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-artifact-content.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-artifact-content.schema.json", "title": "job-artifact-content", "type": "object", "properties": { diff --git a/schemas/job-capabilities.schema.json b/schemas/job-capabilities.schema.json index 7e362ae..82e2042 100644 --- a/schemas/job-capabilities.schema.json +++ b/schemas/job-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-capabilities.schema.json", "title": "job-capabilities", "type": "object", "properties": { diff --git a/schemas/job-events.schema.json b/schemas/job-events.schema.json index 4f7688b..5f9ea2a 100644 --- a/schemas/job-events.schema.json +++ b/schemas/job-events.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-events.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-events.schema.json", "title": "job-events", "type": "object", "properties": { diff --git a/schemas/job-history-page.schema.json b/schemas/job-history-page.schema.json index decc33d..a65681c 100644 --- a/schemas/job-history-page.schema.json +++ b/schemas/job-history-page.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-history-page.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-history-page.schema.json", "title": "job-history-page", "type": "object", "properties": { diff --git a/schemas/job-history-query.schema.json b/schemas/job-history-query.schema.json index b24c65e..fa408d6 100644 --- a/schemas/job-history-query.schema.json +++ b/schemas/job-history-query.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-history-query.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-history-query.schema.json", "title": "job-history-query", "type": "object", "properties": { diff --git a/schemas/job-options.schema.json b/schemas/job-options.schema.json index 9ca2218..643709f 100644 --- a/schemas/job-options.schema.json +++ b/schemas/job-options.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-options.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-options.schema.json", "title": "job-options", "type": "object", "properties": { diff --git a/schemas/job-recovery.schema.json b/schemas/job-recovery.schema.json index 2cfd5ab..b88c836 100644 --- a/schemas/job-recovery.schema.json +++ b/schemas/job-recovery.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-recovery.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-recovery.schema.json", "title": "job-recovery", "oneOf": [ { @@ -125,7 +125,7 @@ "uniqueItems": true }, "snapshot": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-snapshot.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-snapshot.schema.json" }, "events": { "type": "array", @@ -691,7 +691,7 @@ "uniqueItems": true }, "snapshot": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-snapshot.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-snapshot.schema.json" }, "events": { "type": "array", @@ -1257,7 +1257,7 @@ "uniqueItems": true }, "snapshot": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-snapshot.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-snapshot.schema.json" }, "events": { "type": "array", diff --git a/schemas/job-snapshot.schema.json b/schemas/job-snapshot.schema.json index cbdc2cb..b709553 100644 --- a/schemas/job-snapshot.schema.json +++ b/schemas/job-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-snapshot.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-snapshot.schema.json", "title": "job-snapshot", "oneOf": [ { @@ -305,7 +305,7 @@ "const": "succeeded" }, "result": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/json-value.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/json-value.schema.json" } }, "required": [ diff --git a/schemas/job-storage-capabilities.schema.json b/schemas/job-storage-capabilities.schema.json index f1e16b3..39c83be 100644 --- a/schemas/job-storage-capabilities.schema.json +++ b/schemas/job-storage-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-storage-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-storage-capabilities.schema.json", "title": "job-storage-capabilities", "oneOf": [ { diff --git a/schemas/job-store-identity.schema.json b/schemas/job-store-identity.schema.json index fded5b2..c84003e 100644 --- a/schemas/job-store-identity.schema.json +++ b/schemas/job-store-identity.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-store-identity.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-store-identity.schema.json", "title": "job-store-identity", "type": "object", "properties": { diff --git a/schemas/json-value.schema.json b/schemas/json-value.schema.json index 86dac4a..fa6adb3 100644 --- a/schemas/json-value.schema.json +++ b/schemas/json-value.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/json-value.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/json-value.schema.json", "title": "json-value", "$ref": "#/$defs/value", "x-maxUtf8Bytes": 262144, diff --git a/schemas/language-capabilities.schema.json b/schemas/language-capabilities.schema.json index 25f4e28..d27e600 100644 --- a/schemas/language-capabilities.schema.json +++ b/schemas/language-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/language-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/language-capabilities.schema.json", "title": "language-capabilities", "type": "object", "properties": { diff --git a/schemas/language-request.schema.json b/schemas/language-request.schema.json index f64fbc3..aa74568 100644 --- a/schemas/language-request.schema.json +++ b/schemas/language-request.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/language-request.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/language-request.schema.json", "title": "language-request", "oneOf": [ { diff --git a/schemas/language-result.schema.json b/schemas/language-result.schema.json index f447288..ce9ee1f 100644 --- a/schemas/language-result.schema.json +++ b/schemas/language-result.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/language-result.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/language-result.schema.json", "title": "language-result", "oneOf": [ { @@ -3178,7 +3178,7 @@ "const": "document-symbol-tree" }, "data": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/document-symbol-tree.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/document-symbol-tree.schema.json" } }, "required": [ diff --git a/schemas/language-snippet.schema.json b/schemas/language-snippet.schema.json index 04a9e72..5e89a27 100644 --- a/schemas/language-snippet.schema.json +++ b/schemas/language-snippet.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/language-snippet.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/language-snippet.schema.json", "title": "language-snippet", "type": "string", "minLength": 0, diff --git a/schemas/locale-catalog.schema.json b/schemas/locale-catalog.schema.json index 6d20e7c..2cc246d 100644 --- a/schemas/locale-catalog.schema.json +++ b/schemas/locale-catalog.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/locale-catalog.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/locale-catalog.schema.json", "title": "locale-catalog", "type": "object", "properties": { diff --git a/schemas/localized-message.schema.json b/schemas/localized-message.schema.json index fc92f1e..8d614b2 100644 --- a/schemas/localized-message.schema.json +++ b/schemas/localized-message.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/localized-message.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/localized-message.schema.json", "title": "localized-message", "type": "object", "properties": { diff --git a/schemas/manifest.schema.json b/schemas/manifest.schema.json index ccb5841..08e800e 100644 --- a/schemas/manifest.schema.json +++ b/schemas/manifest.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/manifest.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/manifest.schema.json", "title": "manifest", "properties": { "id": { diff --git a/schemas/memory-job-snapshot.schema.json b/schemas/memory-job-snapshot.schema.json index de99818..80f9956 100644 --- a/schemas/memory-job-snapshot.schema.json +++ b/schemas/memory-job-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/memory-job-snapshot.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/memory-job-snapshot.schema.json", "title": "memory-job-snapshot", "type": "object", "properties": { @@ -8,12 +8,12 @@ "const": 1 }, "identity": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-store-identity.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-store-identity.schema.json" }, "records": { "type": "array", "items": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/stored-job.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/stored-job.schema.json" }, "maxItems": 256 }, diff --git a/schemas/plugin-bundle.schema.json b/schemas/plugin-bundle.schema.json new file mode 100644 index 0000000..460165d --- /dev/null +++ b/schemas/plugin-bundle.schema.json @@ -0,0 +1,168 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/plugin-bundle.schema.json", + "title": "plugin-bundle", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "plugin": { + "type": "object", + "properties": { + "archive": { + "type": "string", + "maxLength": 16777216 + }, + "metadata": { + "type": "object" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "archive", + "metadata", + "sha256" + ], + "additionalProperties": false + }, + "dependencies": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + } + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "entry": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "type": { + "enum": [ + "module", + "commonjs" + ] + }, + "dependencies": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + } + }, + "license": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "licenseFile": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "noticeFile": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + { + "type": "null" + } + ] + }, + "redistributable": { + "const": true + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "data": { + "type": "string", + "maxLength": 5592408 + } + }, + "required": [ + "path", + "data" + ], + "additionalProperties": false + }, + "maxItems": 1024 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "name", + "version", + "source", + "entry", + "type", + "dependencies", + "license", + "licenseFile", + "noticeFile", + "redistributable", + "files", + "sha256" + ], + "additionalProperties": false + }, + "maxItems": 32 + }, + "sbom": { + "type": "object" + } + }, + "required": [ + "schemaVersion", + "plugin", + "dependencies", + "packages", + "sbom" + ], + "additionalProperties": false, + "$comment": "Runtime additionally verifies canonical JSON, all digests and archive limits, single-version acyclic reachable graph, licenses/NOTICE, total decoded budget, and exact CycloneDX 1.6 SBOM derived from content." +} diff --git a/schemas/plugin-plan.schema.json b/schemas/plugin-plan.schema.json index ae7e24f..bb3511b 100644 --- a/schemas/plugin-plan.schema.json +++ b/schemas/plugin-plan.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/plugin-plan.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/plugin-plan.schema.json", "title": "plugin-plan", "type": "object", "properties": { diff --git a/schemas/plugin-update-plan.schema.json b/schemas/plugin-update-plan.schema.json new file mode 100644 index 0000000..4ec985f --- /dev/null +++ b/schemas/plugin-update-plan.schema.json @@ -0,0 +1,432 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/plugin-update-plan.schema.json", + "title": "plugin-update-plan", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "current": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "bundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "pluginId": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "api": { + "type": "object", + "properties": { + "protocolVersion": { + "const": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "runtime": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "supportedHosts": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 8 + } + }, + "required": [ + "protocolVersion", + "capabilities", + "runtime", + "supportedHosts" + ], + "additionalProperties": false + }, + "grants": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "required": [ + "name", + "version", + "sha256", + "source" + ], + "additionalProperties": false + }, + "maxItems": 32 + }, + "tools": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 64 + }, + "licenses": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 33 + }, + "settingsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "provenanceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "keyId": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "schemaVersion", + "bundleSha256", + "name", + "publisher", + "pluginId", + "version", + "api", + "grants", + "dependencies", + "tools", + "licenses", + "settingsSha256", + "provenanceSha256", + "keyId" + ], + "additionalProperties": false + }, + "candidate": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "bundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "pluginId": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "api": { + "type": "object", + "properties": { + "protocolVersion": { + "const": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "runtime": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "supportedHosts": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 8 + } + }, + "required": [ + "protocolVersion", + "capabilities", + "runtime", + "supportedHosts" + ], + "additionalProperties": false + }, + "grants": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "required": [ + "name", + "version", + "sha256", + "source" + ], + "additionalProperties": false + }, + "maxItems": 32 + }, + "tools": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 64 + }, + "licenses": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 33 + }, + "settingsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "provenanceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "keyId": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "schemaVersion", + "bundleSha256", + "name", + "publisher", + "pluginId", + "version", + "api", + "grants", + "dependencies", + "tools", + "licenses", + "settingsSha256", + "provenanceSha256", + "keyId" + ], + "additionalProperties": false + }, + "migration": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "reversible": { + "type": "boolean" + } + }, + "required": [ + "id", + "reversible" + ], + "additionalProperties": false + }, + "jobPolicy": { + "enum": [ + "wait", + "retain-old" + ] + }, + "changes": { + "type": "object", + "properties": { + "version": { + "type": "boolean" + }, + "api": { + "type": "boolean" + }, + "grants": { + "type": "boolean" + }, + "dependencies": { + "type": "boolean" + }, + "tools": { + "type": "boolean" + }, + "licenses": { + "type": "boolean" + }, + "settingsSha256": { + "type": "boolean" + }, + "provenanceSha256": { + "type": "boolean" + }, + "keyId": { + "type": "boolean" + } + }, + "required": [ + "version", + "api", + "grants", + "dependencies", + "tools", + "licenses", + "settingsSha256", + "provenanceSha256", + "keyId" + ], + "additionalProperties": false + }, + "externalEffectsUndone": { + "const": false + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "schemaVersion", + "current", + "candidate", + "migration", + "jobPolicy", + "changes", + "externalEffectsUndone", + "sha256" + ], + "additionalProperties": false, + "$comment": "Runtime verifies the plan digest, byte-bound descriptors and current policy. Approval is not transferable to another digest/key/grant/settings set." +} diff --git a/schemas/plugin-update-record.schema.json b/schemas/plugin-update-record.schema.json new file mode 100644 index 0000000..ab32ea4 --- /dev/null +++ b/schemas/plugin-update-record.schema.json @@ -0,0 +1,484 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/plugin-update-record.schema.json", + "title": "plugin-update-record", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "revision": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "activeSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "settingsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "phase": { + "enum": [ + "idle", + "staged", + "approved", + "preparing", + "activated", + "failed" + ] + }, + "plan": { + "oneOf": [ + { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "current": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "bundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "pluginId": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "api": { + "type": "object", + "properties": { + "protocolVersion": { + "const": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "runtime": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "supportedHosts": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 8 + } + }, + "required": [ + "protocolVersion", + "capabilities", + "runtime", + "supportedHosts" + ], + "additionalProperties": false + }, + "grants": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "required": [ + "name", + "version", + "sha256", + "source" + ], + "additionalProperties": false + }, + "maxItems": 32 + }, + "tools": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 64 + }, + "licenses": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 33 + }, + "settingsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "provenanceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "keyId": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "schemaVersion", + "bundleSha256", + "name", + "publisher", + "pluginId", + "version", + "api", + "grants", + "dependencies", + "tools", + "licenses", + "settingsSha256", + "provenanceSha256", + "keyId" + ], + "additionalProperties": false + }, + "candidate": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "bundleSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "pluginId": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "api": { + "type": "object", + "properties": { + "protocolVersion": { + "const": 1 + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "runtime": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "supportedHosts": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 8 + } + }, + "required": [ + "protocolVersion", + "capabilities", + "runtime", + "supportedHosts" + ], + "additionalProperties": false + }, + "grants": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 64 + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "source": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "required": [ + "name", + "version", + "sha256", + "source" + ], + "additionalProperties": false + }, + "maxItems": 32 + }, + "tools": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 64 + }, + "licenses": { + "type": "array", + "items": { + "type": "object" + }, + "maxItems": 33 + }, + "settingsSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "provenanceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "keyId": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "schemaVersion", + "bundleSha256", + "name", + "publisher", + "pluginId", + "version", + "api", + "grants", + "dependencies", + "tools", + "licenses", + "settingsSha256", + "provenanceSha256", + "keyId" + ], + "additionalProperties": false + }, + "migration": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "reversible": { + "type": "boolean" + } + }, + "required": [ + "id", + "reversible" + ], + "additionalProperties": false + }, + "jobPolicy": { + "enum": [ + "wait", + "retain-old" + ] + }, + "changes": { + "type": "object", + "properties": { + "version": { + "type": "boolean" + }, + "api": { + "type": "boolean" + }, + "grants": { + "type": "boolean" + }, + "dependencies": { + "type": "boolean" + }, + "tools": { + "type": "boolean" + }, + "licenses": { + "type": "boolean" + }, + "settingsSha256": { + "type": "boolean" + }, + "provenanceSha256": { + "type": "boolean" + }, + "keyId": { + "type": "boolean" + } + }, + "required": [ + "version", + "api", + "grants", + "dependencies", + "tools", + "licenses", + "settingsSha256", + "provenanceSha256", + "keyId" + ], + "additionalProperties": false + }, + "externalEffectsUndone": { + "const": false + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "schemaVersion", + "current", + "candidate", + "migration", + "jobPolicy", + "changes", + "externalEffectsUndone", + "sha256" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "disposition": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [ + "schemaVersion", + "revision", + "activeSha256", + "settingsSha256", + "phase", + "plan", + "disposition" + ], + "additionalProperties": false +} diff --git a/schemas/project-entry.schema.json b/schemas/project-entry.schema.json index 5a643dc..74dc011 100644 --- a/schemas/project-entry.schema.json +++ b/schemas/project-entry.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-entry.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-entry.schema.json", "title": "project-entry", "type": "object", "properties": { diff --git a/schemas/project-query-capabilities.schema.json b/schemas/project-query-capabilities.schema.json index 7549181..18dd3c2 100644 --- a/schemas/project-query-capabilities.schema.json +++ b/schemas/project-query-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-query-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-query-capabilities.schema.json", "title": "project-query-capabilities", "type": "object", "properties": { diff --git a/schemas/project-query-options.schema.json b/schemas/project-query-options.schema.json index aae0c8b..bb53486 100644 --- a/schemas/project-query-options.schema.json +++ b/schemas/project-query-options.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-query-options.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-query-options.schema.json", "title": "project-query-options", "oneOf": [ { diff --git a/schemas/project-query-page.schema.json b/schemas/project-query-page.schema.json index 1f7a8df..ac4bc49 100644 --- a/schemas/project-query-page.schema.json +++ b/schemas/project-query-page.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-query-page.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-query-page.schema.json", "title": "project-query-page", "type": "object", "properties": { diff --git a/schemas/project-snapshot.schema.json b/schemas/project-snapshot.schema.json index d22eff2..82fb340 100644 --- a/schemas/project-snapshot.schema.json +++ b/schemas/project-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-snapshot.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-snapshot.schema.json", "title": "project-snapshot", "type": "object", "properties": { diff --git a/schemas/project-watch-capabilities.schema.json b/schemas/project-watch-capabilities.schema.json index 07e5210..80bd735 100644 --- a/schemas/project-watch-capabilities.schema.json +++ b/schemas/project-watch-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-watch-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-watch-capabilities.schema.json", "title": "project-watch-capabilities", "type": "object", "properties": { diff --git a/schemas/project-watch-event.schema.json b/schemas/project-watch-event.schema.json index 18cfc1a..914bda1 100644 --- a/schemas/project-watch-event.schema.json +++ b/schemas/project-watch-event.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-watch-event.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-watch-event.schema.json", "title": "project-watch-event", "type": "object", "properties": { diff --git a/schemas/project-watch-options.schema.json b/schemas/project-watch-options.schema.json index 91f7153..eb3c2be 100644 --- a/schemas/project-watch-options.schema.json +++ b/schemas/project-watch-options.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/project-watch-options.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/project-watch-options.schema.json", "title": "project-watch-options", "type": "object", "properties": { diff --git a/schemas/provenance-envelope.schema.json b/schemas/provenance-envelope.schema.json new file mode 100644 index 0000000..b85da12 --- /dev/null +++ b/schemas/provenance-envelope.schema.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/provenance-envelope.schema.json", + "title": "provenance-envelope", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "algorithm": { + "const": "Ed25519" + }, + "keyId": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "payload": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "subject": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "name", + "version", + "sha256" + ], + "additionalProperties": false + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "source": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "url", + "revision" + ], + "additionalProperties": false + }, + "issuedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiresAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "schemaVersion", + "subject", + "publisher", + "source", + "issuedAt", + "expiresAt" + ], + "additionalProperties": false + }, + "signature": { + "type": "string", + "minLength": 88, + "maxLength": 88 + } + }, + "required": [ + "schemaVersion", + "algorithm", + "keyId", + "payload", + "signature" + ], + "additionalProperties": false, + "$comment": "Runtime verifies canonical base64 and Ed25519 over DDS-PROVENANCE-V1 plus canonical statement JSON. Trust keys are operator-owned Node KeyObjects; keys are not taken from this envelope." +} diff --git a/schemas/provenance-receipt.schema.json b/schemas/provenance-receipt.schema.json new file mode 100644 index 0000000..84e9ae9 --- /dev/null +++ b/schemas/provenance-receipt.schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/provenance-receipt.schema.json", + "title": "provenance-receipt", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "subject": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "name", + "version", + "sha256" + ], + "additionalProperties": false + }, + "checksum": { + "type": "boolean" + }, + "signature": { + "enum": [ + "verified", + "invalid", + "unsigned", + "untrusted-key", + "unsupported" + ] + }, + "publisher": { + "enum": [ + "verified", + "unverified" + ] + }, + "policy": { + "enum": [ + "accepted", + "rejected" + ] + }, + "keyId": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + { + "type": "null" + } + ] + }, + "checkedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "offline": { + "type": "boolean" + }, + "revocationsObservedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "reasons": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 32 + }, + "executionAuthorized": { + "const": false + } + }, + "required": [ + "schemaVersion", + "subject", + "checksum", + "signature", + "publisher", + "policy", + "keyId", + "checkedAt", + "offline", + "revocationsObservedAt", + "reasons", + "executionAuthorized" + ], + "additionalProperties": false +} diff --git a/schemas/provenance-statement.schema.json b/schemas/provenance-statement.schema.json new file mode 100644 index 0000000..69b84fe --- /dev/null +++ b/schemas/provenance-statement.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/provenance-statement.schema.json", + "title": "provenance-statement", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "subject": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "name", + "version", + "sha256" + ], + "additionalProperties": false + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "source": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "url", + "revision" + ], + "additionalProperties": false + }, + "issuedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiresAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "schemaVersion", + "subject", + "publisher", + "source", + "issuedAt", + "expiresAt" + ], + "additionalProperties": false +} diff --git a/schemas/secret-reference.schema.json b/schemas/secret-reference.schema.json index 209fa3b..a41fb02 100644 --- a/schemas/secret-reference.schema.json +++ b/schemas/secret-reference.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/secret-reference.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/secret-reference.schema.json", "title": "secret-reference", "type": "object", "properties": { diff --git a/schemas/secret-scope.schema.json b/schemas/secret-scope.schema.json index ceff815..7173070 100644 --- a/schemas/secret-scope.schema.json +++ b/schemas/secret-scope.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/secret-scope.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/secret-scope.schema.json", "title": "secret-scope", "type": "object", "properties": { diff --git a/schemas/semantic-legend.schema.json b/schemas/semantic-legend.schema.json index 3844b66..9ba82f7 100644 --- a/schemas/semantic-legend.schema.json +++ b/schemas/semantic-legend.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/semantic-legend.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/semantic-legend.schema.json", "title": "semantic-legend", "type": "object", "properties": { diff --git a/schemas/semantic-tokens-delta.schema.json b/schemas/semantic-tokens-delta.schema.json index 3a2dafc..b700fbb 100644 --- a/schemas/semantic-tokens-delta.schema.json +++ b/schemas/semantic-tokens-delta.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/semantic-tokens-delta.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/semantic-tokens-delta.schema.json", "title": "semantic-tokens-delta", "type": "object", "properties": { diff --git a/schemas/semantic-tokens.schema.json b/schemas/semantic-tokens.schema.json index cbd6a95..89aab37 100644 --- a/schemas/semantic-tokens.schema.json +++ b/schemas/semantic-tokens.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/semantic-tokens.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/semantic-tokens.schema.json", "title": "semantic-tokens", "type": "object", "properties": { diff --git a/schemas/settings-definition.schema.json b/schemas/settings-definition.schema.json index 14082bf..9efb8e2 100644 --- a/schemas/settings-definition.schema.json +++ b/schemas/settings-definition.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/settings-definition.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/settings-definition.schema.json", "title": "settings-definition", "type": "object", "properties": { diff --git a/schemas/settings-snapshot.schema.json b/schemas/settings-snapshot.schema.json index e2d3aed..7c87732 100644 --- a/schemas/settings-snapshot.schema.json +++ b/schemas/settings-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/settings-snapshot.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/settings-snapshot.schema.json", "title": "settings-snapshot", "type": "object", "properties": { diff --git a/schemas/settings-state.schema.json b/schemas/settings-state.schema.json index d1fb967..266f6bd 100644 --- a/schemas/settings-state.schema.json +++ b/schemas/settings-state.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/settings-state.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/settings-state.schema.json", "title": "settings-state", "type": "object", "properties": { diff --git a/schemas/signature-help.schema.json b/schemas/signature-help.schema.json index 189114e..43f060f 100644 --- a/schemas/signature-help.schema.json +++ b/schemas/signature-help.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/signature-help.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/signature-help.schema.json", "title": "signature-help", "oneOf": [ { diff --git a/schemas/stored-artifact-chunk.schema.json b/schemas/stored-artifact-chunk.schema.json index e32ff60..5342f35 100644 --- a/schemas/stored-artifact-chunk.schema.json +++ b/schemas/stored-artifact-chunk.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/stored-artifact-chunk.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/stored-artifact-chunk.schema.json", "title": "stored-artifact-chunk", "type": "object", "properties": { diff --git a/schemas/stored-artifact-list.schema.json b/schemas/stored-artifact-list.schema.json index d86fa35..be6455c 100644 --- a/schemas/stored-artifact-list.schema.json +++ b/schemas/stored-artifact-list.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/stored-artifact-list.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/stored-artifact-list.schema.json", "title": "stored-artifact-list", "type": "object", "properties": { diff --git a/schemas/stored-artifact-reference.schema.json b/schemas/stored-artifact-reference.schema.json index ddc79f2..c4f205a 100644 --- a/schemas/stored-artifact-reference.schema.json +++ b/schemas/stored-artifact-reference.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/stored-artifact-reference.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/stored-artifact-reference.schema.json", "title": "stored-artifact-reference", "type": "object", "properties": { diff --git a/schemas/stored-artifact.schema.json b/schemas/stored-artifact.schema.json index f884ffa..35d224c 100644 --- a/schemas/stored-artifact.schema.json +++ b/schemas/stored-artifact.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/stored-artifact.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/stored-artifact.schema.json", "title": "stored-artifact", "type": "object", "properties": { diff --git a/schemas/stored-job.schema.json b/schemas/stored-job.schema.json index 01f4a87..cac4d7c 100644 --- a/schemas/stored-job.schema.json +++ b/schemas/stored-job.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/stored-job.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/stored-job.schema.json", "title": "stored-job", "type": "object", "properties": { @@ -79,7 +79,7 @@ "uniqueItems": true }, "snapshot": { - "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/job-snapshot.schema.json" + "$ref": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/job-snapshot.schema.json" }, "events": { "type": "array", diff --git a/schemas/test-trace.schema.json b/schemas/test-trace.schema.json index 8bf6935..6ce6008 100644 --- a/schemas/test-trace.schema.json +++ b/schemas/test-trace.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/test-trace.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/test-trace.schema.json", "title": "test-trace", "type": "object", "properties": { diff --git a/schemas/transfer-queue-options.schema.json b/schemas/transfer-queue-options.schema.json index 607646a..64f4291 100644 --- a/schemas/transfer-queue-options.schema.json +++ b/schemas/transfer-queue-options.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/transfer-queue-options.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/transfer-queue-options.schema.json", "title": "transfer-queue-options", "type": "object", "properties": { diff --git a/schemas/transfer-snapshot.schema.json b/schemas/transfer-snapshot.schema.json index 090600e..5cfa26d 100644 --- a/schemas/transfer-snapshot.schema.json +++ b/schemas/transfer-snapshot.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/transfer-snapshot.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/transfer-snapshot.schema.json", "title": "transfer-snapshot", "type": "object", "properties": { diff --git a/schemas/upload-capabilities.schema.json b/schemas/upload-capabilities.schema.json index c9525f7..eb87ec9 100644 --- a/schemas/upload-capabilities.schema.json +++ b/schemas/upload-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/upload-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upload-capabilities.schema.json", "title": "upload-capabilities", "type": "object", "properties": { diff --git a/schemas/upload-chunk.schema.json b/schemas/upload-chunk.schema.json index dad6f54..e223f4f 100644 --- a/schemas/upload-chunk.schema.json +++ b/schemas/upload-chunk.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/upload-chunk.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upload-chunk.schema.json", "title": "upload-chunk", "type": "object", "properties": { diff --git a/schemas/upload-reference.schema.json b/schemas/upload-reference.schema.json index 90b6e77..cc8db6d 100644 --- a/schemas/upload-reference.schema.json +++ b/schemas/upload-reference.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/upload-reference.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upload-reference.schema.json", "title": "upload-reference", "type": "object", "properties": { diff --git a/schemas/upload-spec.schema.json b/schemas/upload-spec.schema.json index 93f9fac..8a2acb1 100644 --- a/schemas/upload-spec.schema.json +++ b/schemas/upload-spec.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/upload-spec.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upload-spec.schema.json", "title": "upload-spec", "type": "object", "properties": { diff --git a/schemas/upload-status.schema.json b/schemas/upload-status.schema.json index 072785b..805f3af 100644 --- a/schemas/upload-status.schema.json +++ b/schemas/upload-status.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/upload-status.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upload-status.schema.json", "title": "upload-status", "type": "object", "properties": { diff --git a/schemas/upstream-candidate.schema.json b/schemas/upstream-candidate.schema.json new file mode 100644 index 0000000..bd9d0d3 --- /dev/null +++ b/schemas/upstream-candidate.schema.json @@ -0,0 +1,487 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upstream-candidate.schema.json", + "title": "upstream-candidate", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "current": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "source": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "url", + "version", + "sha256" + ], + "additionalProperties": false + }, + "observedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "manifest": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "api": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096, + "uniqueItems": true + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "notice": { + "type": "string", + "maxLength": 131072 + }, + "redistributable": { + "type": "boolean" + } + }, + "required": [ + "name", + "text", + "notice", + "redistributable" + ], + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "path", + "sha256" + ], + "additionalProperties": false + }, + "maxItems": 4096 + } + }, + "required": [ + "schemaVersion", + "name", + "version", + "revision", + "api", + "license", + "files" + ], + "additionalProperties": false + }, + "manifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "schemaVersion", + "source", + "observedAt", + "manifest", + "manifestSha256" + ], + "additionalProperties": false + }, + "candidate": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "source": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "url", + "version", + "sha256" + ], + "additionalProperties": false + }, + "observedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "manifest": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "api": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096, + "uniqueItems": true + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "notice": { + "type": "string", + "maxLength": 131072 + }, + "redistributable": { + "type": "boolean" + } + }, + "required": [ + "name", + "text", + "notice", + "redistributable" + ], + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "path", + "sha256" + ], + "additionalProperties": false + }, + "maxItems": 4096 + } + }, + "required": [ + "schemaVersion", + "name", + "version", + "revision", + "api", + "license", + "files" + ], + "additionalProperties": false + }, + "manifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "schemaVersion", + "source", + "observedAt", + "manifest", + "manifestSha256" + ], + "additionalProperties": false + }, + "tagChanged": { + "type": "boolean" + }, + "api": { + "type": "object", + "properties": { + "added": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096 + }, + "removed": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096 + }, + "missingRequired": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096 + } + }, + "required": [ + "added", + "removed", + "missingRequired" + ], + "additionalProperties": false + }, + "files": { + "type": "object", + "properties": { + "added": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "maxItems": 4096 + }, + "removed": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "maxItems": 4096 + }, + "changed": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "maxItems": 4096 + } + }, + "required": [ + "added", + "removed", + "changed" + ], + "additionalProperties": false + }, + "licenseChanged": { + "type": "boolean" + }, + "redistributable": { + "type": "boolean" + }, + "compatibility": { + "enum": [ + "supported", + "unsupported", + "unverified" + ] + }, + "functional": { + "type": "object", + "properties": { + "state": { + "enum": [ + "passed", + "failed", + "unverified" + ] + }, + "detail": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "required": [ + "state", + "detail" + ], + "additionalProperties": false + }, + "patch": { + "type": "object", + "properties": { + "expectedSourceSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "replacementSource": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "url", + "version", + "sha256" + ], + "additionalProperties": false + } + }, + "required": [ + "expectedSourceSha256", + "replacementSource" + ], + "additionalProperties": false + }, + "reviewRequired": { + "const": true + }, + "automaticActions": { + "type": "array", + "maxItems": 0 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "schemaVersion", + "current", + "candidate", + "tagChanged", + "api", + "files", + "licenseChanged", + "redistributable", + "compatibility", + "functional", + "patch", + "reviewRequired", + "automaticActions", + "sha256" + ], + "additionalProperties": false +} diff --git a/schemas/upstream-manifest.schema.json b/schemas/upstream-manifest.schema.json new file mode 100644 index 0000000..6baffde --- /dev/null +++ b/schemas/upstream-manifest.schema.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upstream-manifest.schema.json", + "title": "upstream-manifest", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "api": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096, + "uniqueItems": true + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "notice": { + "type": "string", + "maxLength": 131072 + }, + "redistributable": { + "type": "boolean" + } + }, + "required": [ + "name", + "text", + "notice", + "redistributable" + ], + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "path", + "sha256" + ], + "additionalProperties": false + }, + "maxItems": 4096 + } + }, + "required": [ + "schemaVersion", + "name", + "version", + "revision", + "api", + "license", + "files" + ], + "additionalProperties": false +} diff --git a/schemas/upstream-snapshot.schema.json b/schemas/upstream-snapshot.schema.json new file mode 100644 index 0000000..07835cb --- /dev/null +++ b/schemas/upstream-snapshot.schema.json @@ -0,0 +1,150 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/upstream-snapshot.schema.json", + "title": "upstream-snapshot", + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "source": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "url", + "version", + "sha256" + ], + "additionalProperties": false + }, + "observedAt": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "manifest": { + "type": "object", + "properties": { + "schemaVersion": { + "const": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 96, + "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" + }, + "revision": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "api": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "maxItems": 4096, + "uniqueItems": true + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "text": { + "type": "string", + "minLength": 1, + "maxLength": 131072 + }, + "notice": { + "type": "string", + "maxLength": 131072 + }, + "redistributable": { + "type": "boolean" + } + }, + "required": [ + "name", + "text", + "notice", + "redistributable" + ], + "additionalProperties": false + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "maxLength": 180 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "path", + "sha256" + ], + "additionalProperties": false + }, + "maxItems": 4096 + } + }, + "required": [ + "schemaVersion", + "name", + "version", + "revision", + "api", + "license", + "files" + ], + "additionalProperties": false + }, + "manifestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "schemaVersion", + "source", + "observedAt", + "manifest", + "manifestSha256" + ], + "additionalProperties": false +} diff --git a/schemas/workspace-conditional-file.schema.json b/schemas/workspace-conditional-file.schema.json index fb626e3..300e13d 100644 --- a/schemas/workspace-conditional-file.schema.json +++ b/schemas/workspace-conditional-file.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-conditional-file.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-conditional-file.schema.json", "title": "workspace-conditional-file", "oneOf": [ { diff --git a/schemas/workspace-edit-preview.schema.json b/schemas/workspace-edit-preview.schema.json index 2bec6a7..a82f36c 100644 --- a/schemas/workspace-edit-preview.schema.json +++ b/schemas/workspace-edit-preview.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-edit-preview.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-edit-preview.schema.json", "title": "workspace-edit-preview", "type": "object", "properties": { diff --git a/schemas/workspace-edit-receipt.schema.json b/schemas/workspace-edit-receipt.schema.json index 0b948fa..6aeb08b 100644 --- a/schemas/workspace-edit-receipt.schema.json +++ b/schemas/workspace-edit-receipt.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-edit-receipt.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-edit-receipt.schema.json", "title": "workspace-edit-receipt", "type": "object", "properties": { diff --git a/schemas/workspace-edit-record.schema.json b/schemas/workspace-edit-record.schema.json index ffbf2fd..e7913fd 100644 --- a/schemas/workspace-edit-record.schema.json +++ b/schemas/workspace-edit-record.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-edit-record.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-edit-record.schema.json", "title": "workspace-edit-record", "type": "object", "properties": { diff --git a/schemas/workspace-edit.schema.json b/schemas/workspace-edit.schema.json index a2375cc..1accbfd 100644 --- a/schemas/workspace-edit.schema.json +++ b/schemas/workspace-edit.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-edit.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-edit.schema.json", "title": "workspace-edit", "type": "object", "properties": { diff --git a/schemas/workspace-file-capabilities.schema.json b/schemas/workspace-file-capabilities.schema.json index 24bff25..03429bd 100644 --- a/schemas/workspace-file-capabilities.schema.json +++ b/schemas/workspace-file-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-file-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-file-capabilities.schema.json", "title": "workspace-file-capabilities", "type": "object", "properties": { diff --git a/schemas/workspace-file-revision.schema.json b/schemas/workspace-file-revision.schema.json index 720d4fa..c5549eb 100644 --- a/schemas/workspace-file-revision.schema.json +++ b/schemas/workspace-file-revision.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-file-revision.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-file-revision.schema.json", "title": "workspace-file-revision", "type": "object", "properties": { diff --git a/schemas/workspace-project-capabilities.schema.json b/schemas/workspace-project-capabilities.schema.json index 791dfe2..088dfaf 100644 --- a/schemas/workspace-project-capabilities.schema.json +++ b/schemas/workspace-project-capabilities.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-project-capabilities.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-project-capabilities.schema.json", "title": "workspace-project-capabilities", "type": "object", "properties": { diff --git a/schemas/workspace-project-poll.schema.json b/schemas/workspace-project-poll.schema.json index 85c6101..4890a22 100644 --- a/schemas/workspace-project-poll.schema.json +++ b/schemas/workspace-project-poll.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.0.0/schemas/workspace-project-poll.schema.json", + "$id": "https://github.com/Altifigence/dds-plugin-sdk/blob/v1.1.0/schemas/workspace-project-poll.schema.json", "title": "workspace-project-poll", "type": "object", "properties": { diff --git a/scripts/api-support.mjs b/scripts/api-support.mjs index 19a3999..40f82ee 100644 --- a/scripts/api-support.mjs +++ b/scripts/api-support.mjs @@ -4,8 +4,8 @@ import ts from 'typescript'; import {SDK_PLUGIN_PEER_RANGE} from '../src/version.mjs'; const mode=process.argv[2];if(!['--write','--check'].includes(mode))throw Error('Use --write or --check'); const root=new URL('../',import.meta.url),pkg=JSON.parse(await readFile(new URL('package.json',root),'utf8')); -const optional=new Set(['./workspace-node','./workspace-client','./jobs','./artifacts','./job-storage','./job-history','./job-storage-node','./artifact-storage','./artifact-storage-node','./workspace-edits','./workspace-edits-node','./project-watch','./project-watch-node','./project-query','./project-query-node','./artifact-transfer','./artifact-transfer-browser','./uploads','./uploads-node','./uploads-browser','./artifact-resume-browser','./transfer-queue','./settings','./secrets']); -const nodeOnly=new Set(['./publishing','./devtools','./workspace-node','./job-storage-node','./artifact-storage-node','./workspace-edits-node','./project-watch-node','./project-query-node','./uploads-node','./conformance-node']); +const optional=new Set(['./bundles','./provenance','./updates','./updates-node','./upstream','./workspace-node','./workspace-client','./jobs','./artifacts','./job-storage','./job-history','./job-storage-node','./artifact-storage','./artifact-storage-node','./workspace-edits','./workspace-edits-node','./project-watch','./project-watch-node','./project-query','./project-query-node','./artifact-transfer','./artifact-transfer-browser','./uploads','./uploads-node','./uploads-browser','./artifact-resume-browser','./transfer-queue','./settings','./secrets']); +const nodeOnly=new Set(['./bundles','./provenance','./updates','./updates-node','./upstream','./publishing','./devtools','./workspace-node','./job-storage-node','./artifact-storage-node','./workspace-edits-node','./project-watch-node','./project-query-node','./uploads-node','./conformance-node']); function typeNames(source){ const file=ts.createSourceFile('entry.d.mts',source,ts.ScriptTarget.Latest,true),names=new Set(); for(const statement of file.statements){ diff --git a/scripts/pack-check.mjs b/scripts/pack-check.mjs index 93d6516..bb3e117 100644 --- a/scripts/pack-check.mjs +++ b/scripts/pack-check.mjs @@ -236,6 +236,9 @@ try { await cp(join(root, 'tests', 'types', 'artifacts.mts'), join(consumer, 'artifacts.mts')); for(const name of ['job-storage.mts','job-history.mts','artifact-storage.mts','project-watch.mts','project-query.mts','workspace-project-tools.mts','artifact-transfer.mts','artifact-resume-browser.mts']) await cp(join(root,'tests','types',name),join(consumer,name)); run(process.execPath, [join(installed, 'examples', 'binary-artifacts', 'run.mjs')], consumer); + const releaseTools=JSON.parse(run(process.execPath,[join(installed,'examples','release-tools','run.mjs')],consumer)); + assert.equal(releaseTools.offlineInstall,true);assert.equal(releaseTools.oldJobPinned,true);assert.equal(releaseTools.rollback,true);assert.equal(releaseTools.upstreamCandidate,true); + await cp(join(root,'tests','types','release-tools.mts'),join(consumer,'release-tools.mts')); await cp(join(root, 'tests', 'types', 'tsconfig.json'), join(consumer, 'tsconfig.json')); const typescript = join(root, 'node_modules', 'typescript', 'bin', 'tsc'); run(process.execPath, [typescript, '--noEmit', '-p', 'tsconfig.json'], consumer); diff --git a/src/bundles.d.mts b/src/bundles.d.mts new file mode 100644 index 0000000..d81358a --- /dev/null +++ b/src/bundles.d.mts @@ -0,0 +1,22 @@ +import type {VerifiedPluginArchive} from './publishing.mjs'; +export interface BundleFile {readonly path:string; readonly data:string;} +export interface BundleDependencyInput { + readonly name:string; readonly version:string; readonly source:string; readonly entry:string; readonly type:'module'|'commonjs'; + readonly dependencies:Readonly>; readonly license:string; readonly licenseFile:string; readonly noticeFile:string|null; + readonly redistributable:true; readonly files:readonly BundleFile[]; +} +export interface LockedBundleDependency extends BundleDependencyInput {readonly sha256:string;} +export interface PluginBundleInput { + readonly plugin:{readonly archive:string; readonly metadata:unknown; readonly sha256:string}; + readonly dependencies:Readonly>; readonly packages:readonly LockedBundleDependency[]; +} +export interface BundleReceipt { + readonly schemaVersion:1; readonly sha256:string; readonly checksumPinned:boolean; readonly plugin:VerifiedPluginArchive; + readonly dependencies:readonly (Omit & {readonly files:readonly {readonly path:string;readonly size:number;readonly sha256:string}[]})[]; + readonly fileCount:number; readonly unpackedBytes:number; readonly sbom:Readonly>; readonly executionAuthorized:false; +} +export const BUNDLE_LIMITS:Readonly<{packages:32;files:1024;fileBytes:number;unpackedBytes:number;bundleBytes:number}>; +export function lockBundleDependency(input:BundleDependencyInput):LockedBundleDependency; +export function createPluginBundle(input:PluginBundleInput):Uint8Array; +export function verifyPluginBundle(bytes:Uint8Array,options?:{readonly expectedSha256?:string}):BundleReceipt; +export function installPluginBundle(bytes:Uint8Array,options:{readonly destination:string;readonly expectedSha256:string;readonly approved:true}):Promise; diff --git a/src/bundles.mjs b/src/bundles.mjs new file mode 100644 index 0000000..493a979 --- /dev/null +++ b/src/bundles.mjs @@ -0,0 +1,136 @@ +import {mkdir, writeFile, realpath, lstat} from 'node:fs/promises'; +import path from 'node:path'; +import {inspectPluginArchiveBytes} from './publishing.mjs'; +import {base64, canonical, clone, digest, exactVersion, fail, fields, portablePath, sha, sourceUrl, string} from './release-internals.mjs'; + +export const BUNDLE_LIMITS = Object.freeze({packages:32, files:1024, fileBytes:4*1024*1024, unpackedBytes:32*1024*1024, bundleBytes:48*1024*1024}); +function packageName(value) { + if (typeof value !== 'string' || value.length > 128 || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value) || value === '@altifigence/dds-plugin-sdk') fail('INVALID', 'Unsupported dependency name'); + for (const part of value.replace(/^@/, '').split('/')) portablePath(part); + return value; +} +function dependencies(value) { + if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length > BUNDLE_LIMITS.packages) fail('INVALID', 'Invalid dependency map'); + return Object.fromEntries(Object.keys(value).sort().map(name => [packageName(name), exactVersion(value[name])])); +} +function dependency(input, pinned) { + fields(input, ['name','version','source','entry','type','dependencies','license','licenseFile','noticeFile','redistributable','files', ...(pinned ? ['sha256'] : [])]); + const value = clone(input); + packageName(value.name); exactVersion(value.version); sourceUrl(value.source); portablePath(value.entry); portablePath(value.licenseFile); + if (value.noticeFile !== null) portablePath(value.noticeFile); + string(value.license, 'license', 256); + if (!['module','commonjs'].includes(value.type) || value.redistributable !== true) fail('LICENSE', 'Explicit redistribution approval and supported module type required'); + value.dependencies = dependencies(value.dependencies); + if (!Array.isArray(value.files) || !value.files.length || value.files.length > BUNDLE_LIMITS.files) fail('LIMIT', 'Invalid dependency file count'); + const names = new Set(); let bytes = 0; + value.files = value.files.map(file => { + fields(file, ['path','data']); portablePath(file.path); + const key = file.path.toLowerCase(); + if (key === 'package.json' || names.has(key)) fail('INVALID', 'Duplicate or generated dependency file'); + names.add(key); const data = base64(file.data, BUNDLE_LIMITS.fileBytes); + if ((bytes += data.length) > BUNDLE_LIMITS.unpackedBytes) fail('LIMIT', 'Dependency bytes exceeded'); + return {path:file.path, data:file.data}; + }).sort((a,b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0); + for (const name of names) { + const parts = name.split('/'); + for (let n=1;nx!==null)) { + const file = value.files.find(f=>f.path===required); + if (!file || !base64(file.data,BUNDLE_LIMITS.fileBytes).length) fail('LICENSE', 'Entry or license/NOTICE file missing or empty'); + } + const {sha256, ...content} = value; + const actual = digest(canonical(content)); + if (pinned && sha(sha256) !== actual) fail('INTEGRITY', 'Dependency digest mismatch'); + return {...content, sha256:actual}; +} +/** Lock a reviewed file tree; does not read disk, fetch dependencies, or grant execution. */ +export function lockBundleDependency(input) { return dependency(input, false); } +function graph(packages, roots) { + const map = new Map(); + for (const pkg of packages) { + if (map.has(pkg.name)) fail('CONFLICT', 'A bundle allows one exact version per dependency name'); + map.set(pkg.name,pkg); + } + const visited = new Set(), active = new Set(); + const visit = (name, version) => { + const pkg = map.get(name); + if (!pkg || pkg.version !== version) fail('CONFLICT', 'Missing dependency or conflicting exact version'); + if (active.has(name)) fail('CONFLICT', 'Dependency cycles are unsupported'); + if (visited.has(name)) return; + active.add(name); + for (const [child, expected] of Object.entries(pkg.dependencies)) visit(child,expected); + active.delete(name); visited.add(name); + }; + for (const [name, version] of Object.entries(roots)) visit(name,version); + if (visited.size !== packages.length) fail('INVALID', 'Unreferenced dependencies are unsupported'); +} +function bom(root, packages, roots) { + const rootRef = `plugin:${root.publisher}/${root.pluginId}@${root.pluginVersion}`; + const ref = p => `pkg:npm/${p.name.replace('@','%40')}@${p.version}`; + const refs = new Map(packages.map(p=>[p.name,ref(p)])); + return { + bomFormat:'CycloneDX', specVersion:'1.6', version:1, + metadata:{component:{type:'application', name:`@${root.publisher}/${root.pluginId}`, version:root.pluginVersion, 'bom-ref':rootRef, hashes:[{alg:'SHA-256', content:root.artifact.sha256}], licenses:[{license:{name:root.license}}]}}, + components:packages.map(p=>({type:'library', name:p.name, version:p.version, 'bom-ref':ref(p), purl:ref(p), hashes:[{alg:'SHA-256', content:p.sha256}], licenses:[{license:{name:p.license}}], externalReferences:[{type:'distribution',url:p.source}], properties:[{name:'dds:digest-subject',value:'dds-dependency-tree-v1'},{name:'dds:license-file',value:p.licenseFile},...(p.noticeFile?[{name:'dds:notice-file',value:p.noticeFile}]:[])]})), + dependencies:[{ref:rootRef,dependsOn:Object.keys(roots).map(n=>refs.get(n))},...packages.map(p=>({ref:ref(p),dependsOn:Object.keys(p.dependencies).map(n=>refs.get(n))}))], + }; +} +function build(input) { + fields(input, ['schemaVersion','plugin','dependencies','packages']); + if (input.schemaVersion !== 1) fail('UNSUPPORTED', 'Unsupported bundle version'); + fields(input.plugin, ['archive','metadata','sha256']); + const plugin = inspectPluginArchiveBytes(base64(input.plugin.archive,12*1024*1024),input.plugin.metadata,{expectedSha256:sha(input.plugin.sha256)}); + const roots = dependencies(input.dependencies); + if (!Array.isArray(input.packages) || input.packages.length>BUNDLE_LIMITS.packages) fail('LIMIT','Too many packages'); + const packages = input.packages.map(p=>dependency(p,true)).sort((a,b)=>a.nameb.name?1:0); + graph(packages,roots); + let count=plugin.files.length, bytes=plugin.receipt.unpackedSize; + for(const pkg of packages){count+=pkg.files.length+1;for(const file of pkg.files)bytes+=base64(file.data,BUNDLE_LIMITS.fileBytes).length;bytes+=Buffer.byteLength(canonical(packageMetadata(pkg)));} + if(count>BUNDLE_LIMITS.files||bytes>BUNDLE_LIMITS.unpackedBytes)fail('LIMIT','Bundle expanded file/byte limit exceeded'); + const value={schemaVersion:1,plugin:clone(input.plugin),dependencies:roots,packages}; + const sbom=bom(plugin.receipt,packages,roots); + return {value,plugin,sbom,bytes,count}; +} +function packageMetadata(pkg) {return {name:pkg.name,version:pkg.version,type:pkg.type,main:`./${pkg.entry}`,exports:`./${pkg.entry}`,license:pkg.license,dependencies:pkg.dependencies};} +/** Produces platform-independent UTF-8 JSON bytes with embedded original archive and SBOM. */ +export function createPluginBundle(input) { + const {value,sbom}=build({schemaVersion:1,...input}); + return Uint8Array.from(Buffer.from(canonical({...value,sbom},BUNDLE_LIMITS.bundleBytes))); +} +function inspect(input, options={}) { + fields(options,[],['expectedSha256']); + if(!(input instanceof Uint8Array)||input.byteLength>BUNDLE_LIMITS.bundleBytes)fail('LIMIT','Invalid bundle byte count'); + const bytes=Buffer.from(input);const hash=digest(bytes); + if(options.expectedSha256!==undefined&&sha(options.expectedSha256)!==hash)fail('INTEGRITY','Bundle digest mismatch'); + let value;try{value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes));}catch{fail('INVALID','Invalid bundle JSON');} + fields(value,['schemaVersion','plugin','dependencies','packages','sbom']); + if(canonical(value,BUNDLE_LIMITS.bundleBytes)!==bytes.toString('utf8'))fail('INVALID','Bundle must use canonical DDS JSON'); + const {sbom:claimed,...body}=value;const result=build(body); + if(canonical(result.sbom)!==canonical(claimed))fail('INTEGRITY','SBOM does not match locked graph'); + return {...result,receipt:{schemaVersion:1,sha256:hash,checksumPinned:options.expectedSha256!==undefined,plugin:result.plugin.receipt,dependencies:result.value.packages.map(({files,...p})=>({...p,files:files.map(f=>({path:f.path,size:base64(f.data,BUNDLE_LIMITS.fileBytes).length,sha256:digest(base64(f.data,BUNDLE_LIMITS.fileBytes))}))})),fileCount:result.count,unpackedBytes:result.bytes,sbom:result.sbom,executionAuthorized:false}}; +} +/** Verification grants no permissions and never executes contained code. */ +export function verifyPluginBundle(bytes,options={}){return clone(inspect(bytes,options).receipt);} +/** The operator supplies a trusted parent and a new destination; this is not an OS sandbox. */ +export async function installPluginBundle(bytes,options){ + fields(options,['destination','expectedSha256','approved']); + if(options.approved!==true)fail('APPROVAL_REQUIRED','Installation approval required'); + const snapshot=inspect(bytes,{expectedSha256:sha(options.expectedSha256)}); + string(options.destination,'destination',4096); + const requested=path.resolve(options.destination), parent=await realpath(path.dirname(requested)); + if((await lstat(parent)).isSymbolicLink())fail('INVALID','Use a real destination parent'); + const destination=path.join(parent,path.basename(requested)); + await mkdir(destination,{recursive:false}); + // Never remove a failed installation automatically: a receipt/caller may need to inspect it. + const write=async(name,data)=>{const target=path.join(destination,...name.split('/'));await mkdir(path.dirname(target),{recursive:true});await writeFile(target,data,{flag:'wx',mode:0o600});}; + try{ + for(const file of snapshot.plugin.files)await write(file.path,file.data); + for(const pkg of snapshot.value.packages){ + const prefix=`node_modules/${pkg.name}/`; + for(const file of pkg.files)await write(prefix+file.path,base64(file.data,BUNDLE_LIMITS.fileBytes)); + await write(prefix+'package.json',canonical(packageMetadata(pkg))+'\n'); + } + return {...clone(snapshot.receipt),destination,installed:true,lifecycleScriptsRun:false,networkUsed:false}; + }catch(error){throw Object.assign(new Error('Installation incomplete; inspect or remove the new destination'),{code:'INSTALL_FAILED',destination,cause:error});} +} diff --git a/src/provenance.d.mts b/src/provenance.d.mts new file mode 100644 index 0000000..7502079 --- /dev/null +++ b/src/provenance.d.mts @@ -0,0 +1,12 @@ +export interface ProvenanceSubject {readonly name:string;readonly version:string;readonly sha256:string;} +export interface ProvenanceStatement {readonly schemaVersion:1;readonly subject:ProvenanceSubject;readonly publisher:string;readonly source:{readonly url:string;readonly revision:string};readonly issuedAt:number;readonly expiresAt:number;} +/** Node crypto KeyObject; use generateKeyPairSync/createPublicKey in the owning host. */ +export interface ProvenanceKey {readonly type:string;readonly asymmetricKeyType?:string;} +export interface ProvenanceEnvelope {readonly schemaVersion:1;readonly algorithm:'Ed25519';readonly keyId:string;readonly payload:ProvenanceStatement;readonly signature:string;} +export interface ProvenanceTrustRoot {readonly keyId:string;readonly publisher:string;readonly publicKey:ProvenanceKey;readonly validFrom:number;readonly validUntil:number;readonly revokedAt:number|null;} +export interface ProvenancePolicy {readonly subject:ProvenanceSubject;readonly publisher:string;readonly trustRoots:readonly ProvenanceTrustRoot[];readonly now:number;readonly revocations:{readonly observedAt:number;readonly revokedKeyIds:readonly string[]};readonly maxRevocationAgeMs:number;readonly offline:boolean;readonly allowUnsigned:boolean;readonly clockSkewMs?:number;} +export interface ProvenanceReceipt {readonly schemaVersion:1;readonly subject:ProvenanceSubject;readonly checksum:boolean;readonly signature:'verified'|'invalid'|'unsigned'|'untrusted-key'|'unsupported';readonly publisher:'verified'|'unverified';readonly policy:'accepted'|'rejected';readonly keyId:string|null;readonly checkedAt:number;readonly offline:boolean;readonly revocationsObservedAt:number;readonly reasons:readonly string[];readonly executionAuthorized:false;} +export function createProvenanceStatement(input:Omit):ProvenanceStatement; +export function canonicalProvenancePayload(statement:ProvenanceStatement):Uint8Array; +export function signPluginProvenance(statement:ProvenanceStatement,options:{readonly keyId:string;readonly privateKey:ProvenanceKey}):ProvenanceEnvelope; +export function verifyPluginProvenance(bytes:Uint8Array,envelope:ProvenanceEnvelope|null,options:ProvenancePolicy):ProvenanceReceipt; diff --git a/src/provenance.mjs b/src/provenance.mjs new file mode 100644 index 0000000..1bd5465 --- /dev/null +++ b/src/provenance.mjs @@ -0,0 +1,68 @@ +import {KeyObject, sign, verify} from 'node:crypto'; +import {base64, canonical, clone, digest, exactVersion, fail, fields, sha, sourceUrl, string} from './release-internals.mjs'; + +const MAX_ARTIFACT=48*1024*1024; +function time(value){if(!Number.isSafeInteger(value)||value<0)fail('INVALID','Expected nonnegative epoch milliseconds');return value;} +function subject(value){fields(value,['name','version','sha256']);string(value.name,'subject',256);exactVersion(value.version);sha(value.sha256);return value;} +function statement(value){ + fields(value,['schemaVersion','subject','publisher','source','issuedAt','expiresAt']); + if(value.schemaVersion!==1)fail('UNSUPPORTED','Unsupported provenance statement'); + subject(value.subject);string(value.publisher,'publisher',128); + fields(value.source,['url','revision']);sourceUrl(value.source.url);string(value.source.revision,'source revision',128); + if(time(value.expiresAt)<=time(value.issuedAt))fail('INVALID','Provenance expiration must follow issue time'); + canonical(value,16384);return clone(value); +} +/** DDS canonical JSON v1: sorted object keys, preserved arrays, finite JSON numbers, UTF-8. */ +export function canonicalProvenancePayload(value){return Uint8Array.from(Buffer.from('DDS-PROVENANCE-V1\n'+canonical(statement(value),16384)));} +export function createProvenanceStatement(value){return statement({schemaVersion:1,...value});} +/** The caller owns the key. No release or DDS product signing key is supplied by this SDK. */ +export function signPluginProvenance(value,options){ + fields(options,['keyId','privateKey']);string(options.keyId,'key ID',128); + if(!(options.privateKey instanceof KeyObject)||options.privateKey.type!=='private'||options.privateKey.asymmetricKeyType!=='ed25519')fail('UNSUPPORTED','An Ed25519 private KeyObject is required'); + const payload=statement(value); + return {schemaVersion:1,algorithm:'Ed25519',keyId:options.keyId,payload,signature:sign(null,canonicalProvenancePayload(payload),options.privateKey).toString('base64')}; +} +/** The operator supplies current trust/revocation evidence; verification never fetches it. */ +export function verifyPluginProvenance(bytes,envelope,options){ + fields(options,['subject','publisher','trustRoots','now','revocations','maxRevocationAgeMs','offline','allowUnsigned'],['clockSkewMs']); + subject(options.subject);string(options.publisher,'publisher',128);time(options.now); + if(!(bytes instanceof Uint8Array)||bytes.byteLength>MAX_ARTIFACT)fail('LIMIT','Invalid artifact byte count'); + if(typeof options.offline!=='boolean'||typeof options.allowUnsigned!=='boolean')fail('INVALID','Explicit offline/unsigned policy required'); + const skew=options.clockSkewMs??0;time(skew);time(options.maxRevocationAgeMs); + if(skew>300000||options.maxRevocationAgeMs>30*86400000)fail('LIMIT','Trust freshness policy exceeds maximum'); + fields(options.revocations,['observedAt','revokedKeyIds']);time(options.revocations.observedAt); + if(!Array.isArray(options.revocations.revokedKeyIds)||options.revocations.revokedKeyIds.length>1024)fail('LIMIT','Too many revoked keys'); + for(const key of options.revocations.revokedKeyIds)string(key,'revoked key ID',128); + if(!Array.isArray(options.trustRoots)||options.trustRoots.length>64)fail('LIMIT','Too many trust roots'); + const keys=new Set(); + for(const root of options.trustRoots){ + fields(root,['keyId','publisher','publicKey','validFrom','validUntil','revokedAt']);string(root.keyId,'key ID',128);string(root.publisher,'publisher',128); + if(keys.has(root.keyId))fail('INVALID','Duplicate trust root');keys.add(root.keyId); + if(!(root.publicKey instanceof KeyObject)||root.publicKey.type!=='public'||root.publicKey.asymmetricKeyType!=='ed25519')fail('UNSUPPORTED','An Ed25519 public KeyObject is required'); + if(time(root.validUntil)<=time(root.validFrom))fail('INVALID','Invalid trust-root validity'); + if(root.revokedAt!==null)time(root.revokedAt); + } + const checksum=digest(bytes)===options.subject.sha256; + const result={schemaVersion:1,subject:clone(options.subject),checksum,signature:'unsigned',publisher:'unverified',policy:'rejected',keyId:null,checkedAt:options.now,offline:options.offline,revocationsObservedAt:options.revocations.observedAt,reasons:[],executionAuthorized:false}; + if(!checksum)result.reasons.push('artifact-digest-mismatch'); + if(envelope===null){if(!options.allowUnsigned)result.reasons.push('unsigned-not-allowed');else result.reasons.push('unsigned-explicitly-allowed');result.policy=checksum&&options.allowUnsigned?'accepted':'rejected';return result;} + fields(envelope,['schemaVersion','algorithm','keyId','payload','signature']); + if(envelope.schemaVersion!==1||envelope.algorithm!=='Ed25519'){result.signature='unsupported';result.reasons.push('unsupported-envelope');return result;} + string(envelope.keyId,'key ID',128);result.keyId=envelope.keyId; + const payload=statement(envelope.payload),sig=base64(envelope.signature,64); + if(sig.length!==64)fail('INVALID','Ed25519 signature must contain 64 bytes'); + if(canonical(payload.subject)!==canonical(options.subject))result.reasons.push('subject-mismatch'); + if(payload.publisher!==options.publisher)result.reasons.push('publisher-mismatch'); + const root=options.trustRoots.find(r=>r.keyId===envelope.keyId); + if(!root){result.signature='untrusted-key';result.reasons.push('untrusted-key');return result;} + result.signature=verify(null,canonicalProvenancePayload(payload),root.publicKey,sig)?'verified':'invalid'; + if(result.signature!=='verified')result.reasons.push('invalid-signature'); + if(root.publisher!==options.publisher||root.publisher!==payload.publisher)result.reasons.push('key-publisher-mismatch'); + else if(result.signature==='verified')result.publisher='verified'; + if(options.now+skew=payload.expiresAt)result.reasons.push('statement-time-invalid'); + if(options.now+skew=root.validUntil||payload.issuedAt=root.validUntil)result.reasons.push('key-time-invalid'); + if((root.revokedAt!==null&&root.revokedAt<=options.now+skew)||options.revocations.revokedKeyIds.includes(root.keyId))result.reasons.push('key-revoked'); + if(options.revocations.observedAt>options.now+skew||options.now-options.revocations.observedAt>options.maxRevocationAgeMs)result.reasons.push('revocation-evidence-stale'); + if(!result.reasons.length)result.policy='accepted'; + return result; +} diff --git a/src/publishing.d.mts b/src/publishing.d.mts index 2c684c5..8a381b5 100644 --- a/src/publishing.d.mts +++ b/src/publishing.d.mts @@ -30,3 +30,9 @@ export function verifyPluginArchive(archivePath: string, options?: { /** A lowercase SHA-256 obtained independently from a trusted release channel. */ readonly expectedSha256?: string; }): Promise; +/** Returns copies of verified bytes. Later mutation does not alter the receipt's subject. */ +export function inspectPluginArchiveBytes(archive: Uint8Array, metadata: unknown, options?: {readonly expectedSha256?: string}): { + readonly receipt: VerifiedPluginArchive; + readonly manifest: import('./index.mjs').PluginManifest; + readonly files: readonly {readonly path: string; readonly data: Uint8Array}[]; +}; diff --git a/src/publishing.mjs b/src/publishing.mjs index 12af192..552e4ac 100644 --- a/src/publishing.mjs +++ b/src/publishing.mjs @@ -301,11 +301,22 @@ export async function verifyPluginArchive(archivePath, options = {}) { if (options.expectedSha256 !== undefined && digest !== options.expectedSha256) fail('archive does not match expectedSha256'); const metadataPath = options.metadataPath ?? `${archivePath}.release.json`; const metadata = json(await readInputFile(metadataPath, MAX_RELEASE_METADATA), 'release metadata'); + return inspectPluginArchiveBytes(archive, metadata, {expectedSha256: options.expectedSha256}).receipt; +} + +/** Inspect a bounded in-memory snapshot with the same verifier as local archives. */ +export function inspectPluginArchiveBytes(input, metadata, options = {}) { + if (!(input instanceof Uint8Array) || input.byteLength > MAX_ARCHIVE) fail('invalid or oversized archive bytes'); + record(options, [], ['expectedSha256']); + const archive = Buffer.from(input); + const digest = sha256(archive); + if (options.expectedSha256 !== undefined && (!/^[a-f0-9]{64}$/.test(options.expectedSha256) || digest !== options.expectedSha256)) fail('archive does not match expectedSha256'); + if (JSON.stringify(metadata)?.length > MAX_RELEASE_METADATA) fail('oversized release metadata'); const fields = ['schemaVersion', 'pluginId', 'pluginVersion', 'publisher', 'license', 'sourceVisibility', 'manifestSha256', 'disclosureSha256', 'unpackedSize', 'files', 'artifact']; record(metadata, fields); record(metadata.artifact, ['filename', 'size', 'sha256']); if (metadata.artifact.size !== archive.length || metadata.artifact.sha256 !== digest) fail('archive does not match release metadata'); - const {manifest, report} = inspectContents(archiveContents(archive), true); + const {manifest, report, contents} = inspectContents(archiveContents(archive), true); if (metadata.artifact.filename !== releaseName(manifest)) fail('release filename does not match plugin identity'); for (const field of fields.filter(field => field !== 'files' && field !== 'artifact')) if (metadata[field] !== report[field]) fail('release metadata does not match packaged content'); if (!Array.isArray(metadata.files) || metadata.files.length !== report.files.length) fail('release file inventory does not match archive'); @@ -317,5 +328,6 @@ export async function verifyPluginArchive(archivePath, options = {}) { const artifact = Object.freeze({filename: releaseName(manifest), size: archive.length, sha256: digest}); // This receipt describes the bytes read now, not a mutable path's future // contents, a trusted publisher, an installation or a malware assessment. - return Object.freeze({...report, artifact, checksumPinned: options.expectedSha256 !== undefined}); + const receipt = Object.freeze({...report, artifact, checksumPinned: options.expectedSha256 !== undefined}); + return Object.freeze({receipt, manifest, files: Object.freeze([...contents].map(([path, data]) => Object.freeze({path, data: Uint8Array.from(data)})))}); } diff --git a/src/release-internals.mjs b/src/release-internals.mjs new file mode 100644 index 0000000..c2b081b --- /dev/null +++ b/src/release-internals.mjs @@ -0,0 +1,66 @@ +import {createHash} from 'node:crypto'; +import {WINDOWS_DEVICE_COMPONENT, isPrivateFileComponent} from './patterns.mjs'; + +export const fail = (code, message) => { throw Object.assign(new Error(message), {code}); }; +export const digest = bytes => createHash('sha256').update(bytes).digest('hex'); +export function fields(value, required, optional = []) { + if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || + required.some(k => !Object.hasOwn(value, k)) || Object.keys(value).some(k => ![...required, ...optional].includes(k))) fail('INVALID', 'Unexpected or missing fields'); + return value; +} +export function string(value, label = 'text', max = 1024) { + if (typeof value !== 'string' || !value.trim() || value.length > max || !value.isWellFormed() || /[\u0000-\u001f\u007f]/.test(value)) fail('INVALID', `Invalid ${label}`); + return value; +} +export function sha(value) { if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) fail('INVALID', 'Expected lowercase SHA-256'); return value; } +export function exactVersion(value) { + if (typeof value !== 'string' || value.length > 96 || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/.test(value)) fail('INVALID', 'Expected exact version'); + return value; +} +export function sourceUrl(value, allowLoopback = false) { + string(value, 'source URL', 2048); let url; + try { url = new URL(value); } catch { fail('INVALID', 'Invalid source URL'); } + if (url.username || url.password || url.hash || (url.protocol !== 'https:' && !(allowLoopback && url.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(url.hostname)))) fail('INVALID', 'Expected an explicit HTTPS source URL'); + return url.href; +} +export function canonical(value, maxBytes = 64 * 1024 * 1024) { + const seen = new Set(); let nodes = 0; + const walk = (v, depth) => { + if (++nodes > 100000 || depth > 32) fail('LIMIT', 'JSON structure limit exceeded'); + if (v === null || typeof v === 'boolean') return JSON.stringify(v); + if (typeof v === 'number' && Number.isFinite(v)) return JSON.stringify(v); + if (typeof v === 'string' && v.isWellFormed() && v.length <= maxBytes) return JSON.stringify(v); + if (!v || typeof v !== 'object' || seen.has(v)) fail('INVALID', 'Expected acyclic JSON data'); + seen.add(v); let result; + if (Array.isArray(v)) { + if (v.length > 10000 || Object.keys(v).length !== v.length) fail('LIMIT', 'Invalid JSON array'); + result = '[' + v.map(x => walk(x, depth + 1)).join(',') + ']'; + } else { + if (![Object.prototype, null].includes(Object.getPrototypeOf(v))) fail('INVALID', 'Expected plain JSON object'); + const keys = Object.keys(v).sort(); + if (keys.length > 10000 || keys.some(k => ['__proto__', 'prototype', 'constructor'].includes(k) || !k.isWellFormed())) fail('INVALID', 'Invalid JSON keys'); + result = '{' + keys.map(k => { + if (!Object.hasOwn(Object.getOwnPropertyDescriptor(v, k), 'value')) fail('INVALID', 'JSON accessors are unsupported'); + return JSON.stringify(k) + ':' + walk(v[k], depth + 1); + }).join(',') + '}'; + } + seen.delete(v); if (result.length > maxBytes) fail('LIMIT', 'JSON byte limit exceeded'); return result; + }; + const result = walk(value, 0); + if (Buffer.byteLength(result) > maxBytes) fail('LIMIT', 'JSON byte limit exceeded'); + return result; +} +export const clone = value => JSON.parse(canonical(value)); +export function base64(value, max) { + if (typeof value !== 'string' || value.length > Math.ceil(max / 3) * 4 || value.length % 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) fail('INVALID', 'Invalid or oversized base64'); + const bytes = Buffer.from(value, 'base64'); + if (bytes.length > max || bytes.toString('base64') !== value) fail('LIMIT', 'Invalid base64 content'); + return bytes; +} +export function portablePath(value) { + string(value, 'file path', 180); + if (/[^\x20-\x7e]|[\\:*?"<>|]/.test(value)) fail('INVALID', 'Expected portable relative path'); + const parts = value.split('/'); + if (parts.length > 12 || parts.some(p => !p || p === '.' || p === '..' || /[. ]$/.test(p) || WINDOWS_DEVICE_COMPONENT.test(p) || isPrivateFileComponent(p) || p.toLowerCase() === 'node_modules')) fail('INVALID', 'Unsupported file path'); + return value; +} diff --git a/src/release-schemas.mjs b/src/release-schemas.mjs new file mode 100644 index 0000000..1d04251 --- /dev/null +++ b/src/release-schemas.mjs @@ -0,0 +1,28 @@ +const obj=properties=>({type:'object',properties,required:Object.keys(properties),additionalProperties:false}); +const text=(maxLength=256)=>({type:'string',minLength:1,maxLength}); +const hash={type:'string',pattern:'^[a-f0-9]{64}$'},version={...text(96),pattern:'^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$'}; +const num={type:'integer',minimum:0,maximum:Number.MAX_SAFE_INTEGER},list=(items,maxItems=4096)=>({type:'array',items,maxItems}); +const nullable=value=>({oneOf:[value,{type:'null'}]}); +const source=obj({url:text(2048),version,sha256:hash}); +const files=list(obj({path:text(180),data:{type:'string',maxLength:5592408}}),1024); +const deps={type:'object',maxProperties:32,additionalProperties:version}; +const dependency=obj({name:text(128),version,source:text(2048),entry:text(180),type:{enum:['module','commonjs']},dependencies:deps,license:text(),licenseFile:text(180),noticeFile:nullable(text(180)),redistributable:{const:true},files,sha256:hash}); +const subject=obj({name:text(),version,sha256:hash}); +const statement=obj({schemaVersion:{const:1},subject,publisher:text(128),source:obj({url:text(2048),revision:text(128)}),issuedAt:num,expiresAt:num}); +const descriptor=obj({schemaVersion:{const:1},bundleSha256:hash,name:text(),publisher:text(128),pluginId:text(128),version,api:obj({protocolVersion:{const:1},capabilities:list(text(),64),runtime:text(),supportedHosts:list(text(),8)}),grants:list(text(),64),dependencies:list(obj({name:text(128),version,sha256:hash,source:text(2048)}),32),tools:list({type:'object'},64),licenses:list({type:'object'},33),settingsSha256:hash,provenanceSha256:hash,keyId:nullable(text(128))}); +const changes=obj(Object.fromEntries(['version','api','grants','dependencies','tools','licenses','settingsSha256','provenanceSha256','keyId'].map(x=>[x,{type:'boolean'}]))); +const plan=obj({schemaVersion:{const:1},current:descriptor,candidate:descriptor,migration:obj({id:text(128),reversible:{type:'boolean'}}),jobPolicy:{enum:['wait','retain-old']},changes,externalEffectsUndone:{const:false},sha256:hash}); +const manifest=obj({schemaVersion:{const:1},name:text(128),version,revision:text(128),api:{...list(text()),uniqueItems:true},license:obj({name:text(),text:text(131072),notice:{type:'string',maxLength:131072},redistributable:{type:'boolean'}}),files:list(obj({path:text(180),sha256:hash}))}); +const snapshot=obj({schemaVersion:{const:1},source,observedAt:num,manifest,manifestSha256:hash}); +export const RELEASE_SCHEMAS=Object.freeze({ + 'plugin-bundle':{...obj({schemaVersion:{const:1},plugin:obj({archive:{type:'string',maxLength:16777216},metadata:{type:'object'},sha256:hash}),dependencies:deps,packages:list(dependency,32),sbom:{type:'object'}}),$comment:'Runtime additionally verifies canonical JSON, all digests and archive limits, single-version acyclic reachable graph, licenses/NOTICE, total decoded budget, and exact CycloneDX 1.6 SBOM derived from content.'}, + 'bundle-dependency':dependency, + 'provenance-statement':statement, + 'provenance-envelope':{...obj({schemaVersion:{const:1},algorithm:{const:'Ed25519'},keyId:text(128),payload:statement,signature:{type:'string',minLength:88,maxLength:88}}),$comment:'Runtime verifies canonical base64 and Ed25519 over DDS-PROVENANCE-V1 plus canonical statement JSON. Trust keys are operator-owned Node KeyObjects; keys are not taken from this envelope.'}, + 'provenance-receipt':obj({schemaVersion:{const:1},subject,checksum:{type:'boolean'},signature:{enum:['verified','invalid','unsigned','untrusted-key','unsupported']},publisher:{enum:['verified','unverified']},policy:{enum:['accepted','rejected']},keyId:nullable(text(128)),checkedAt:num,offline:{type:'boolean'},revocationsObservedAt:num,reasons:list(text(),32),executionAuthorized:{const:false}}), + 'plugin-update-plan':{...plan,$comment:'Runtime verifies the plan digest, byte-bound descriptors and current policy. Approval is not transferable to another digest/key/grant/settings set.'}, + 'plugin-update-record':obj({schemaVersion:{const:1},revision:{...num,minimum:1},activeSha256:hash,settingsSha256:hash,phase:{enum:['idle','staged','approved','preparing','activated','failed']},plan:nullable(plan),disposition:text()}), + 'upstream-manifest':manifest, + 'upstream-snapshot':snapshot, + 'upstream-candidate':obj({schemaVersion:{const:1},current:snapshot,candidate:snapshot,tagChanged:{type:'boolean'},api:obj({added:list(text()),removed:list(text()),missingRequired:list(text())}),files:obj({added:list(text(180)),removed:list(text(180)),changed:list(text(180))}),licenseChanged:{type:'boolean'},redistributable:{type:'boolean'},compatibility:{enum:['supported','unsupported','unverified']},functional:obj({state:{enum:['passed','failed','unverified']},detail:text(2048)}),patch:obj({expectedSourceSha256:hash,replacementSource:source}),reviewRequired:{const:true},automaticActions:{type:'array',maxItems:0},sha256:hash}), +}); diff --git a/src/schemas.d.mts b/src/schemas.d.mts index 75c9bc9..506b68d 100644 --- a/src/schemas.d.mts +++ b/src/schemas.d.mts @@ -1,2 +1,2 @@ // Generated by npm run schemas:write. -export const SCHEMAS: Readonly>>>; +export const SCHEMAS: Readonly>>>; diff --git a/src/schemas.mjs b/src/schemas.mjs index 0347540..c094b39 100644 --- a/src/schemas.mjs +++ b/src/schemas.mjs @@ -23,6 +23,7 @@ import {CONFIGURATION_SCHEMAS, DATA_SCHEMA_CONTRACT, DATA_SCHEMA_DEFINITIONS, DI import {LOCALIZATION_SCHEMAS} from './localization-schemas.mjs'; import {DEVELOPMENT_SCHEMAS} from './development-schemas.mjs'; import {CONFORMANCE_SCHEMAS} from './conformance-schemas.mjs'; +import {RELEASE_SCHEMAS} from './release-schemas.mjs'; const schema = 'https://json-schema.org/draft/2020-12/schema'; const base = `https://github.com/Altifigence/dds-plugin-sdk/blob/v${SDK_VERSION}/schemas/`; @@ -123,6 +124,7 @@ const historyCursor = {...text(40),pattern:'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4} const historyItem = object({jobId,commandId:text(128),state:{enum:JOB_STATES},disposition:historyDisposition,startedAt:integer(Number.MAX_SAFE_INTEGER),updatedAt:integer(Number.MAX_SAFE_INTEGER),expiresAt:integer(Number.MAX_SAFE_INTEGER,1),revision:integer(Number.MAX_SAFE_INTEGER,1),attemptOf:{oneOf:[jobId,{type:'null'}]},contentPolicy:{enum:['metadata-only','host-redacted']},artifactCount:integer(JOB_LIMITS.artifacts),snapshotCount:integer(JOB_LIMITS.artifacts),resultAvailability:{enum:['none','source-references','snapshot-references','mixed-references','expired']}}); export const SCHEMAS = Object.freeze({ ...Object.fromEntries(Object.entries(CONFORMANCE_SCHEMAS).map(([name,body])=>[name,define(name,body)])), + ...Object.fromEntries(Object.entries(RELEASE_SCHEMAS).map(([name,body])=>[name,define(name,body)])), ...Object.fromEntries(Object.entries(DEVELOPMENT_SCHEMAS).map(([name,body])=>[name,define(name,body)])), ...Object.fromEntries(Object.entries(CONFIGURATION_SCHEMAS).map(([name, body]) => [name, define(name, body)])), ...Object.fromEntries(Object.entries(LOCALIZATION_SCHEMAS).map(([name, body]) => [name, define(name, body)])), diff --git a/src/updates-node.d.mts b/src/updates-node.d.mts new file mode 100644 index 0000000..d6e3bf5 --- /dev/null +++ b/src/updates-node.d.mts @@ -0,0 +1,3 @@ +import type {PluginUpdateJournal,PluginUpdateRecord} from './updates.mjs'; +export function inspectNodeUpdateJournal(options:{readonly directory:string}):Promise<{state:'readable';record:PluginUpdateRecord|null;total:number}|{state:'unreadable';record:null;errorCode:string}>; +export function createNodeUpdateJournal(options:{readonly directory:string;readonly workspaceRoot:string;readonly recoverStaleLock?:boolean}):Promise}>; diff --git a/src/updates-node.mjs b/src/updates-node.mjs new file mode 100644 index 0000000..644afd9 --- /dev/null +++ b/src/updates-node.mjs @@ -0,0 +1,87 @@ +import {constants} from 'node:fs'; +import {lstat,realpath,open,mkdir,readdir,unlink} from 'node:fs/promises'; +import {randomUUID} from 'node:crypto'; +import {hostname} from 'node:os'; +import path from 'node:path'; +import {canonical,clone,digest,fail,fields,sha} from './release-internals.mjs'; + +const LIMIT=128*1024, MAX_RECORDS=256, MAX_TOTAL=8*1024*1024; +const same=(a,b)=>a.dev===b.dev&&a.ino===b.ino; +async function directory(value){ + if(typeof value!=='string'||!path.isAbsolute(value))fail('INVALID','Expected an absolute directory'); + const before=await lstat(value),root=await realpath(value),after=await lstat(root); + if(!before.isDirectory()||before.isSymbolicLink()||!same(before,after))fail('CONFLICT','Expected a stable real directory'); + return {root,stat:after}; +} +async function read(file,limit=LIMIT){ + const info=await lstat(file); + if(!info.isFile()||info.isSymbolicLink()||info.nlink!==1||info.size>limit)fail('INVALID','Expected bounded regular journal file'); + const handle=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW??0)); + try{const before=await handle.stat();if(!same(info,before)||before.size!==info.size)fail('CONFLICT','Journal file changed');const bytes=Buffer.alloc(info.size+1);let size=0;while(size/^\d{6}\.json$/.test(x)).sort(); + if(files.length>MAX_RECORDS)fail('LIMIT','Journal record limit exceeded'); + let result=null,total=0; + for(let n=0;nMAX_TOTAL)fail('LIMIT','Journal byte budget exceeded'); + const envelope=decode(bytes);fields(envelope,['schemaVersion','sha256','record']); + if(envelope.schemaVersion!==1||digest(canonical(envelope.record,LIMIT))!==envelope.sha256)fail('CORRUPT','Journal digest mismatch'); + result=record(envelope.record);if(result.revision!==n+1)fail('CORRUPT','Journal revision mismatch'); + } + return {record:result,total}; +} +/** Readback is available without a writer; a partial/corrupt tail is reported, never replayed. */ +export async function inspectNodeUpdateJournal(options){ + fields(options,['directory']);const {root}=await directory(options.directory); + try{const value=await latest(root);return {state:'readable',...value};}catch(error){return {state:'unreadable',record:null,errorCode:error.code??'IO_ERROR'};} +} +/** Dedicated operator-owned directory outside the workspace. Records are bounded and append-only. */ +export async function createNodeUpdateJournal(options){ + fields(options,['directory','workspaceRoot'],['recoverStaleLock']); + const workspace=await directory(options.workspaceRoot); + if(typeof options.directory!=='string'||!path.isAbsolute(options.directory))fail('INVALID','Expected an absolute journal directory'); + const prospective=path.join(await realpath(path.dirname(options.directory)),path.basename(options.directory)); + const proposedRelative=path.relative(workspace.root,prospective); + if(proposedRelative===''||proposedRelative!=='..'&&!proposedRelative.startsWith('..'+path.sep)&&!path.isAbsolute(proposedRelative))fail('INVALID','Keep the update journal outside the workspace'); + try{await mkdir(options.directory);}catch(error){if(error.code!=='EEXIST')throw error;} + const binding=await directory(options.directory),root=binding.root; + const relative=path.relative(workspace.root,root); + if(relative===''||relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative))fail('INVALID','Keep the update journal outside the workspace'); + const lock=path.join(root,'.writer.lock'),recovery=path.join(root,'.recovery.lock'); + const owner={pid:process.pid,host:hostname(),token:randomUUID()};let closed=false,busy=false; + const exclusive=async(file,bytes)=>{const handle=await open(file,constants.O_CREAT|constants.O_EXCL|constants.O_WRONLY|(constants.O_NOFOLLOW??0),0o600);try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}}; + const acquire=async()=>{try{await lstat(recovery);fail('CONFLICT','Journal recovery is already in progress');}catch(error){if(error.code!=='ENOENT')throw error;}await exclusive(lock,canonical(owner));}; + try{await acquire();}catch(error){ + if(error.code!=='EEXIST'||options.recoverStaleLock!==true)throw error; + await exclusive(recovery,canonical(owner)); + try{ + const old=decode(await read(lock,2048));fields(old,['pid','host','token']); + if(old.host!==hostname()||!Number.isSafeInteger(old.pid)||old.pid<1)fail('CONFLICT','Cannot prove stale lock ownership'); + let dead=false;try{process.kill(old.pid,0);}catch(probe){dead=probe.code==='ESRCH';} + if(!dead)fail('CONFLICT','Writer may still be alive'); + const again=decode(await read(lock,2048));if(canonical(again)!==canonical(old))fail('CONFLICT','Writer changed'); + await unlink(lock);await exclusive(lock,canonical(owner)); + }finally{await unlink(recovery);} + } + const check=async()=>{ + if(closed)fail('DISPOSED','Journal closed');const next=await directory(root);if(!same(next.stat,binding.stat))fail('CONFLICT','Journal directory changed'); + if(canonical(decode(await read(lock,2048)))!==canonical(owner))fail('CONFLICT','Journal ownership changed'); + }; + try{await latest(root);}catch(error){await unlink(lock);throw error;} + return { + async read(){await check();return clone((await latest(root)).record);}, + async compareAndSwap(expected,next){ + if(busy)fail('CONFLICT','Concurrent journal write');busy=true; + try{await check();const current=await latest(root);if((current.record?.revision??0)!==expected||next.revision!==expected+1)fail('CONFLICT','Journal revision changed');record(next);if(next.revision>MAX_RECORDS)fail('LIMIT','Archive this journal before starting a new one');const bytes=canonical({schemaVersion:1,sha256:digest(canonical(next,LIMIT)),record:next},LIMIT);if(current.total+Buffer.byteLength(bytes)>MAX_TOTAL)fail('LIMIT','Journal byte budget exceeded');await exclusive(path.join(root,`${String(next.revision).padStart(6,'0')}.json`),bytes);if(process.platform!=='win32'){const handle=await open(root,constants.O_RDONLY);try{await handle.sync();}finally{await handle.close();}}return clone(next);}finally{busy=false;} + }, + async close(){if(closed)return;if(busy)fail('CONFLICT','Journal write running');await check();await unlink(lock);closed=true;}, + }; +} diff --git a/src/updates.d.mts b/src/updates.d.mts new file mode 100644 index 0000000..8d5e840 --- /dev/null +++ b/src/updates.d.mts @@ -0,0 +1,22 @@ +import type {ProvenanceEnvelope,ProvenancePolicy} from './provenance.mjs'; +export interface UpdateArtifact {readonly bundle:Uint8Array;readonly envelope:ProvenanceEnvelope|null;} +export interface UpdateDescriptor {readonly schemaVersion:1;readonly bundleSha256:string;readonly name:string;readonly publisher:string;readonly pluginId:string;readonly version:string;readonly api:Readonly>;readonly grants:readonly string[];readonly dependencies:readonly {readonly name:string;readonly version:string;readonly sha256:string;readonly source:string}[];readonly tools:readonly unknown[];readonly licenses:readonly Readonly>[];readonly settingsSha256:string;readonly provenanceSha256:string;readonly keyId:string|null;} +export interface UpdateMigration {readonly id:string;readonly reversible:boolean;} +export interface PluginUpdatePlan {readonly schemaVersion:1;readonly current:UpdateDescriptor;readonly candidate:UpdateDescriptor;readonly migration:UpdateMigration;readonly jobPolicy:'wait'|'retain-old';readonly changes:Readonly>;readonly externalEffectsUndone:false;readonly sha256:string;} +export interface PluginUpdateRecord {readonly schemaVersion:1;readonly revision:number;readonly activeSha256:string;readonly settingsSha256:string;readonly phase:'idle'|'staged'|'approved'|'preparing'|'activated'|'failed';readonly plan:PluginUpdatePlan|null;readonly disposition:string;} +export interface PluginUpdateJournal {read():Promise;compareAndSwap(expectedRevision:number,next:PluginUpdateRecord):Promise;} +export interface UpdateHost {execute(input:unknown,context:{readonly settings:unknown;readonly signal?:AbortSignal;readonly pluginSha256:string}):unknown|Promise;dispose():void|Promise;} +export interface PluginUpdateController { + inspect():{record:PluginUpdateRecord;current:UpdateDescriptor;pending:{plan:PluginUpdatePlan;approved:boolean}|null;jobs:number;retired:{sha256:string;jobs:number}[];disposed:boolean;uncertain:boolean}; + stage(input:{readonly candidate:UpdateArtifact;readonly nextSettings:unknown;readonly migration:UpdateMigration;readonly jobPolicy:'wait'|'retain-old'}):Promise; + approve(input:{readonly sha256:string;readonly grants:readonly string[];readonly keyId:string|null;readonly approved:true}):Promise; + activate(input:{readonly sha256:string;readonly signal?:AbortSignal}):Promise; + execute(input:unknown,options?:{readonly signal?:AbortSignal}):Promise<{pluginSha256:string;result:unknown}>; + replaceSettings(expectedSha256:string,nextSettings:unknown):Promise; + discard():Promise;dispose():Promise; +} +export function createPluginUpdatePlan(input:{readonly current:UpdateArtifact;readonly candidate:UpdateArtifact;readonly settings:unknown;readonly nextSettings:unknown;readonly migration:UpdateMigration;readonly jobPolicy:'wait'|'retain-old'}):PluginUpdatePlan; +export function parsePluginUpdatePlan(value:unknown):PluginUpdatePlan; +export function createMemoryUpdateJournal():PluginUpdateJournal; +export function reconcilePluginUpdateJournal(journal:PluginUpdateJournal,input:{readonly expectedRevision:number;readonly activeSha256:string;readonly settingsSha256:string;readonly approved:true}):Promise; +export function createPluginUpdateController(options:{readonly initial:UpdateArtifact;readonly initialHost:UpdateHost;readonly settings:unknown;readonly getPolicy:(descriptor:UpdateDescriptor,envelope:ProvenanceEnvelope|null)=>ProvenancePolicy|Promise;readonly prepare:(bundle:Uint8Array,context:{readonly settings:unknown;readonly grants:readonly string[];readonly signal?:AbortSignal})=>UpdateHost|Promise;readonly journal:PluginUpdateJournal}):Promise; diff --git a/src/updates.mjs b/src/updates.mjs new file mode 100644 index 0000000..2d8b751 --- /dev/null +++ b/src/updates.mjs @@ -0,0 +1,154 @@ +import {verifyPluginBundle} from './bundles.mjs'; +import {inspectPluginArchiveBytes} from './publishing.mjs'; +import {verifyPluginProvenance} from './provenance.mjs'; +import {base64,canonical,clone,digest,fail,fields,sha,string} from './release-internals.mjs'; + +const hash=value=>digest(canonical(value,1024*1024)); +function snapshot(artifact){ + fields(artifact,['bundle','envelope']); + if(!(artifact.bundle instanceof Uint8Array))fail('INVALID','Expected bundle bytes'); + return {bundle:Uint8Array.from(artifact.bundle),envelope:artifact.envelope===null?null:clone(artifact.envelope)}; +} +function describe(artifact,settings){ + const receipt=verifyPluginBundle(artifact.bundle); + const value=JSON.parse(Buffer.from(artifact.bundle).toString('utf8')); + const inspected=inspectPluginArchiveBytes(base64(value.plugin.archive,12*1024*1024),value.plugin.metadata,{expectedSha256:value.plugin.sha256}); + const manifest=inspected.manifest; + const disclosure=JSON.parse(Buffer.from(inspected.files.find(f=>f.path==='disclosure.json').data).toString('utf8')); + return {schemaVersion:1,bundleSha256:receipt.sha256,name:`@${manifest.publisher}/${manifest.id}`,publisher:manifest.publisher,pluginId:manifest.id,version:manifest.version,api:{protocolVersion:manifest.protocolVersion,capabilities:[...manifest.capabilities].sort(),runtime:manifest.runtime??'ui',supportedHosts:manifest.supportedHosts??[]},grants:[...manifest.permissions].sort(),dependencies:receipt.dependencies.map(p=>({name:p.name,version:p.version,sha256:p.sha256,source:p.source})),tools:disclosure.backends,licenses:[{name:`@${manifest.publisher}/${manifest.id}`,license:manifest.license},...receipt.dependencies.map(p=>({name:p.name,license:p.license,licenseFile:p.licenseFile,noticeFile:p.noticeFile}))],settingsSha256:hash(settings),provenanceSha256:hash(artifact.envelope),keyId:artifact.envelope?.keyId??null}; +} +function planBody(current,candidate,migration,jobPolicy){ + for(const value of [current,candidate]){ + fields(value,['schemaVersion','bundleSha256','name','publisher','pluginId','version','api','grants','dependencies','tools','licenses','settingsSha256','provenanceSha256','keyId']); + if(value.schemaVersion!==1)fail('UNSUPPORTED','Unsupported update descriptor'); + for(const key of ['bundleSha256','settingsSha256','provenanceSha256'])sha(value[key]); + for(const key of ['name','publisher','pluginId','version'])string(value[key],key,256); + if(value.keyId!==null)string(value.keyId,'key ID',128); + for(const key of ['grants','dependencies','tools','licenses'])if(!Array.isArray(value[key])||value[key].length>64)fail('LIMIT','Invalid update descriptor list'); + fields(value.api,['protocolVersion','capabilities','runtime','supportedHosts']); + if(value.api.protocolVersion!==1||!Array.isArray(value.api.capabilities)||value.api.capabilities.length>64||!Array.isArray(value.api.supportedHosts)||value.api.supportedHosts.length>8)fail('INVALID','Invalid update API descriptor'); + for(const grant of value.grants)string(grant,'grant',128); + } + fields(migration,['id','reversible']);string(migration.id,'migration ID',128);if(typeof migration.reversible!=='boolean')fail('INVALID','Migration reversibility required'); + if(!['wait','retain-old'].includes(jobPolicy))fail('INVALID','Select wait or retain-old job policy'); + if(current.name!==candidate.name)fail('CONFLICT','Updates must preserve publisher and plugin identity'); + const changes=Object.fromEntries(['version','api','grants','dependencies','tools','licenses','settingsSha256','provenanceSha256','keyId'].map(key=>[key,canonical(current[key])!==canonical(candidate[key])])); + return {schemaVersion:1,current,candidate,migration:clone(migration),jobPolicy,changes,externalEffectsUndone:false}; +} +/** Pure byte-bound review plan; provenance acceptance is refreshed by the controller. */ +export function createPluginUpdatePlan(input){ + fields(input,['current','candidate','settings','nextSettings','migration','jobPolicy']); + const current=snapshot(input.current),candidate=snapshot(input.candidate); + const body=planBody(describe(current,input.settings),describe(candidate,input.nextSettings),input.migration,input.jobPolicy); + return {...body,sha256:hash(body)}; +} +export function parsePluginUpdatePlan(value){ + fields(value,['schemaVersion','current','candidate','migration','jobPolicy','changes','externalEffectsUndone','sha256']); + const body=planBody(value.current,value.candidate,value.migration,value.jobPolicy); + if(value.schemaVersion!==1||value.externalEffectsUndone!==false||hash(body)!==sha(value.sha256)||canonical(body.changes)!==canonical(value.changes))fail('INTEGRITY','Update plan digest mismatch'); + return clone(value); +} +export function createMemoryUpdateJournal(){ + let value=null; + return {async read(){return value===null?null:clone(value);},async compareAndSwap(expected,next){if((value?.revision??0)!==expected)fail('CONFLICT','Journal revision changed');if(next.revision!==expected+1)fail('INVALID','Invalid next revision');canonical(next,1024*1024);value=clone(next);return clone(value);}}; +} +/** Explicit host readback after a crash. No candidate code, settings migration or job is replayed. */ +export async function reconcilePluginUpdateJournal(journal,input){ + fields(input,['expectedRevision','activeSha256','settingsSha256','approved']); + if(input.approved!==true)fail('APPROVAL_REQUIRED','Explicit host readback approval required'); + const record=await journal.read(); + if(!record||record.revision!==input.expectedRevision)fail('CONFLICT','Journal revision changed'); + const allowed=[{sha256:record.activeSha256,settingsSha256:record.settingsSha256},...(record.plan?[{sha256:record.plan.current.bundleSha256,settingsSha256:record.plan.current.settingsSha256},{sha256:record.plan.candidate.bundleSha256,settingsSha256:record.plan.candidate.settingsSha256}]:[])]; + if(!allowed.some(x=>x.sha256===sha(input.activeSha256)&&x.settingsSha256===sha(input.settingsSha256)))fail('CONFLICT','Host readback does not match a recorded state'); + return journal.compareAndSwap(record.revision,{...record,revision:record.revision+1,activeSha256:input.activeSha256,settingsSha256:input.settingsSha256,phase:'idle',plan:null,disposition:'operator-reconciled'}); +} +/** One owning controller per journal. prepare/execute/dispose ports belong to the host operator. */ +export async function createPluginUpdateController(options){ + fields(options,['initial','initialHost','settings','getPolicy','prepare','journal']); + if(typeof options.getPolicy!=='function'||typeof options.prepare!=='function'||typeof options.initialHost?.execute!=='function'||typeof options.initialHost?.dispose!=='function'||typeof options.journal?.read!=='function'||typeof options.journal?.compareAndSwap!=='function')fail('INVALID','Host and journal ports required'); + let current={artifact:snapshot(options.initial),host:options.initialHost,settings:clone(options.settings),jobs:0,retired:false}; + let description=describe(current.artifact,current.settings), pending=null,busy=false,disposed=false,uncertain=false; + let record=await options.journal.read();const retired=new Set(); + const verifyArtifact=async(artifact,desc)=>{ + const policy=await options.getPolicy(clone(desc),artifact.envelope===null?null:clone(artifact.envelope)); + if(policy.publisher!==desc.publisher||canonical(policy.subject)!==canonical({name:desc.name,version:desc.version,sha256:desc.bundleSha256}))fail('CONFLICT','Policy must bind the exact bundle identity'); + const receipt=verifyPluginProvenance(artifact.bundle,artifact.envelope,policy); + if(receipt.policy!=='accepted')fail('PROVENANCE_REJECTED',receipt.reasons.join(', ')); + return receipt; + }; + await verifyArtifact(current.artifact,description); + if(record){ + if(record.schemaVersion!==1||record.activeSha256!==description.bundleSha256||record.settingsSha256!==description.settingsSha256||!['idle','activated','failed'].includes(record.phase))fail('RECOVERY_REQUIRED','Read back the active host and reconcile its journal explicitly'); + }else record=await options.journal.compareAndSwap(0,{schemaVersion:1,revision:1,activeSha256:description.bundleSha256,settingsSha256:description.settingsSha256,phase:'idle',plan:null,disposition:'initialized'}); + const save=async(patch)=>{ + try{const next={...record,...patch,revision:record.revision+1};record=await options.journal.compareAndSwap(record.revision,next);} + catch(error){uncertain=true;throw Object.assign(new Error('Journal write outcome requires explicit readback'),{code:'RECOVERY_REQUIRED',cause:error});} + }; + const enter=()=>{if(disposed)fail('DISPOSED','Update controller disposed');if(uncertain)fail('RECOVERY_REQUIRED','Journal requires readback');if(busy)fail('BUSY','Another update operation is running');busy=true;}; + const cleanup=async(item)=>{if(item.retired&&item.jobs===0){await item.host.dispose();retired.delete(item);}}; + return { + inspect(){return {record:clone(record),current:clone(description),pending:pending?{plan:clone(pending.plan),approved:pending.approved}:null,jobs:current.jobs,retired:[...retired].map(x=>({sha256:describe(x.artifact,x.settings).bundleSha256,jobs:x.jobs})),disposed,uncertain};}, + async stage(input){ + enter();try{ + fields(input,['candidate','nextSettings','migration','jobPolicy']); + if(pending)fail('CONFLICT','Discard the current staged plan before staging another'); + const artifact=snapshot(input.candidate),settings=clone(input.nextSettings); + const plan=createPluginUpdatePlan({current:current.artifact,candidate:artifact,settings:current.settings,nextSettings:settings,migration:input.migration,jobPolicy:input.jobPolicy}); + await verifyArtifact(artifact,plan.candidate); + await save({phase:'staged',plan,disposition:'review-required'}); + pending={artifact,settings,plan,approved:false};return clone(plan); + }finally{busy=false;} + }, + async approve(input){ + enter();try{ + fields(input,['sha256','grants','keyId','approved']); + if(!pending||input.approved!==true||input.sha256!==pending.plan.sha256||input.keyId!==pending.plan.candidate.keyId||canonical(input.grants)!==canonical(pending.plan.candidate.grants))fail('APPROVAL_REQUIRED','Approval must match the reviewed plan, grants and key'); + await verifyArtifact(pending.artifact,pending.plan.candidate); + await save({phase:'approved',disposition:'explicitly-approved'});pending.approved=true;return clone(record); + }finally{busy=false;} + }, + async activate(input){ + enter();let prepared=null; + try{ + fields(input,['sha256'],['signal']); + if(!pending?.approved||input.sha256!==pending.plan.sha256)fail('APPROVAL_REQUIRED','Approve this exact plan before activation'); + if(input.signal?.aborted)fail('CANCELLED','Activation cancelled'); + if(pending.plan.current.bundleSha256!==description.bundleSha256||pending.plan.current.settingsSha256!==hash(current.settings))fail('CONFLICT','Active bytes or settings changed'); + if(pending.plan.jobPolicy==='wait'&¤t.jobs)fail('ACTIVE_JOBS','Wait for the current jobs to settle'); + await verifyArtifact(pending.artifact,pending.plan.candidate); + await save({phase:'preparing',disposition:'activation-pending'}); + try{ + prepared=await options.prepare(Uint8Array.from(pending.artifact.bundle),{settings:clone(pending.settings),grants:[...pending.plan.candidate.grants],signal:input.signal}); + if(typeof prepared?.execute!=='function'||typeof prepared?.dispose!=='function')fail('INVALID','prepare must return a host'); + if(input.signal?.aborted)fail('CANCELLED','Activation cancelled'); + await verifyArtifact(pending.artifact,pending.plan.candidate); + }catch(error){ + await prepared?.dispose?.();prepared=null; + await save({phase:'failed',disposition:'prepare-failed'});pending=null;throw error; + } + const old=current,next=pending; + await save({phase:'activated',activeSha256:next.plan.candidate.bundleSha256,settingsSha256:next.plan.candidate.settingsSha256,disposition:'activated'}); + current={artifact:next.artifact,host:prepared,settings:next.settings,jobs:0,retired:false};prepared=null;description=next.plan.candidate;pending=null; + old.retired=true;retired.add(old); + let cleanupFailed=false;try{await cleanup(old);}catch{cleanupFailed=true;} + return {...clone(record),cleanupFailed,externalEffectsUndone:false}; + }finally{try{if(prepared)await prepared.dispose();}finally{busy=false;}} + }, + async execute(input,{signal}={}){ + if(disposed)fail('DISPOSED','Update controller disposed');if(uncertain)fail('RECOVERY_REQUIRED','Journal requires readback'); + if(busy&&pending?.plan.jobPolicy==='wait')fail('BUSY','Activation is waiting to switch'); + if(signal?.aborted)fail('CANCELLED','Execution cancelled'); + const item=current,pluginSha256=description.bundleSha256;item.jobs++; + try{return {pluginSha256,result:await item.host.execute(input,{settings:clone(item.settings),signal,pluginSha256})};} + finally{item.jobs--;try{await cleanup(item);}catch{/* Readback still identifies completed jobs; disposal errors must not replace their results. */}} + }, + async replaceSettings(expectedSha256,nextSettings){ + enter();try{if(sha(expectedSha256)!==description.settingsSha256)fail('CONFLICT','Settings revision changed');const next=clone(nextSettings),nextHash=hash(next);await save({settingsSha256:nextHash,phase:'idle',plan:null,disposition:'settings-changed'});current.settings=next;description={...description,settingsSha256:nextHash};pending=null;return nextHash;}finally{busy=false;} + }, + async discard(){enter();try{await save({phase:'idle',plan:null,disposition:'discarded'});pending=null;}finally{busy=false;}}, + async dispose(){ + if(disposed)return;if(busy)fail('BUSY','Update operation still running');if(current.jobs||[...retired].some(x=>x.jobs))fail('ACTIVE_JOBS','Wait for jobs; disposal never cancels them implicitly'); + disposed=true;await current.host.dispose();for(const item of retired)await item.host.dispose();retired.clear(); + }, + }; +} diff --git a/src/upstream.d.mts b/src/upstream.d.mts new file mode 100644 index 0000000..445eb5f --- /dev/null +++ b/src/upstream.d.mts @@ -0,0 +1,9 @@ +export interface UpstreamSource {readonly url:string;readonly version:string;readonly sha256:string;} +export interface UpstreamFetch {readonly schemaVersion:1;readonly source:UpstreamSource;readonly observedAt:number;readonly status:'verified'|'unavailable'|'offline'|'unverified'|'digest-mismatch';readonly actualSha256:string|null;readonly bytes:number;readonly redirects:readonly string[];readonly reason:string|null;readonly artifact:Uint8Array|null;} +export interface UpstreamManifest {readonly schemaVersion:1;readonly name:string;readonly version:string;readonly revision:string;readonly api:readonly string[];readonly license:{readonly name:string;readonly text:string;readonly notice:string;readonly redistributable:boolean};readonly files:readonly {readonly path:string;readonly sha256:string}[];} +export interface UpstreamSnapshot {readonly schemaVersion:1;readonly source:UpstreamSource;readonly observedAt:number;readonly manifest:UpstreamManifest;readonly manifestSha256:string;} +export interface UpstreamCandidate {readonly schemaVersion:1;readonly current:UpstreamSnapshot;readonly candidate:UpstreamSnapshot;readonly tagChanged:boolean;readonly api:{readonly added:readonly string[];readonly removed:readonly string[];readonly missingRequired:readonly string[]};readonly files:{readonly added:readonly string[];readonly removed:readonly string[];readonly changed:readonly string[]};readonly licenseChanged:boolean;readonly redistributable:boolean;readonly compatibility:'supported'|'unsupported'|'unverified';readonly functional:{readonly state:'passed'|'failed'|'unverified';readonly detail:string};readonly patch:{readonly expectedSourceSha256:string;readonly replacementSource:UpstreamSource};readonly reviewRequired:true;readonly automaticActions:readonly never[];readonly sha256:string;} +export function fetchUpstreamArtifact(options:{readonly source:UpstreamSource;readonly allowedUrls:readonly string[];readonly enabled:true;readonly offline:boolean;readonly maxBytes:number;readonly timeoutMs:number;readonly allowLoopback?:boolean;readonly signal?:AbortSignal}):Promise; +export function createUpstreamManifest(value:Omit):UpstreamManifest; +export function inspectUpstreamManifest(bytes:Uint8Array,source:UpstreamSource,observedAt:number):UpstreamSnapshot; +export function createUpstreamCandidate(options:{readonly current:UpstreamSnapshot;readonly candidate:UpstreamSnapshot;readonly support:{readonly versions:readonly string[];readonly requiredApi:readonly string[]};readonly check?:(candidate:UpstreamSnapshot,context:{readonly signal?:AbortSignal})=>Promise<{passed:boolean;detail:string}>|{passed:boolean;detail:string};readonly signal?:AbortSignal}):Promise; diff --git a/src/upstream.mjs b/src/upstream.mjs new file mode 100644 index 0000000..cfe3700 --- /dev/null +++ b/src/upstream.mjs @@ -0,0 +1,86 @@ +import {canonical,clone,digest,exactVersion,fail,fields,portablePath,sha,sourceUrl,string} from './release-internals.mjs'; + +function source(value,loopback=false){fields(value,['url','version','sha256']);return {url:sourceUrl(value.url,loopback),version:exactVersion(value.version),sha256:sha(value.sha256)};} +/** Developer-triggered fetch only. Redirect targets must also be exact allowlisted URLs. */ +export async function fetchUpstreamArtifact(options){ + fields(options,['source','allowedUrls','enabled','offline','maxBytes','timeoutMs'],['allowLoopback','signal']); + const target=source(options.source,options.allowLoopback===true); + if(options.enabled!==true)fail('APPROVAL_REQUIRED','Explicit upstream checking configuration is required'); + if(typeof options.offline!=='boolean'||!Array.isArray(options.allowedUrls)||!options.allowedUrls.length||options.allowedUrls.length>32)fail('INVALID','Explicit offline policy and allowlist required'); + const allowed=new Set(options.allowedUrls.map(url=>sourceUrl(url,options.allowLoopback===true))); + if(!allowed.has(target.url))fail('SOURCE_DENIED','Source is outside the configured allowlist'); + if(!Number.isSafeInteger(options.maxBytes)||options.maxBytes<1||options.maxBytes>48*1024*1024||!Number.isSafeInteger(options.timeoutMs)||options.timeoutMs<1||options.timeoutMs>60000)fail('LIMIT','Invalid fetch budget'); + const receipt={schemaVersion:1,source:target,observedAt:Date.now(),status:'unverified',actualSha256:null,bytes:0,redirects:[],reason:null}; + if(options.offline)return {...receipt,status:'offline',reason:'network-disabled',artifact:null}; + const abort=new AbortController(),cancel=()=>abort.abort(); + if(options.signal?.aborted)abort.abort();else options.signal?.addEventListener('abort',cancel,{once:true}); + let timedOut=false;const timer=setTimeout(()=>{timedOut=true;abort.abort();},options.timeoutMs);let response; + try{ + let url=target.url; + for(let n=0;n<=3;n++){ + if(abort.signal.aborted)throw new Error('aborted'); + response=await fetch(url,{redirect:'manual',signal:abort.signal,credentials:'omit',headers:{accept:'application/octet-stream'}}); + if([301,302,303,307,308].includes(response.status)){ + const location=response.headers.get('location');await response.body?.cancel(); + if(!location||n===3)fail('REDIRECT','Invalid or excessive redirects'); + const next=sourceUrl(new URL(location,url).href,options.allowLoopback===true); + if(!allowed.has(next))fail('SOURCE_DENIED','Redirect target outside configured allowlist'); + receipt.redirects.push(next);url=next;continue; + } + break; + } + if(!response.ok){await response.body?.cancel();return {...receipt,status:response.status===404?'unavailable':'unverified',reason:`http-${response.status}`,artifact:null};} + const declared=response.headers.get('content-length'); + if(declared!==null&&(!/^\d+$/.test(declared)||Number(declared)>options.maxBytes))fail('LIMIT','Response exceeds byte budget'); + if(!response.body)fail('INVALID','Missing upstream response body'); + const reader=response.body.getReader(),chunks=[];let length=0; + try{for(;;){const {done,value}=await reader.read();if(done)break;if((length+=value.byteLength)>options.maxBytes)fail('LIMIT','Response exceeds byte budget');chunks.push(Buffer.from(value));}}finally{await reader.cancel().catch(()=>{});reader.releaseLock();} + const bytes=Buffer.concat(chunks,length),actual=digest(bytes); + return {...receipt,observedAt:Date.now(),status:actual===target.sha256?'verified':'digest-mismatch',actualSha256:actual,bytes:length,reason:actual===target.sha256?null:'source-or-tag-changed',artifact:actual===target.sha256?Uint8Array.from(bytes):null}; + }catch(error){return {...receipt,observedAt:Date.now(),status:'unverified',reason:options.signal?.aborted?'cancelled':timedOut?'timeout':error.code??'network-failed',artifact:null};} + finally{clearTimeout(timer);options.signal?.removeEventListener('abort',cancel);await response?.body?.cancel().catch(()=>{});} +} +function manifest(value){ + fields(value,['schemaVersion','name','version','revision','api','license','files']); + if(value.schemaVersion!==1)fail('UNSUPPORTED','Unsupported upstream manifest'); + string(value.name,'upstream name',128);exactVersion(value.version);string(value.revision,'upstream revision',128); + if(!Array.isArray(value.api)||value.api.length>4096||new Set(value.api).size!==value.api.length)fail('LIMIT','Invalid upstream API inventory');for(const api of value.api)string(api,'API name',256); + fields(value.license,['name','text','notice','redistributable']);string(value.license.name,'license',256); + if(typeof value.license.text!=='string'||!value.license.text.trim()||value.license.text.length>128*1024||typeof value.license.notice!=='string'||value.license.notice.length>128*1024||typeof value.license.redistributable!=='boolean')fail('INVALID','License/NOTICE and redistribution decision required'); + if(!Array.isArray(value.files)||value.files.length>4096)fail('LIMIT','Invalid upstream file inventory');const names=new Set(); + for(const file of value.files){fields(file,['path','sha256']);portablePath(file.path);sha(file.sha256);if(names.has(file.path.toLowerCase()))fail('INVALID','Duplicate upstream path');names.add(file.path.toLowerCase());} + return clone(value); +} +/** Adapters publish reviewed metadata in this format. Inventory claims are not a code execution test. */ +export function createUpstreamManifest(value){return manifest({schemaVersion:1,...value});} +export function inspectUpstreamManifest(bytes,declaredSource,observedAt){ + const declared=source(declaredSource,true); + if(!(bytes instanceof Uint8Array)||bytes.byteLength>1024*1024||!Number.isSafeInteger(observedAt)||observedAt<0)fail('LIMIT','Invalid manifest bytes/time'); + if(digest(bytes)!==declared.sha256)fail('INTEGRITY','Upstream source digest mismatch'); + let value;try{value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes));}catch{fail('INVALID','Invalid upstream JSON');} + value=manifest(value);if(value.version!==declared.version)fail('CONFLICT','Upstream version mismatch'); + return {schemaVersion:1,source:declared,observedAt,manifest:value,manifestSha256:digest(canonical(value))}; +} +function checkedSnapshot(value){fields(value,['schemaVersion','source','observedAt','manifest','manifestSha256']);source(value.source,true);const checked=manifest(value.manifest);if(value.schemaVersion!==1||value.source.version!==checked.version||digest(canonical(checked))!==sha(value.manifestSha256)||!Number.isSafeInteger(value.observedAt)||value.observedAt<0)fail('INVALID','Invalid upstream snapshot');return clone(value);} +/** Creates review data only; check is a developer-owned functional adapter test port. */ +export async function createUpstreamCandidate(options){ + fields(options,['current','candidate','support'],['check','signal']); + const current=checkedSnapshot(options.current),candidate=checkedSnapshot(options.candidate); + if(current.manifest.name!==candidate.manifest.name)fail('CONFLICT','Upstream identity changed'); + fields(options.support,['versions','requiredApi']); + for(const key of ['versions','requiredApi'])if(!Array.isArray(options.support[key])||options.support[key].length>4096)fail('LIMIT','Invalid adapter support matrix'); + for(const version of options.support.versions)exactVersion(version);for(const name of options.support.requiredApi)string(name,'API name',256); + const removedApi=current.manifest.api.filter(x=>!candidate.manifest.api.includes(x)),addedApi=candidate.manifest.api.filter(x=>!current.manifest.api.includes(x)); + const oldFiles=new Map(current.manifest.files.map(x=>[x.path,x.sha256])),newFiles=new Map(candidate.manifest.files.map(x=>[x.path,x.sha256])); + const files={added:[...newFiles.keys()].filter(x=>!oldFiles.has(x)).sort(),removed:[...oldFiles.keys()].filter(x=>!newFiles.has(x)).sort(),changed:[...newFiles.keys()].filter(x=>oldFiles.has(x)&&oldFiles.get(x)!==newFiles.get(x)).sort()}; + const missingApi=options.support.requiredApi.filter(x=>!candidate.manifest.api.includes(x)); + let functional={state:'unverified',detail:'No developer adapter check supplied'}; + if(options.signal?.aborted)functional={state:'unverified',detail:'cancelled'}; + else if(options.check){ + try{const result=await options.check(clone(candidate),{signal:options.signal});fields(result,['passed','detail']);if(typeof result.passed!=='boolean')fail('INVALID','Invalid functional check');string(result.detail,'check detail',2048);functional={state:result.passed?'passed':'failed',detail:result.detail};}catch{functional={state:'failed',detail:'Developer adapter check failed'};} + } + if(options.signal?.aborted)functional={state:'unverified',detail:'cancelled'}; + const supported=options.support.versions.includes(candidate.manifest.version)&&!missingApi.length; + const body={schemaVersion:1,current,candidate,tagChanged:current.source.url===candidate.source.url&¤t.source.version===candidate.source.version&¤t.source.sha256!==candidate.source.sha256,api:{added:addedApi.sort(),removed:removedApi.sort(),missingRequired:missingApi.sort()},files,licenseChanged:canonical(current.manifest.license)!==canonical(candidate.manifest.license),redistributable:candidate.manifest.license.redistributable,compatibility:!supported?'unsupported':functional.state==='passed'?'supported':'unverified',functional,patch:{expectedSourceSha256:current.source.sha256,replacementSource:clone(candidate.source)},reviewRequired:true,automaticActions:[]}; + return {...body,sha256:digest(canonical(body,2*1024*1024))}; +} diff --git a/src/version.mjs b/src/version.mjs index f7e8961..3bd3a09 100644 --- a/src/version.mjs +++ b/src/version.mjs @@ -1,3 +1,3 @@ // The release checks compare this value with the package metadata. -export const SDK_VERSION = '1.0.0'; +export const SDK_VERSION = '1.1.0'; export const SDK_PLUGIN_PEER_RANGE = '>=1.0.0 <2.0.0'; diff --git a/tests/fixtures/update-crash.mjs b/tests/fixtures/update-crash.mjs new file mode 100644 index 0000000..baa6779 --- /dev/null +++ b/tests/fixtures/update-crash.mjs @@ -0,0 +1,5 @@ +import {createNodeUpdateJournal} from '../../src/updates-node.mjs'; +const journal=await createNodeUpdateJournal({directory:process.argv[2],workspaceRoot:process.argv[3]}); +await journal.compareAndSwap(0,{schemaVersion:1,revision:1,activeSha256:'a'.repeat(64),settingsSha256:'b'.repeat(64),phase:'preparing',plan:null,disposition:'fixture-before-activation'}); +process.stdout.write('ready\n'); +setInterval(()=>{},1000); diff --git a/tests/release-tools.test.mjs b/tests/release-tools.test.mjs new file mode 100644 index 0000000..342d101 --- /dev/null +++ b/tests/release-tools.test.mjs @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import {mkdtemp,realpath,rm,mkdir,readFile,writeFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {generateKeyPairSync,sign} from 'node:crypto'; +import {spawn} from 'node:child_process'; +import {once} from 'node:events'; +import {createServer} from 'node:http'; +import {createPluginBundle,verifyPluginBundle,installPluginBundle,lockBundleDependency} from '../src/bundles.mjs'; +import {canonicalProvenancePayload,createProvenanceStatement,signPluginProvenance,verifyPluginProvenance} from '../src/provenance.mjs'; +import {createPluginUpdateController,createMemoryUpdateJournal,parsePluginUpdatePlan,reconcilePluginUpdateJournal} from '../src/updates.mjs'; +import {createNodeUpdateJournal,inspectNodeUpdateJournal} from '../src/updates-node.mjs'; +import {createUpstreamManifest,inspectUpstreamManifest,fetchUpstreamArtifact,createUpstreamCandidate} from '../src/upstream.mjs'; +import {makeExampleBundle,loadExampleHost,sha256} from '../examples/release-tools/fixture.mjs'; + +async function temp(t){const root=await realpath(await mkdtemp(path.join(tmpdir(),'dds-release-test-')));t.after(async()=>{assert.equal(path.dirname(root),await realpath(tmpdir()));assert.ok(path.basename(root).startsWith('dds-release-test-'));await rm(root,{recursive:true});});return root;} +const unsigned=desc=>({subject:{name:desc.name,version:desc.version,sha256:desc.bundleSha256},publisher:desc.publisher,trustRoots:[],now:1000,revocations:{observedAt:1000,revokedKeyIds:[]},maxRevocationAgeMs:100,offline:true,allowUnsigned:true}); +const dummyHost=(value=1)=>({execute:async()=>value,dispose:async()=>{}}); +const policyFor=(bytes,key)=>({subject:{name:'@example/unit',version:'1.0.0',sha256:sha256(bytes)},publisher:'example',trustRoots:[{keyId:'key-1',publisher:'example',publicKey:key,validFrom:100,validUntil:5000,revokedAt:null}],now:1000,revocations:{observedAt:900,revokedKeyIds:[]},maxRevocationAgeMs:500,offline:true,allowUnsigned:false}); + +test('bundle lock and bytes are deterministic; offline installation executes a real dependency through SDK host',async t=>{ + const root=await temp(t),fixture=await makeExampleBundle(path.join(root,'source')); + assert.deepEqual(createPluginBundle(fixture.input),fixture.bundle); + const receipt=verifyPluginBundle(fixture.bundle,{expectedSha256:sha256(fixture.bundle)}); + assert.equal(receipt.executionAuthorized,false);assert.equal(receipt.sbom.dependencies.length,2); + assert.equal(receipt.dependencies[0].noticeFile,'NOTICE'); + const host=await loadExampleHost(fixture.bundle,path.join(root,'installed')); + try{assert.deepEqual(await host.execute({value:4}),{value:9,version:'1.0.0'});}finally{host.dispose();} + await assert.rejects(installPluginBundle(fixture.bundle,{destination:path.join(root,'installed'),expectedSha256:receipt.sha256,approved:true}),{code:'EEXIST'}); + await assert.rejects(installPluginBundle(fixture.bundle,{destination:path.join(root,'denied'),expectedSha256:receipt.sha256,approved:false}),{code:'APPROVAL_REQUIRED'}); +}); + +test('bundle rejects changed root/dependency/SBOM bytes and invalid lock graphs',async t=>{ + const root=await temp(t),fixture=await makeExampleBundle(path.join(root,'source')); + const change=(mutate)=>{const input=structuredClone(fixture.input);mutate(input);assert.throws(()=>createPluginBundle(input));}; + change(x=>x.plugin.sha256='0'.repeat(64)); + change(x=>x.packages[0].files[0].data=Buffer.from('changed').toString('base64')); + change(x=>x.dependencies['dds-example-math']='^1.0.0'); + change(x=>x.dependencies['dds-example-math']='2.0.0'); + change(x=>x.dependencies.missing='1.0.0'); + change(x=>x.packages.push(x.packages[0])); + change(x=>x.dependencies={}); + change(x=>{const {sha256,...pkg}=x.packages[0];pkg.dependencies={'dds-example-math':'1.0.0'};x.packages[0]=lockBundleDependency(pkg);}); + const value=JSON.parse(Buffer.from(fixture.bundle));value.sbom.version=2; + assert.throws(()=>verifyPluginBundle(Buffer.from(JSON.stringify(value)))); + assert.throws(()=>verifyPluginBundle(fixture.bundle,{expectedSha256:'0'.repeat(64)})); +}); + +test('dependency file inventory rejects missing rights, paths, links/scripts metadata and limits',async t=>{ + const root=await temp(t),fixture=await makeExampleBundle(path.join(root,'source')); + const {sha256,...base}=fixture.input.packages[0]; + for(const file of ['../outside','/absolute','folder\\file','NUL','trailing.','node_modules/hidden.mjs','package.json']){ + assert.throws(()=>lockBundleDependency({...base,files:[...base.files,{path:file,data:'eA=='}]})); + } + assert.throws(()=>lockBundleDependency({...base,files:base.files.filter(x=>x.path!=='LICENSE')})); + assert.throws(()=>lockBundleDependency({...base,redistributable:false})); + assert.throws(()=>lockBundleDependency({...base,scripts:{install:'unsupported'}})); + assert.throws(()=>lockBundleDependency({...base,files:[...base.files,{path:'linked',data:'eA==',type:'symlink'}]})); + assert.throws(()=>lockBundleDependency({...base,files:new Array(1025).fill(base.files[0])})); + assert.throws(()=>lockBundleDependency({...base,files:[...base.files,{path:'folder',data:'eA=='},{path:'folder/a',data:'eA=='}]})); + assert.throws(()=>lockBundleDependency({...base,files:[...base.files,{path:'license',data:'eA=='}]})); +}); + +test('independently signed literal payload verifies, while wrong bytes/key/subject/publisher/algorithm do not',()=>{ + const bytes=Buffer.from('public fixture'),pair=generateKeyPairSync('ed25519'),other=generateKeyPairSync('ed25519'); + const policy=policyFor(bytes,pair.publicKey); + const payload=createProvenanceStatement({subject:policy.subject,publisher:'example',source:{url:'https://example.com/source',revision:'abc'},issuedAt:500,expiresAt:2000}); + const literal='DDS-PROVENANCE-V1\n'+`{"expiresAt":2000,"issuedAt":500,"publisher":"example","schemaVersion":1,"source":{"revision":"abc","url":"https://example.com/source"},"subject":{"name":"@example/unit","sha256":"${sha256(bytes)}","version":"1.0.0"}}`; + assert.equal(Buffer.from(canonicalProvenancePayload(payload)).toString(),literal); + const envelope={schemaVersion:1,algorithm:'Ed25519',keyId:'key-1',payload,signature:sign(null,Buffer.from(literal),pair.privateKey).toString('base64')}; + assert.deepEqual(signPluginProvenance(payload,{keyId:'key-1',privateKey:pair.privateKey}),envelope); + const receipt=verifyPluginProvenance(bytes,envelope,policy);assert.equal(receipt.policy,'accepted');assert.equal(receipt.executionAuthorized,false); + assert.equal(verifyPluginProvenance(Buffer.from('changed'),envelope,policy).checksum,false); + assert.equal(verifyPluginProvenance(bytes,envelope,{...policy,subject:{...policy.subject,name:'@example/other'}}).policy,'rejected'); + assert.equal(verifyPluginProvenance(bytes,envelope,{...policy,publisher:'other'}).policy,'rejected'); + assert.equal(verifyPluginProvenance(bytes,envelope,{...policy,trustRoots:[{...policy.trustRoots[0],publicKey:other.publicKey}]}).signature,'invalid'); + assert.equal(verifyPluginProvenance(bytes,{...envelope,algorithm:'unknown'},policy).signature,'unsupported'); + assert.equal(verifyPluginProvenance(bytes,{...envelope,keyId:'new-package-key'},policy).signature,'untrusted-key'); +}); + +test('signature expiry, revocation freshness, rotation, skew and unsigned compatibility remain explicit',()=>{ + const bytes=Buffer.from('fixture'),pair=generateKeyPairSync('ed25519'),policy=policyFor(bytes,pair.publicKey); + const payload=createProvenanceStatement({subject:policy.subject,publisher:'example',source:{url:'https://example.com/source',revision:'abc'},issuedAt:500,expiresAt:2000}); + const envelope=signPluginProvenance(payload,{keyId:'key-1',privateKey:pair.privateKey}); + for(const overrides of [{now:2000},{now:400},{revocations:{observedAt:0,revokedKeyIds:[]}},{revocations:{observedAt:900,revokedKeyIds:['key-1']}},{trustRoots:[{...policy.trustRoots[0],revokedAt:900}]},{trustRoots:[{...policy.trustRoots[0],validUntil:900}]}])assert.equal(verifyPluginProvenance(bytes,envelope,{...policy,...overrides}).policy,'rejected'); + assert.equal(verifyPluginProvenance(bytes,envelope,{...policy,now:450,clockSkewMs:100,revocations:{observedAt:400,revokedKeyIds:[]}}).policy,'accepted'); + const rotated=signPluginProvenance(payload,{keyId:'key-2',privateKey:pair.privateKey}); + assert.equal(verifyPluginProvenance(bytes,rotated,{...policy,trustRoots:[...policy.trustRoots,{...policy.trustRoots[0],keyId:'key-2'}]}).policy,'accepted'); + assert.equal(verifyPluginProvenance(bytes,null,policy).policy,'rejected'); + const unsigned=verifyPluginProvenance(bytes,null,{...policy,allowUnsigned:true});assert.equal(unsigned.policy,'accepted');assert.equal(unsigned.publisher,'unverified');assert.equal(unsigned.signature,'unsigned'); +}); + +async function updateFixture(t,overrides={}){ + const root=await temp(t),before=await makeExampleBundle(path.join(root,'before')),after=await makeExampleBundle(path.join(root,'after'),'1.1.0',2); + const current={bundle:before.bundle,envelope:null},candidate={bundle:after.bundle,envelope:null}; + const journal=createMemoryUpdateJournal();const controller=await createPluginUpdateController({initial:current,initialHost:dummyHost(1),settings:{value:1},getPolicy:unsigned,prepare:()=>dummyHost(2),journal,...overrides}); + t.after(()=>controller.dispose()); + return {root,current,candidate,controller,journal}; +} +const stage=async(f,jobPolicy='wait')=>f.controller.stage({candidate:f.candidate,nextSettings:{value:2},migration:{id:'settings-v2',reversible:true},jobPolicy}); +const approve=(controller,plan)=>controller.approve({sha256:plan.sha256,grants:plan.candidate.grants,keyId:plan.candidate.keyId,approved:true}); + +test('plan detects API/grants/dependencies/license/settings/key changes, binds bytes and requires exact approval',async t=>{ + const f=await updateFixture(t),plan=await stage(f);assert.deepEqual(parsePluginUpdatePlan(plan),plan);assert.equal(plan.changes.version,true);assert.equal(plan.changes.settingsSha256,true);assert.equal(plan.changes.grants,false); + assert.throws(()=>parsePluginUpdatePlan({...plan,changes:{...plan.changes,grants:true}})); + await assert.rejects(f.controller.activate({sha256:plan.sha256}),{code:'APPROVAL_REQUIRED'}); + await assert.rejects(approve(f.controller,{...plan,sha256:'0'.repeat(64)}),{code:'APPROVAL_REQUIRED'}); + await assert.rejects(f.controller.approve({sha256:plan.sha256,grants:['workspace.read'],keyId:null,approved:true}),{code:'APPROVAL_REQUIRED'}); + await approve(f.controller,plan);await f.controller.activate({sha256:plan.sha256});assert.equal((await f.controller.execute({})).result,2); + const rollback=await f.controller.stage({candidate:f.current,nextSettings:{value:1},migration:{id:'restore',reversible:true},jobPolicy:'wait'});await approve(f.controller,rollback);assert.equal((await f.controller.activate({sha256:rollback.sha256})).externalEffectsUndone,false); +}); + +test('settings changes invalidate approval; preparation failure preserves the active host',async t=>{ + const f=await updateFixture(t,{prepare:()=>{throw Error('migration cannot prepare');}});const plan=await stage(f);await approve(f.controller,plan); + await f.controller.replaceSettings(f.controller.inspect().current.settingsSha256,{value:3});await assert.rejects(f.controller.activate({sha256:plan.sha256}),{code:'APPROVAL_REQUIRED'}); + const retry=await stage(f);await approve(f.controller,retry);await assert.rejects(f.controller.activate({sha256:retry.sha256}),/migration/);assert.equal((await f.controller.execute({})).result,1);assert.equal(f.controller.inspect().record.phase,'failed'); +}); + +test('old execution leases survive retain-old updates; wait and disposal never cancel active jobs',async t=>{ + let finish,oldDisposed=0;const gate=new Promise(resolve=>{finish=resolve;}); + const f=await updateFixture(t,{initialHost:{execute:()=>gate,dispose:()=>{oldDisposed++;}}}); + const running=f.controller.execute({});const blocked=await stage(f);await approve(f.controller,blocked); + await assert.rejects(f.controller.activate({sha256:blocked.sha256}),{code:'ACTIVE_JOBS'});await assert.rejects(f.controller.dispose(),{code:'ACTIVE_JOBS'}); + await f.controller.discard();const keep=await stage(f,'retain-old');await approve(f.controller,keep);await f.controller.activate({sha256:keep.sha256});assert.equal(oldDisposed,0); + finish(7);assert.equal((await running).pluginSha256,sha256(f.current.bundle));assert.equal(oldDisposed,1);assert.equal((await f.controller.execute({})).pluginSha256,sha256(f.candidate.bundle)); +}); + +test('activation rechecks current policy and rejects concurrent requests or cancelled preparation',async t=>{ + let denied=false,finish;const gate=new Promise(resolve=>{finish=resolve;}); + const f=await updateFixture(t,{getPolicy:desc=>({...unsigned(desc),allowUnsigned:!denied}),prepare:async()=>{await gate;return dummyHost(2);}}); + const plan=await stage(f);await approve(f.controller,plan);denied=true;await assert.rejects(f.controller.activate({sha256:plan.sha256}),{code:'PROVENANCE_REJECTED'});denied=false; + const abort=new AbortController(),pending=f.controller.activate({sha256:plan.sha256,signal:abort.signal}); + await assert.rejects(f.controller.activate({sha256:plan.sha256}),{code:'BUSY'});abort.abort();finish();await assert.rejects(pending,{code:'CANCELLED'});assert.equal((await f.controller.execute({})).result,1); +}); + +test('uncertain journal acknowledgement blocks activation and requires explicit readback',async t=>{ + const journal=createMemoryUpdateJournal();let inject=false; + const wrapper={read:()=>journal.read(),async compareAndSwap(expected,next){const result=await journal.compareAndSwap(expected,next);if(inject&&next.phase==='activated')throw Error('ack lost');return result;}}; + const f=await updateFixture(t,{journal:wrapper});const plan=await stage(f);await approve(f.controller,plan);inject=true; + await assert.rejects(f.controller.activate({sha256:plan.sha256}),{code:'RECOVERY_REQUIRED'});assert.equal(f.controller.inspect().uncertain,true);await assert.rejects(f.controller.execute({}),{code:'RECOVERY_REQUIRED'}); + const recorded=await journal.read();assert.equal(recorded.activeSha256,plan.candidate.bundleSha256); + const reconciled=await reconcilePluginUpdateJournal(journal,{expectedRevision:recorded.revision,activeSha256:plan.current.bundleSha256,settingsSha256:plan.current.settingsSha256,approved:true});assert.equal(reconciled.phase,'idle'); +}); + +test('Node journal persists exact CAS state and prevents multiple writers or workspace placement',async t=>{ + const root=await temp(t),workspace=path.join(root,'workspace');await mkdir(workspace); + await assert.rejects(createNodeUpdateJournal({directory:path.join(workspace,'state'),workspaceRoot:workspace}),{code:'INVALID'}); + const options={directory:path.join(root,'state'),workspaceRoot:workspace};const journal=await createNodeUpdateJournal(options); + await assert.rejects(createNodeUpdateJournal(options),{code:'EEXIST'}); + const record={schemaVersion:1,revision:1,activeSha256:'a'.repeat(64),settingsSha256:'b'.repeat(64),phase:'idle',plan:null,disposition:'fixture'}; + await journal.compareAndSwap(0,record);await assert.rejects(journal.compareAndSwap(0,record),{code:'CONFLICT'});await journal.close(); + const reopened=await createNodeUpdateJournal(options);assert.deepEqual(await reopened.read(),record);await reopened.close(); + await writeFile(path.join(options.directory,'000002.json'),'partial');assert.equal((await inspectNodeUpdateJournal({directory:options.directory})).state,'unreadable'); +}); + +test('real child termination leaves a preparing record and never replays candidate activation', {timeout:15000},async t=>{ + const root=await temp(t),workspace=path.join(root,'workspace'),directory=path.join(root,'state');await mkdir(workspace); + const child=spawn(process.execPath,[new URL('./fixtures/update-crash.mjs',import.meta.url).pathname.replace(/^\/([A-Za-z]:)/,'$1'),directory,workspace],{stdio:['ignore','pipe','pipe']}); + const ready=once(child.stdout,'data');const exited=once(child,'exit');t.after(()=>{if(child.exitCode===null)child.kill();});await ready;child.kill();await exited; + const journal=await createNodeUpdateJournal({directory,workspaceRoot:workspace,recoverStaleLock:true});try{const record=await journal.read();assert.equal(record.phase,'preparing');assert.equal(record.disposition,'fixture-before-activation');const next=await reconcilePluginUpdateJournal(journal,{expectedRevision:record.revision,activeSha256:record.activeSha256,settingsSha256:record.settingsSha256,approved:true});assert.equal(next.phase,'idle');}finally{await journal.close();} +}); + +function upstream(version,api=['run'],license='Apache-2.0'){const value=createUpstreamManifest({name:'example',version,revision:'r'+version,api,license:{name:license,text:'Example license text',notice:'Example NOTICE',redistributable:true},files:[{path:'index.mjs',sha256:sha256(Buffer.from(version))}]});const bytes=Buffer.from(JSON.stringify(value));return {bytes,source:{url:'https://example.com/release',version,sha256:sha256(bytes)}};} + +test('upstream comparison distinguishes supported, unsupported, unverified and failed candidates',async()=>{ + const a=upstream('1.0.0'),b=upstream('1.1.0',['run','extra'],'MIT'),current=inspectUpstreamManifest(a.bytes,a.source,100),candidate=inspectUpstreamManifest(b.bytes,b.source,200),support={versions:['1.1.0'],requiredApi:['run']}; + let report=await createUpstreamCandidate({current,candidate,support});assert.equal(report.compatibility,'unverified');assert.equal(report.licenseChanged,true);assert.deepEqual(report.api.added,['extra']);assert.deepEqual(report.files.changed,['index.mjs']); + report=await createUpstreamCandidate({current,candidate,support,check:()=>({passed:true,detail:'Functional adapter fixture'})});assert.equal(report.compatibility,'supported');assert.equal(report.reviewRequired,true);assert.deepEqual(report.automaticActions,[]); + assert.equal((await createUpstreamCandidate({current,candidate,support:{...support,requiredApi:['missing']},check:()=>({passed:true,detail:'fixture'})})).compatibility,'unsupported'); + assert.equal((await createUpstreamCandidate({current,candidate,support,check:()=>{throw Error('failed');}})).functional.state,'failed'); + const moved=upstream('1.0.0',['run','moved']);assert.equal((await createUpstreamCandidate({current,candidate:inspectUpstreamManifest(moved.bytes,moved.source,300),support})).tagChanged,true); + assert.throws(()=>inspectUpstreamManifest(a.bytes,{...a.source,version:'9.0.0'},100)); +}); + +test('explicit upstream fetch enforces byte/time/source/offline budgets and detects tag drift/missing releases',async t=>{ + const artifact=upstream('1.1.0');let hits=0;const server=createServer((req,res)=>{hits++;if(req.url==='/missing'){res.writeHead(404).end();return;}if(req.url==='/slow'){setTimeout(()=>res.end(artifact.bytes),200);return;}if(req.url==='/redirect'){res.writeHead(302,{location:'/outside'}).end();return;}res.end(artifact.bytes);});server.listen(0,'127.0.0.1');await once(server,'listening');t.after(async()=>{server.closeAllConnections();await new Promise(resolve=>server.close(resolve));}); + const url=`http://127.0.0.1:${server.address().port}/release`,options={source:{...artifact.source,url},allowedUrls:[url],enabled:true,offline:false,maxBytes:4096,timeoutMs:1000,allowLoopback:true}; + assert.equal((await fetchUpstreamArtifact(options)).status,'verified'); + const before=hits;assert.equal((await fetchUpstreamArtifact({...options,offline:true})).status,'offline');assert.equal(hits,before); + assert.equal((await fetchUpstreamArtifact({...options,source:{...options.source,sha256:'0'.repeat(64)}})).status,'digest-mismatch'); + assert.equal((await fetchUpstreamArtifact({...options,maxBytes:4})).reason,'LIMIT'); + for(const [route,expected] of [['missing','http-404'],['slow','timeout'],['redirect','SOURCE_DENIED']]){const target=url.replace('/release','/'+route);assert.equal((await fetchUpstreamArtifact({...options,source:{...options.source,url:target},allowedUrls:[target],timeoutMs:route==='slow'?10:1000})).reason,expected);} + await assert.rejects(fetchUpstreamArtifact({...options,enabled:false}),{code:'APPROVAL_REQUIRED'}); + await assert.rejects(fetchUpstreamArtifact({...options,allowedUrls:[url+'/other']}),{code:'SOURCE_DENIED'}); + const abort=new AbortController();abort.abort();assert.equal((await fetchUpstreamArtifact({...options,signal:abort.signal})).reason,'cancelled'); +}); diff --git a/tests/types/release-tools.mts b/tests/types/release-tools.mts new file mode 100644 index 0000000..a1071a3 --- /dev/null +++ b/tests/types/release-tools.mts @@ -0,0 +1,25 @@ +import {createPluginBundle,lockBundleDependency,verifyPluginBundle,installPluginBundle,type PluginBundleInput,type BundleReceipt} from '@altifigence/dds-plugin-sdk/bundles'; +import {createProvenanceStatement,signPluginProvenance,verifyPluginProvenance,type ProvenancePolicy,type ProvenanceKey,type ProvenanceEnvelope} from '@altifigence/dds-plugin-sdk/provenance'; +import {createPluginUpdatePlan,createPluginUpdateController,createMemoryUpdateJournal,parsePluginUpdatePlan,type UpdateArtifact,type PluginUpdatePlan} from '@altifigence/dds-plugin-sdk/updates'; +import {createNodeUpdateJournal,inspectNodeUpdateJournal} from '@altifigence/dds-plugin-sdk/updates-node'; +import {fetchUpstreamArtifact,createUpstreamManifest,inspectUpstreamManifest,createUpstreamCandidate,type UpstreamSource} from '@altifigence/dds-plugin-sdk/upstream'; +declare const bytes:Uint8Array,input:PluginBundleInput,key:ProvenanceKey,policy:ProvenancePolicy,artifact:UpdateArtifact; +const bundle:Uint8Array=createPluginBundle(input),receipt:BundleReceipt=verifyPluginBundle(bundle); +await installPluginBundle(bundle,{destination:'/operator/new',expectedSha256:receipt.sha256,approved:true}); +lockBundleDependency({name:'example',version:'1.0.0',source:'https://example.com/source',entry:'index.mjs',type:'module',dependencies:{},license:'MIT',licenseFile:'LICENSE',noticeFile:null,redistributable:true,files:[]}); +const statement=createProvenanceStatement({subject:policy.subject,publisher:'example',source:{url:'https://example.com/source',revision:'one'},issuedAt:1,expiresAt:2}); +const envelope:ProvenanceEnvelope=signPluginProvenance(statement,{keyId:'operator',privateKey:key});verifyPluginProvenance(bytes,envelope,policy).policy; +const plan:PluginUpdatePlan=createPluginUpdatePlan({current:artifact,candidate:artifact,settings:{},nextSettings:{},migration:{id:'one',reversible:true},jobPolicy:'wait'});parsePluginUpdatePlan(plan); +const controller=await createPluginUpdateController({initial:artifact,initialHost:{execute:()=>null,dispose(){}},settings:{},getPolicy:()=>policy,prepare:()=>({execute:()=>null,dispose(){}}),journal:createMemoryUpdateJournal()}); +await controller.approve({sha256:plan.sha256,grants:plan.candidate.grants,keyId:plan.candidate.keyId,approved:true});await controller.activate({sha256:plan.sha256}); +const nodeJournal=await createNodeUpdateJournal({directory:'/operator/state',workspaceRoot:'/workspace'});await nodeJournal.close();await inspectNodeUpdateJournal({directory:'/operator/state'}); +declare const source:UpstreamSource; +await fetchUpstreamArtifact({source,allowedUrls:[source.url],enabled:true,offline:true,maxBytes:4096,timeoutMs:500}); +createUpstreamManifest({name:'example',version:'1.0.0',revision:'r1',api:['run'],license:{name:'MIT',text:'license',notice:'',redistributable:true},files:[]}); +const snapshot=inspectUpstreamManifest(bytes,source,1);await createUpstreamCandidate({current:snapshot,candidate:snapshot,support:{versions:['1.0.0'],requiredApi:['run']},check:()=>({passed:true,detail:'fixture'})}); +// @ts-expect-error installation must carry an explicit true approval +installPluginBundle(bundle,{destination:'/operator/new',expectedSha256:receipt.sha256,approved:false}); +// @ts-expect-error jobs cannot be implicitly cancelled by an update +createPluginUpdatePlan({current:artifact,candidate:artifact,settings:{},nextSettings:{},migration:{id:'one',reversible:true},jobPolicy:'cancel-all'}); +// @ts-expect-error trust results never authorize execution +const allowed:true=verifyPluginProvenance(bytes,envelope,policy).executionAuthorized;