diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4accd02..920345e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,17 +83,3 @@ jobs: name: distributions path: dist - run: bash tools/check-collector.sh - - reader: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: "1.4.2" - - run: bash tools/check-reader.sh - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: orrery-reader - path: reader-dist/*.tgz - if-no-files-found: error diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 24a857b..e41d87c 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -13,8 +13,8 @@ permissions: # run, and the app may bypass the main ruleset. On that release commit, the # remaining jobs verify, tag, then publish, each only after the one before. # -# PyPI and npm trust this file by name, in the pypi environment, which only -# main can use. Renaming it stops publication. +# PyPI trusts this file by name, in the pypi environment, which only main can +# use. Renaming it stops publication. jobs: versioning: name: Update versioning @@ -76,22 +76,3 @@ jobs: - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 with: packages-dir: dist/ - - publish-reader: - needs: [verify, publish] - runs-on: ubuntu-latest - environment: pypi - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: orrery-reader - path: reader-dist - - run: bash tools/publish-reader.sh diff --git a/.gitignore b/.gitignore index e598fa6..5c2ae59 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,3 @@ dist/ .coverage .collector/ .test-artifacts/ -node_modules/ -reader-dist/ -typescript/orrery/dist/ diff --git a/README.md b/README.md index 44ed9da..f126750 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,6 @@ The host can also start telemetry and graph modules together with `start_service Preserve the selected source files until publication succeeds. `microcosm-emitter-publish-graph` asks the service to retry a publication by ID and can re-enqueue a preserved directory on another machine. It never rewrites the original build receipt; the queue and separate latest-status file record retries. -The existing TypeScript package `@policyengine/microcosm-provider-orrery` reads immutable published graph files on a server or in a browser. It remains separate from the Python distribution and does not change the hosted telemetry API. - ## Development Use Python 3.13 or 3.14 on Linux or macOS. @@ -46,23 +44,22 @@ uv sync --locked bash tools/check-quality.sh uv run --no-sync pytest -m 'not collector' bash tools/check-artifacts.sh -bash tools/check-reader.sh bash tools/check-changelog.sh ``` -Tests isolate ambient credentials and use synthetic identities. The artifact check builds one source archive and wheel, installs that wheel into a clean environment away from the source checkout, verifies import boundaries, and exercises telemetry-only, graph-only and combined service startup with packaged migrations. Reader checks cover tests, formatting, types, build and an isolated tarball installation; they do not inspect visual styling. +Tests isolate ambient credentials and use synthetic identities. The artifact check builds one source archive and wheel, installs that wheel into a clean environment away from the source checkout, verifies import boundaries, and exercises telemetry-only, graph-only and combined service startup with packaged migrations. The collector compatibility job tests the installed wheel against a pinned dashboard collector and disposable PostgreSQL. It covers the HTTP exchange, event ingestion, blocked outcomes and supplied failure codes in dashboard run documents, rejected identities, and duplicate delivery after a lost acknowledgement. Local execution requires that pinned collector checkout and a loopback test database; `tools/check-collector.sh` verifies these inputs. Never point this test at a deployed database. The pin verifies source compatibility, not which revision the hosted collector currently runs. ## Publication -Every merge to `main` releases the Python distribution `microcosm-emitter` to PyPI and the TypeScript reader `@policyengine/microcosm-provider-orrery` to npm at one shared version. Each pull request adds a changelog fragment named `changelog.d/..md`, where the type is `breaking`, `added`, `changed`, `fixed` or `removed`; `tools/check-changelog.sh` refuses a pull request without one. +Every merge to `main` releases the Python distribution `microcosm-emitter` to PyPI. Each pull request adds a changelog fragment named `changelog.d/..md`, where the type is `breaking`, `added`, `changed`, `fixed` or `removed`; `tools/check-changelog.sh` refuses a pull request without one. `push.yaml` runs twice for each merge: -1. On the merge commit, it bumps the version in `pyproject.toml`, both `package.json` files and `uv.lock`: a major bump for a breaking fragment, a minor bump for an added or removed fragment, and a patch bump otherwise. It collects the fragments into `CHANGELOG.md` and commits the result as "Update microcosm-emitter" using the PolicyEngine GitHub App, which may bypass the `main` ruleset. -2. On that commit, it runs every CI check, tags the commit with the bare version (for example `0.2.0`), publishes the tested wheel and source archive to PyPI, and then publishes the tested reader tarball to npm. Each step starts only after the one before succeeds. +1. On the merge commit, it bumps the version in `pyproject.toml` and `uv.lock`: a major bump for a breaking fragment, a minor bump for an added or removed fragment, and a patch bump otherwise. It collects the fragments into `CHANGELOG.md` and commits the result as "Update microcosm-emitter" using the PolicyEngine GitHub App, which may bypass the `main` ruleset. +2. On that commit, it runs every CI check, tags the commit with the bare version (for example `0.2.0`), and publishes the tested wheel and source archive to PyPI. Each step starts only after the one before succeeds. -Both registries accept uploads only through trusted publishing from `push.yaml` in the `pypi` environment, which only `main` can use. CI holds no long-lived registry token. Renaming the workflow or the environment stops publication until both registries' trusted-publisher settings change to match. +PyPI accepts uploads only through trusted publishing from `push.yaml` in the `pypi` environment, which only `main` can use. CI holds no long-lived registry token. Renaming the workflow or the environment stops publication until PyPI's trusted-publisher settings change to match. No hosted-service deployment or consumer activation follows from publishing this package. diff --git a/changelog.d/remove-typescript-reader.removed.md b/changelog.d/remove-typescript-reader.removed.md new file mode 100644 index 0000000..a9661e6 --- /dev/null +++ b/changelog.d/remove-typescript-reader.removed.md @@ -0,0 +1 @@ +Remove the unused TypeScript reader `@policyengine/microcosm-provider-orrery`, so each release now publishes only the `microcosm-emitter` Python distribution to PyPI. diff --git a/package.json b/package.json deleted file mode 100644 index ddb5d92..0000000 --- a/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@policyengine/microcosm-provider-orrery", - "version": "0.2.1", - "private": true, - "type": "module", - "description": "Pinned-source development entry points; publish the built package from typescript/orrery", - "exports": { - "./contract": "./typescript/orrery/src/contract.ts", - "./browser": "./typescript/orrery/src/browser.ts", - "./server": "./typescript/orrery/src/server.ts" - }, - "peerDependencies": { - "@axiom-foundation/orrery": "0.6.1" - } -} diff --git a/tests/test_workflows.py b/tests/test_workflows.py index d475788..30a70dc 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[1] RELEASE_COMMIT = "Update microcosm-emitter" -VERSIONED_FILES = ("pyproject.toml", "package.json", "typescript/orrery/package.json") +VERSIONED_FILE = "pyproject.toml" def workflow_paths(): @@ -30,9 +30,7 @@ def bump_version(): def release_tree(root, *fragments): - for name in VERSIONED_FILES: - (root / name).parent.mkdir(parents=True, exist_ok=True) - shutil.copy(ROOT / name, root / name) + shutil.copy(ROOT / VERSIONED_FILE, root / VERSIONED_FILE) (root / "changelog.d").mkdir() (root / "changelog.d/.gitkeep").touch() for fragment in fragments: @@ -40,6 +38,16 @@ def release_tree(root, *fragments): return root +def remove_version_line(pyproject_text): + return re.sub( + r'^version = "[^"]+"\n', "", pyproject_text, count=1, flags=re.MULTILINE + ) + + +def add_tool_version_line(pyproject_text): + return pyproject_text + '\n[tool.example]\nversion = "9.9.9"\n' + + def test_shell_logic_lives_in_scripts(): for path in workflow_paths(): config = workflow(path.name) @@ -65,7 +73,7 @@ def test_verification_includes_real_collector_and_matrix(): config = workflow("ci.yml") assert set(config["on"]) == {"workflow_call"} jobs = config["jobs"] - assert set(jobs) == {"quality", "tests", "build-packages", "collector", "reader"} + assert set(jobs) == {"quality", "tests", "build-packages", "collector"} assert jobs["tests"]["strategy"]["matrix"] == { "os": ["ubuntu-latest", "macos-latest"], "python": ["3.13", "3.14"], @@ -78,6 +86,7 @@ def test_release_commit_is_verified_then_tagged_then_published(): config = workflow("push.yaml") assert config["on"]["push"]["branches"] == ["main"] jobs = config["jobs"] + assert set(jobs) == {"versioning", "verify", "tag", "publish"} assert f"!= '{RELEASE_COMMIT}'" in jobs["versioning"]["if"] assert jobs["versioning"]["steps"][-2]["run"] == "bash tools/update-version.sh" assert jobs["versioning"]["steps"][-1]["with"]["message"] == RELEASE_COMMIT @@ -86,7 +95,6 @@ def test_release_commit_is_verified_then_tagged_then_published(): assert jobs["tag"]["needs"] == "verify" assert jobs["tag"]["steps"][-1]["run"] == "bash tools/publish-git-tag.sh" assert jobs["publish"]["needs"] == "tag" - assert jobs["publish-reader"]["needs"] == ["verify", "publish"] def test_only_publication_holds_oidc_and_only_versioning_holds_secrets(): @@ -96,29 +104,18 @@ def test_only_publication_holds_oidc_and_only_versioning_holds_secrets(): oidc_jobs = { name for name, job in jobs.items() if "id-token" in job.get("permissions", {}) } - assert oidc_jobs == {"publish", "publish-reader"} - for name in oidc_jobs: - assert jobs[name]["environment"] == "pypi" + assert oidc_jobs == {"publish"} + assert jobs["publish"]["environment"] == "pypi" assert jobs["publish"]["permissions"] == {"id-token": "write"} text = (ROOT / ".github/workflows/push.yaml").read_text() assert set(re.findall(r"secrets\.(\w+)", text)) == {"APP_ID", "APP_PRIVATE_KEY"} assert text.count("secrets.") == 2 -def test_registries_receive_only_verified_artifacts(): - jobs = workflow("push.yaml")["jobs"] - publish = jobs["publish"] +def test_pypi_receives_only_verified_artifacts(): + publish = workflow("push.yaml")["jobs"]["publish"] assert publish["steps"][0]["with"]["name"] == "distributions" assert not any("run" in step for step in publish["steps"]) - reader = jobs["publish-reader"] - download = next( - step for step in reader["steps"] if "download-artifact@" in step.get("uses", "") - ) - assert download["with"]["name"] == "orrery-reader" - assert reader["steps"][-1]["run"] == "bash tools/publish-reader.sh" - publish = (ROOT / "tools/publish-reader.sh").read_text() - # A bare "reader-dist/" is a GitHub shorthand to npm, not a path. - assert 'npm publish --access public --provenance "./${archives[0]}"' in publish @pytest.mark.parametrize( @@ -135,20 +132,21 @@ def test_fragment_types_choose_the_bump(bump_version, types, expected): assert bump_version["next_version"]("1.4.2", types) == expected -def test_release_writes_one_version_to_every_package(tmp_path, bump_version): +def test_release_rewrites_only_the_version_line(tmp_path, bump_version): root = release_tree(tmp_path, "1.added.md", "repair.fixed.md") current, upcoming = bump_version["plan_release"](root) assert upcoming == bump_version["next_version"](current, {"added", "fixed"}) bump_version["write_version"](root, current, upcoming) assert bump_version["current_version"](root) == upcoming - for name in VERSIONED_FILES: - original = (ROOT / name).read_text().splitlines() - updated = (root / name).read_text().splitlines() - assert len(updated) == len(original) - changed = [ - pair for pair in zip(original, updated, strict=True) if pair[0] != pair[1] - ] - assert changed == [(changed[0][0], changed[0][0].replace(current, upcoming))] + original = (ROOT / VERSIONED_FILE).read_text().splitlines() + updated = (root / VERSIONED_FILE).read_text().splitlines() + assert len(updated) == len(original) + changed = [ + (original_line, updated_line) + for original_line, updated_line in zip(original, updated, strict=True) + if original_line != updated_line + ] + assert changed == [(f'version = "{current}"', f'version = "{upcoming}"')] @pytest.mark.parametrize( @@ -162,13 +160,18 @@ def test_release_refuses_missing_or_untyped_fragments( bump_version["plan_release"](root) -def test_release_refuses_diverged_package_versions(tmp_path, bump_version): +@pytest.mark.parametrize( + "edit_pyproject", + [remove_version_line, add_tool_version_line], + ids=["missing", "duplicated"], +) +def test_release_refuses_a_missing_or_duplicated_version_line( + tmp_path, bump_version, edit_pyproject +): root = release_tree(tmp_path, "1.fixed.md") - reader = root / "typescript/orrery/package.json" - reader.write_text( - re.sub(r'"version": "[^"]+"', '"version": "9.9.9"', reader.read_text(), count=1) - ) - with pytest.raises(ValueError, match="one shared version"): + pyproject = root / VERSIONED_FILE + pyproject.write_text(edit_pyproject(pyproject.read_text())) + with pytest.raises(ValueError, match="one version line"): bump_version["plan_release"](root) diff --git a/tools/bump-version.py b/tools/bump-version.py index 993ea51..d31a7c7 100644 --- a/tools/bump-version.py +++ b/tools/bump-version.py @@ -1,8 +1,8 @@ -"""Choose the next shared release version from pending changelog fragments. +"""Choose the next release version from pending changelog fragments. A breaking fragment bumps the major version, an added or removed fragment the -minor version, and any other fragment the patch version. The Python -distribution and both reader package manifests always share one version. +minor version, and any other fragment the patch version. The version is read +from and written to the single version line in pyproject.toml. Prints the next version. With --check, writes nothing. """ @@ -17,13 +17,8 @@ "Name each fragment changelog.d/..md, where is one of: " + ", ".join(FRAGMENT_TYPES) ) +VERSIONED_FILE = "pyproject.toml" PYPROJECT_VERSION = re.compile(r'^version = "(\d+\.\d+\.\d+)"$', re.MULTILINE) -PACKAGE_VERSION = re.compile(r'^ "version": "(\d+\.\d+\.\d+)",$', re.MULTILINE) -VERSIONED_FILES = { - "pyproject.toml": PYPROJECT_VERSION, - "package.json": PACKAGE_VERSION, - "typescript/orrery/package.json": PACKAGE_VERSION, -} def fragment_types(changelog_dir: Path) -> set[str]: @@ -57,14 +52,12 @@ def next_version(version: str, types: set[str]) -> str: def current_version(root: Path) -> str: - found = { - name: pattern.findall((root / name).read_text()) - for name, pattern in VERSIONED_FILES.items() - } - versions = {version for matches in found.values() for version in matches} - if len(versions) != 1 or any(len(matches) != 1 for matches in found.values()): - raise ValueError(f"Expected one shared version, found {found}.") - return versions.pop() + versions = PYPROJECT_VERSION.findall((root / VERSIONED_FILE).read_text()) + if len(versions) != 1: + raise ValueError( + f"Expected one version line in {VERSIONED_FILE}, found {versions}." + ) + return versions[0] def plan_release(root: Path) -> tuple[str, str]: @@ -73,15 +66,14 @@ def plan_release(root: Path) -> tuple[str, str]: def write_version(root: Path, current: str, upcoming: str) -> None: - for name, pattern in VERSIONED_FILES.items(): - path = root / name - path.write_text( - pattern.sub( - lambda match: match.group(0).replace(current, upcoming), - path.read_text(), - count=1, - ) + path = root / VERSIONED_FILE + path.write_text( + PYPROJECT_VERSION.sub( + lambda match: match.group(0).replace(current, upcoming), + path.read_text(), + count=1, ) + ) def main(arguments: list[str]) -> None: diff --git a/tools/check-changelog.sh b/tools/check-changelog.sh index 0b9636e..29ab417 100644 --- a/tools/check-changelog.sh +++ b/tools/check-changelog.sh @@ -2,5 +2,5 @@ set -euo pipefail # Refuse a pull request whose merge could not be released: changelog.d must hold -# at least one typed fragment, and all package manifests must share one version. +# at least one typed fragment, and pyproject.toml exactly one version line. uv run --no-project python tools/bump-version.py --check diff --git a/tools/check-reader-installed.ts b/tools/check-reader-installed.ts deleted file mode 100644 index 6529608..0000000 --- a/tools/check-reader-installed.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { mkdtempSync, writeFileSync, readdirSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; - -const files = readdirSync("reader-dist").filter((name) => - name.endsWith(".tgz"), -); -if (files.length !== 1) throw new Error("Expected exactly one reader tarball."); -const workspace = mkdtempSync(join(tmpdir(), "provider-reader-")); -function run(args: string[]) { - const result = Bun.spawnSync(args, { - cwd: workspace, - stdout: "inherit", - stderr: "inherit", - }); - if (result.exitCode !== 0) throw new Error("Installed reader check failed."); -} -try { - writeFileSync( - join(workspace, "package.json"), - JSON.stringify({ - private: true, - type: "module", - dependencies: { - "@policyengine/microcosm-provider-orrery": resolve( - "reader-dist", - files[0], - ), - "@axiom-foundation/orrery": "0.6.1", - }, - }), - ); - run(["bun", "install", "--ignore-scripts"]); - writeFileSync( - join(workspace, "check.ts"), - ` -import { loadDocument } from "@policyengine/microcosm-provider-orrery/browser"; -import { createPublicationReader } from "@policyengine/microcosm-provider-orrery/server"; -const reader = createPublicationReader({read: async () => null}); -if (await reader.read("missing") !== null) throw new Error("Expected missing publication."); -try { - await loadDocument("missing", new AbortController().signal, async () => new Response("", {status: 404})); - throw new Error("Missing publication was accepted."); -} catch (error) { - if (!(error instanceof Error) || !error.message.includes("not found")) throw error; -} -`, - ); - run(["bun", "check.ts"]); - writeFileSync( - join(workspace, "browser.ts"), - 'export { loadDocument } from "@policyengine/microcosm-provider-orrery/browser";', - ); - run([ - "bun", - "build", - "browser.ts", - "--target", - "browser", - "--outfile", - "browser.js", - ]); -} finally { - rmSync(workspace, { recursive: true, force: true }); -} diff --git a/tools/check-reader.sh b/tools/check-reader.sh deleted file mode 100644 index c438509..0000000 --- a/tools/check-reader.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd typescript/orrery -bun install --frozen-lockfile -bun run format:check -bun run typecheck -bun test -bun run build -bun pm pack --destination ../../reader-dist -cd ../.. -bun tools/check-reader-installed.ts diff --git a/tools/publish-reader.sh b/tools/publish-reader.sh deleted file mode 100644 index 97467a6..0000000 --- a/tools/publish-reader.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -node -e 'const [major, minor, patch] = require("child_process").execFileSync("npm", ["--version"], {encoding:"utf8"}).trim().split(".").map(Number); if (major < 11 || (major === 11 && (minor < 5 || (minor === 5 && patch < 1)))) throw new Error("npm OIDC requires npm >=11.5.1");' -shopt -s nullglob -archives=(reader-dist/*.tgz) -if (("${#archives[@]}" != 1)); then - echo "Expected exactly one verified reader tarball." >&2 - exit 1 -fi -# Without "./", npm reads "reader-dist/" as a GitHub owner/repo shorthand. -npm publish --access public --provenance "./${archives[0]}" diff --git a/tools/update-version.sh b/tools/update-version.sh index 0abfd63..f73adfe 100644 --- a/tools/update-version.sh +++ b/tools/update-version.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -# Release the Python distribution and the reader at one version: bump both from -# the pending fragment types, collect the fragments into CHANGELOG.md, and -# record the emitter's own new version in uv.lock for `uv sync --locked`. +# Release the Python distribution: bump pyproject.toml from the pending fragment +# types, collect the fragments into CHANGELOG.md, and record the emitter's own +# new version in uv.lock for `uv sync --locked`. version="$(uv run --no-project python tools/bump-version.py)" uvx --from "towncrier==26.9.0" towncrier build --yes --version "$version" uv lock diff --git a/typescript/orrery/README.md b/typescript/orrery/README.md deleted file mode 100644 index e7830bc..0000000 --- a/typescript/orrery/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Orrery publication reads - -The package owns publication validation, server-side metadata/evidence reads and -browser-side bounded downloads, SHA-256 verification and Orrery parsing. -It does not contain React components, routes, storage credentials or a hosted API. - -Import `createPublicationReader` from the `/server` entry point with a storage -adapter. Import `loadDocument` from `/browser` in the runs app. Browser exports -never import Node cryptography or server credentials. Existing publication URLs -and the Orrery React component remain unchanged. - -Build and test with Bun. Package publication and consumer activation are separate -operations; do not activate consumers before verifying the published tarball. diff --git a/typescript/orrery/bun.lock b/typescript/orrery/bun.lock deleted file mode 100644 index 58fb6c7..0000000 --- a/typescript/orrery/bun.lock +++ /dev/null @@ -1,88 +0,0 @@ -{ - "lockfileVersion": 2, - "configVersion": 1, - "workspaces": { - "": { - "name": "@policyengine/microcosm-provider-orrery", - "devDependencies": { - "@axiom-foundation/orrery": "0.6.1", - "@types/bun": "^1.3.0", - "@types/node": "^24.0.0", - "prettier": "^3.6.0", - "typescript": "^5.9.0", - }, - "peerDependencies": { - "@axiom-foundation/orrery": "0.6.1", - }, - }, - }, - "packages": { - "@axiom-foundation/orrery": ["@axiom-foundation/orrery@0.6.1", "", { "dependencies": { "@dagrejs/dagre": "3.1.1", "@xyflow/react": "12.11.6" }, "peerDependencies": { "react": ">=18 <20", "react-dom": ">=18 <20" }, "bin": { "orrery": "dist/cli.js", "graph-explorer": "dist/cli.js" } }, "sha512-VHYAuBZ50KhSXou1ogGG5IBjG1ET3Mnr05rIwUtoUgmgFu7eg5iH9rlsLQ28KQI/Lkd6yBsdYUsE3wo7kyCkhA=="], - - "@dagrejs/dagre": ["@dagrejs/dagre@3.1.1", "", { "dependencies": { "@dagrejs/graphlib": "4.0.5" } }, "sha512-zroZB1dFOFiGgv4Xcrn1DckB1o4aOikPqD2NDQPV0WM//CXGcS6xiD0rNkqHmw6FEg4tabt4nxPLwgCWT+Vb2A=="], - - "@dagrejs/graphlib": ["@dagrejs/graphlib@4.0.5", "", {}, "sha512-7xrBTqIts3o+PMUZX97wSc+7TUbW+/rULzGNCTP6yooNVDXbzw4Wutg/H/xOutTB/c/k0YqOAavgPh4/Zk9PFA=="], - - "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-selection": ["@types/d3-selection@3.0.12", "", {}, "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw=="], - - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - - "@types/d3-zoom": ["@types/d3-zoom@3.0.9", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA=="], - - "@types/node": ["@types/node@24.19.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ=="], - - "@xyflow/react": ["@xyflow/react@12.11.6", "", { "dependencies": { "@xyflow/system": "0.0.82", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9XsEJNHjatKYndszKTF/bsU7FOP9dJ6V/EQwzy3oMdtqgBuUq7BjKSwkEo+C7s4qHstHQfwwoHA3E8QfpPxZZQ=="], - - "@xyflow/system": ["@xyflow/system@0.0.82", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-4DKnL3CGtCGLRSmgDqaajRVgeksMXq/Yw4wPfdMfm7JvdIiWHGzVYOfFUztiATwdBXZqUU6HhehwUdbV9G23PQ=="], - - "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], - - "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - - "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - - "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - - "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - - "prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="], - - "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], - - "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], - - "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - - "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], - - "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - - "bun-types/@types/node": ["@types/node@26.6.4", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg=="], - - "bun-types/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], - } -} diff --git a/typescript/orrery/package.json b/typescript/orrery/package.json deleted file mode 100644 index 05fbea2..0000000 --- a/typescript/orrery/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@policyengine/microcosm-provider-orrery", - "version": "0.2.1", - "description": "Validated immutable publication reads for Orrery applications", - "type": "module", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/PolicyEngine/microcosm-emitter.git", - "directory": "typescript/orrery" - }, - "files": [ - "dist", - "README.md" - ], - "exports": { - "./contract": { - "types": "./dist/contract.d.ts", - "import": "./dist/contract.js" - }, - "./browser": { - "types": "./dist/browser.d.ts", - "import": "./dist/browser.js" - }, - "./server": { - "types": "./dist/server.d.ts", - "import": "./dist/server.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "test": "bun test", - "format:check": "prettier --check src tests *.json README.md" - }, - "peerDependencies": { - "@axiom-foundation/orrery": "0.6.1" - }, - "devDependencies": { - "@axiom-foundation/orrery": "0.6.1", - "@types/bun": "^1.3.0", - "@types/node": "^24.0.0", - "prettier": "^3.6.0", - "typescript": "^5.9.0" - } -} diff --git a/typescript/orrery/src/browser.ts b/typescript/orrery/src/browser.ts deleted file mode 100644 index a42e98d..0000000 --- a/typescript/orrery/src/browser.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { parseGraphDocument } from "@axiom-foundation/orrery"; -import { - inventory, - publication, - publicationId, - MAX_FILE_BYTES, -} from "./contract.js"; - -export type Fetcher = ( - input: string | URL | Request, - init?: RequestInit, -) => Promise; - -async function boundedBytes( - response: Response, - maximum: number, -): Promise { - if (!response.body) throw new Error("The stored graph is unavailable."); - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - let size = 0; - try { - while (true) { - const { value, done } = await reader.read(); - if (done) break; - size += value.byteLength; - if (size > maximum) - throw new Error("Response exceeds its publication size limit."); - chunks.push(value); - } - } finally { - await reader.cancel(); - } - const result = new Uint8Array(size); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.byteLength; - } - return result; -} -async function digest(bytes: Uint8Array): Promise { - return Array.from( - new Uint8Array( - await crypto.subtle.digest("SHA-256", bytes as Uint8Array), - ), - (value) => value.toString(16).padStart(2, "0"), - ).join(""); -} -export async function loadDocument( - id: string, - signal: AbortSignal, - fetcher: Fetcher = fetch, -) { - publicationId(id); - const response = await fetcher(`/api/runs/${encodeURIComponent(id)}/`, { - signal, - redirect: "error", - credentials: "omit", - }); - if (response.status === 404) - throw new Error("This graph publication was not found."); - if (!response.ok) - throw new Error("Graph publication metadata is unavailable."); - const bytes = await boundedBytes(response, 256 * 1024); - const saved = publication( - JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)), - id, - ); - if ( - (await digest( - new TextEncoder().encode(JSON.stringify(inventory(saved))), - )) !== saved.inventory_sha256 - ) - throw new Error("Inventory digest does not match its publication."); - const file = saved.files.find((file) => file.role === "graph")!; - const graph = await fetcher(file.url, { - signal, - redirect: "error", - credentials: "omit", - }); - if (!graph.ok) throw new Error("The stored graph is unavailable."); - const payload = await boundedBytes( - graph, - Math.min(file.bytes, MAX_FILE_BYTES), - ); - if (payload.length !== file.bytes) - throw new Error("Graph size does not match its publication."); - const checksum = await digest(payload); - if (checksum !== file.sha256) - throw new Error("Graph digest does not match its publication."); - return { - document: parseGraphDocument( - JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload)), - ), - digest: checksum, - }; -} diff --git a/typescript/orrery/src/contract.ts b/typescript/orrery/src/contract.ts deleted file mode 100644 index 2774419..0000000 --- a/typescript/orrery/src/contract.ts +++ /dev/null @@ -1,130 +0,0 @@ -export const MAX_FILE_BYTES = 64 * 1024 * 1024; -export const MAX_TOTAL_BYTES = 512 * 1024 * 1024; -export const MAX_FILES = 256; -export type FileRole = - | "graph" - | "schema" - | "index" - | "declaration" - | "manifest" - | "binding" - | "summaries" - | "upstream"; -export interface RunFile { - name: string; - role: FileRole; - bytes: number; - sha256: string; -} -export interface Inventory { - version: 1; - publication_id: string; - files: RunFile[]; -} -export interface Publication extends Inventory { - inventory_sha256: string; - published_at: string; - files: (RunFile & { url: string })[]; -} -export class ApiError extends Error { - constructor( - public status: number, - public code: string, - ) { - super(code); - } -} -export function publicationId(value: unknown): string { - if ( - typeof value !== "string" || - !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value) - ) - throw new ApiError(400, "invalid_publication_id"); - return value; -} -const roleNames: Record = { - graph: /^graph\.orrery\.json$/, - schema: /^graph\.schema\.json$/, - index: /^execution\.evidence\.json$/, - declaration: /^evidence-[a-zA-Z0-9_-]+-graph\.json$/, - manifest: /^evidence-[a-zA-Z0-9_-]+-manifest\.json$/, - binding: /^evidence-[a-zA-Z0-9_-]+-binding\.json$/, - summaries: /^evidence-[a-zA-Z0-9_-]+-summaries\.json$/, - upstream: /^upstream-[a-f0-9]{64}\.json$/, -}; -export function inventory(raw: unknown): Inventory { - if (!raw || typeof raw !== "object") - throw new ApiError(400, "invalid_inventory"); - const data = raw as Record; - const id = publicationId(data.publication_id); - if ( - data.version !== 1 || - !Array.isArray(data.files) || - data.files.length === 0 || - data.files.length > MAX_FILES - ) - throw new ApiError(400, "invalid_inventory"); - const names = new Set(); - let total = 0; - const files: RunFile[] = data.files.map((raw) => { - if (!raw || typeof raw !== "object") - throw new ApiError(400, "invalid_file"); - const f = raw as RunFile; - if ( - !Object.hasOwn(roleNames, f.role) || - typeof f.name !== "string" || - !roleNames[f.role].test(f.name) || - names.has(f.name) || - !Number.isSafeInteger(f.bytes) || - f.bytes <= 0 || - f.bytes > MAX_FILE_BYTES || - typeof f.sha256 !== "string" || - !/^[a-f0-9]{64}$/.test(f.sha256) - ) - throw new ApiError(400, "invalid_file"); - names.add(f.name); - total += f.bytes; - return { name: f.name, role: f.role, bytes: f.bytes, sha256: f.sha256 }; - }); - if ( - total > MAX_TOTAL_BYTES || - files.filter((f) => f.role === "graph").length !== 1 - ) - throw new ApiError(400, "invalid_inventory"); - return { - version: 1, - publication_id: id, - files: files.sort((a, b) => - a.name < b.name ? -1 : a.name > b.name ? 1 : 0, - ), - }; -} - -export function publication(raw: unknown, expectedId: string): Publication { - const value = inventory(raw); - const saved = raw as Publication; - if ( - value.publication_id !== expectedId || - !/^[a-f0-9]{64}$/.test(saved.inventory_sha256 ?? "") || - typeof saved.published_at !== "string" || - !Number.isFinite(Date.parse(saved.published_at)) - ) - throw new ApiError(400, "invalid_publication"); - const urls = new Map(saved.files.map((file) => [file.name, file.url])); - return { - ...value, - inventory_sha256: saved.inventory_sha256, - published_at: saved.published_at, - files: value.files.map((file) => { - const url = new URL(urls.get(file.name)!); - if ( - url.protocol !== "https:" || - !url.hostname.endsWith(".public.blob.vercel-storage.com") || - url.username || - url.password - ) - throw new ApiError(400, "invalid_storage_url"); - return { ...file, url: url.toString() }; - }), - }; -} diff --git a/typescript/orrery/src/server.ts b/typescript/orrery/src/server.ts deleted file mode 100644 index 3dff69c..0000000 --- a/typescript/orrery/src/server.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createHash } from "node:crypto"; -import { - ApiError, - inventory, - publication, - publicationId, - type Inventory, -} from "./contract.js"; - -export function sha256(bytes: string | Uint8Array): string { - return createHash("sha256").update(bytes).digest("hex"); -} -export function inventoryDigest(value: Inventory): string { - return sha256(JSON.stringify(inventory(value))); -} -export function publicationPath(id: string): string { - return `publications/${publicationId(id)}.json`; -} -export function objectPath(value: Inventory, name: string): string { - return `objects/${value.publication_id}/${inventoryDigest(value)}/${name}`; -} -export interface PublicationStorage { - read( - path: string, - maximum: number, - ): Promise<{ bytes: Uint8Array; url: string } | null>; -} -export function createPublicationReader(storage: PublicationStorage) { - async function read(id: string) { - const object = await storage.read(publicationPath(id), 256 * 1024); - if (!object) return null; - if (object.bytes.byteLength > 256 * 1024) - throw new ApiError(400, "metadata_too_large"); - const value = publication( - JSON.parse( - new TextDecoder("utf-8", { fatal: true }).decode(object.bytes), - ), - id, - ); - if (inventoryDigest(value) !== value.inventory_sha256) - throw new ApiError(400, "inventory_digest_mismatch"); - return value; - } - return { - read, - async evidence(id: string, name: string) { - return ( - (await read(id))?.files.find((file) => file.name === name)?.url ?? null - ); - }, - }; -} diff --git a/typescript/orrery/tests/contract.test.ts b/typescript/orrery/tests/contract.test.ts deleted file mode 100644 index eb3f19a..0000000 --- a/typescript/orrery/tests/contract.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { inventory, MAX_FILE_BYTES } from "../src/contract.js"; -import { inventoryDigest, objectPath } from "../src/server.js"; - -const graph = { - name: "graph.orrery.json", - role: "graph", - bytes: 100, - sha256: "a".repeat(64), -}; -const valid = { version: 1, publication_id: "uk-run-123", files: [graph] }; -describe("publication inventory", () => { - test("canonical order and content-addressed paths", () => { - const evidence = { - ...graph, - name: "execution.evidence.json", - role: "index", - }; - const a = inventory({ ...valid, files: [graph, evidence] }); - const b = inventory({ ...valid, files: [evidence, graph] }); - expect(inventoryDigest(a)).toBe(inventoryDigest(b)); - expect(objectPath(a, graph.name)).toBe( - `objects/uk-run-123/${inventoryDigest(a)}/graph.orrery.json`, - ); - }); - for (const value of [ - { ...valid, version: 2 }, - { ...valid, publication_id: "../bad" }, - { ...valid, files: [graph, graph] }, - { ...valid, files: [{ ...graph, name: "../graph.orrery.json" }] }, - { ...valid, files: [{ ...graph, bytes: MAX_FILE_BYTES + 1 }] }, - { ...valid, files: [{ ...graph, bytes: -1 }] }, - { ...valid, files: [{ ...graph, sha256: "wrong" }] }, - { ...valid, files: [{ ...graph, name: "private.h5", role: "manifest" }] }, - { - ...valid, - files: [{ ...graph, name: "unreviewed.json", role: "manifest" }], - }, - { ...valid, files: [] }, - ]) - test(`rejects ${JSON.stringify(value).slice(0, 100)}`, () => - expect(() => inventory(value)).toThrow()); -}); diff --git a/typescript/orrery/tests/reader.test.ts b/typescript/orrery/tests/reader.test.ts deleted file mode 100644 index 5bc02b5..0000000 --- a/typescript/orrery/tests/reader.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { expect, test } from "bun:test"; -import { loadDocument, type Fetcher } from "../src/browser.js"; -import { - createPublicationReader, - inventoryDigest, - sha256, -} from "../src/server.js"; -import { inventory, type Publication } from "../src/contract.js"; - -const document = { - schemaVersion: "graph-explorer/v1", - id: "fixture", - title: "Fixture", - nodes: [], - edges: [], -}; -function fixture(payload = JSON.stringify(document)): Publication { - const value = inventory({ - version: 1, - publication_id: "graph", - files: [ - { - name: "graph.orrery.json", - role: "graph", - bytes: new TextEncoder().encode(payload).length, - sha256: sha256(payload), - }, - ], - }); - return { - ...value, - inventory_sha256: inventoryDigest(value), - published_at: "2026-10-08T00:00:00Z", - files: value.files.map((file) => ({ - ...file, - url: "https://example.public.blob.vercel-storage.com/graph.json", - })), - }; -} -function fetcher(saved: unknown, payload = JSON.stringify(document)): Fetcher { - let calls = 0; - return async (_url, init) => { - expect(init?.credentials).toBe("omit"); - expect(init?.redirect).toBe("error"); - return ++calls === 1 ? Response.json(saved) : new Response(payload); - }; -} -const signal = () => new AbortController().signal; -test("browser verifies metadata, exact bytes and graph parser", async () => { - const result = await loadDocument("graph", signal(), fetcher(fixture())); - expect(result.document.id).toBe("fixture"); - expect(result.digest).toBe(sha256(JSON.stringify(document))); -}); -test("missing publications return an explicit error", async () => { - await expect( - loadDocument( - "graph", - signal(), - async () => new Response("", { status: 404 }), - ), - ).rejects.toThrow("not found"); -}); -for (const field of [ - "publication_id", - "inventory_sha256", - "published_at", -] as const) { - test("rejects invalid " + field, async () => { - const saved = { ...fixture(), [field]: "invalid" }; - await expect( - loadDocument("graph", signal(), fetcher(saved)), - ).rejects.toThrow(); - }); -} -for (const url of [ - "http://example.public.blob.vercel-storage.com/x", - "https://example.com/x", - "https://user:secret@example.public.blob.vercel-storage.com/x", -]) { - test("rejects credential-bearing or non-storage URL " + url, async () => { - const saved = fixture(); - saved.files[0].url = url; - await expect( - loadDocument("graph", signal(), fetcher(saved)), - ).rejects.toThrow(); - }); -} -test("rejects modified graph bytes", async () => { - await expect( - loadDocument( - "graph", - signal(), - fetcher(fixture(), "x".repeat(JSON.stringify(document).length)), - ), - ).rejects.toThrow("digest"); -}); -test("bounds graph streaming before parsing", async () => { - await expect( - loadDocument( - "graph", - signal(), - fetcher(fixture(), JSON.stringify(document) + "extra"), - ), - ).rejects.toThrow("size limit"); -}); -test("bounds metadata streaming", async () => { - await expect( - loadDocument( - "graph", - signal(), - async () => new Response("x".repeat(256 * 1024 + 1)), - ), - ).rejects.toThrow("size limit"); -}); -test("rejects a valid but incorrect inventory digest", async () => { - await expect( - loadDocument( - "graph", - signal(), - fetcher({ ...fixture(), inventory_sha256: "a".repeat(64) }), - ), - ).rejects.toThrow("Inventory digest"); -}); -test("server reads only completed publications and listed evidence", async () => { - const saved = fixture(); - const reads: string[] = []; - const reader = createPublicationReader({ - async read(path, maximum) { - reads.push(path); - expect(maximum).toBe(256 * 1024); - return path === "publications/graph.json" - ? { - bytes: new TextEncoder().encode(JSON.stringify(saved)), - url: "unused", - } - : null; - }, - }); - expect(await reader.read("missing")).toBeNull(); - expect((await reader.read("graph"))?.publication_id).toBe("graph"); - expect(await reader.evidence("graph", "private.h5")).toBeNull(); - expect(await reader.evidence("graph", "graph.orrery.json")).toBe( - saved.files[0].url, - ); - expect(reads.every((path) => path.startsWith("publications/"))).toBe(true); -}); -test("server rejects corrupt stored metadata", async () => { - const saved = { ...fixture(), inventory_sha256: "a".repeat(64) }; - const reader = createPublicationReader({ - async read() { - return { - bytes: new TextEncoder().encode(JSON.stringify(saved)), - url: "unused", - }; - }, - }); - await expect(reader.read("graph")).rejects.toThrow( - "inventory_digest_mismatch", - ); -}); diff --git a/typescript/orrery/tsconfig.build.json b/typescript/orrery/tsconfig.build.json deleted file mode 100644 index 0f6eea2..0000000 --- a/typescript/orrery/tsconfig.build.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - "declaration": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["tests"] -} diff --git a/typescript/orrery/tsconfig.json b/typescript/orrery/tsconfig.json deleted file mode 100644 index 8c1f9cd..0000000 --- a/typescript/orrery/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2023", "DOM"], - "strict": true, - "skipLibCheck": true, - "types": ["bun"], - "noEmit": true - }, - "include": ["src", "tests"] -}