Skip to content

Quickstart

This creates an App, a public route, and one Release whose artifact is a small HTML page, then promotes the Release and reads the page back through the route. It runs in Node with the in-memory store and a ten-line artifact reader:

import { createHash } from "node:crypto";
import {
createAppReleaseCatalog,
parseAppId,
parseArtifactRef,
parseIdempotencyKey,
parsePublicSurfaceId,
parseReleaseId,
parseSha256Digest,
parseSourceRevision,
parseStableRoute,
type ArtifactMediaType,
type ArtifactReader,
type ArtifactRef,
} from "@fungi.computer/app-release";
import { createDeterministicMemoryAppReleaseStore } from "@fungi.computer/app-release/effect";
const sha256 = (bytes: Uint8Array) =>
parseSha256Digest(
`sha256:${createHash("sha256").update(bytes).digest("hex")}`,
);
// Two artifacts: the HTML page, and the App root that points at it.
const html = new TextEncoder().encode("<!doctype html><h1>Hello</h1>");
const htmlRef = parseArtifactRef("artifact:hello.ui");
const root = new TextEncoder().encode(
JSON.stringify({
format: "fungi-app-v1",
ui: {
artifactRef: htmlRef,
digest: sha256(html),
version: "v1",
byteLength: html.byteLength,
mediaType: "text/html; charset=utf-8",
},
}),
);
const rootRef = parseArtifactRef("artifact:hello.release");
// A tiny artifact reader: look the bytes up, hash them, compare digests.
const files = new Map<
ArtifactRef,
{ bytes: Uint8Array; mediaType: ArtifactMediaType }
>([
[htmlRef, { bytes: html, mediaType: "text/html; charset=utf-8" }],
[rootRef, { bytes: root, mediaType: "application/vnd.fungi.app+json" }],
]);
const artifactReader: ArtifactReader = {
read: async ({ artifactRef, expectedDigest }) => {
const file = files.get(artifactRef);
if (file === undefined) return { kind: "missing" };
const digest = sha256(file.bytes);
if (digest !== expectedDigest)
return { kind: "digest-mismatch", actualDigest: digest };
return {
kind: "verified",
digest,
mediaType: file.mediaType,
byteLength: file.bytes.byteLength,
bytes: {
byteLength: file.bytes.byteLength,
copy: () => file.bytes.slice(),
},
};
},
};
const catalog = createAppReleaseCatalog({
artifactReader,
store: createDeterministicMemoryAppReleaseStore(),
});
const appId = parseAppId("app_hello");
const publicSurfaceId = parsePublicSurfaceId("ps_hello");
const releaseId = parseReleaseId("rel_hello1");
const route = parseStableRoute("/hello");
await catalog.createApp({ appId });
await catalog.createPublicSurface({
publicSurfaceId,
appId,
stableRoute: route,
});
await catalog.createRelease({
releaseId,
appId,
artifact: {
artifactRef: rootRef,
artifactDigest: sha256(root),
provenance: {
sourceRevision: parseSourceRevision(`git:${"a".repeat(40)}`),
sourceTreeDigest: sha256(new TextEncoder().encode("source tree")),
buildRecipeDigest: sha256(new TextEncoder().encode("build recipe")),
},
},
});
await catalog.verifyReleaseArtifact(releaseId);
await catalog.transition({
kind: "promotion",
publicSurfaceId,
releaseId,
idempotencyKey: parseIdempotencyKey("idem_hello-promote-1"),
});
const website = await catalog.resolvePublicWebsite(route);
console.log(new TextDecoder().decode(website.bytes.copy()));
// <!doctype html><h1>Hello</h1>