Heimdall/deployment/idunn/recipe.toml
Meta Vens 6a923d525e Follow CultLib's package rename and select workspaces by path
Moving the CultLib pin forward to pick up the presence contract also brought
ten unrelated commits, one of which republished the TypeScript packages under
the @gamecult scope. Two things broke on that rename.

The recipe selected build workspaces by package name, so `--workspace=
cultcache-ts` no longer matched anything. Selecting by path instead says what
is actually meant and survives the next rename.

cultnet-ts depends on cultcache-ts by registry range, not by workspace
protocol, so the vendored copy is only used because Heimdall overrides that
name to the vendored path. The override still named the old one, and pnpm went
looking for @gamecult/cultcache-ts on a registry that does not have it.
Heimdall now uses the package's real name in its dependency, its override and
its imports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 22:26:24 +02:00

194 lines
7 KiB
TOML

# The runtime contract below is implemented as of 2026-09-06.
#
# GAMECULT_IDUNN_CANDIDATE_BIND src/config.ts takes the candidate socket
# over HOST/PORT, requires loopback and a
# usable port, and refuses to start unless
# GC_ACCESS_BASE_URL is set, so an
# ephemeral port can never become the
# provider-registered OAuth callback.
# GAMECULT_IDUNN_PROCESS_WRITE_LEASE src/process-write-lease.ts. The single
# writer of heimdall.service.cc declines
# unless the lease names this incarnation.
# GAMECULT_IDUNN_RUNTIME_BUNDLE read for expected.cc, which is how the
# process learns which incarnation it is.
#
# Health reports `warming` until the lease is held and `active` after, because
# Idunn must observe warming presence before it fences the incumbent.
#
# Still untested against a live Idunn: nothing here has been through an actual
# `idunn up heimdall`, and the daemon on yggdrasil is still the previous
# generation. Treat the first deployment as the real verification.
schema = "gamecult.idunn.target_declaration.v1"
target = "heimdall"
source_stamp_environment = "HEIMDALL_BUILD_COMMIT"
required_gitlinks = ["vendor/CultLib"]
# CultLib's TypeScript packages resolve through main/types to dist/, which a
# fresh checkout does not have. They must be built before Heimdall's own
# typecheck can resolve cultcache-ts, cultnet-ts or cultmesh-ts. This used to
# live in a shell manifest on the host; it belongs here, where the source seal
# covers it.
[[steps]]
id = "install-cultlib"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "ci", "--ignore-scripts", "--prefix", "vendor/CultLib"]
[[steps]]
id = "build-cultcache-ts"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "run", "build", "--prefix", "vendor/CultLib", "--workspace=packages/cultcache-ts"]
[[steps]]
id = "build-cultnet-ts"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "run", "build", "--prefix", "vendor/CultLib", "--workspace=packages/cultnet-ts"]
[[steps]]
id = "build-cultmesh-ts"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "run", "build", "--prefix", "vendor/CultLib", "--workspace=packages/cultmesh-ts"]
[[steps]]
id = "install-heimdall"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "install", "--frozen-lockfile", "--config.node-linker=hoisted"]
[[steps]]
id = "test-heimdall"
phase = "test"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "test"]
[[steps]]
id = "build-heimdall"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "build"]
# The release is one self-contained executable. node_modules is a build
# input, not an artifact: pnpm lays it out as symlinks into .pnpm, which
# Idunn refuses to seal, and shipping a dependency tree to run a service is
# a larger liability than bundling it.
[[steps]]
id = "bundle-heimdall"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "bundle"]
# The unit runs dist/src/index.js, and Node resolves its imports at runtime, so
# the release is the compiled output plus the production dependency tree. These
# are the two artifacts; nothing else from the build tree is installed.
[[steps]]
id = "stage-node"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["node", "-e", "require('fs').copyFileSync(process.execPath, 'dist/node')"]
# Idunn compares the running process's executable inode against the sealed
# artifact, so the executed inode must live inside the release. A shebang script
# cannot satisfy that: the kernel execs the interpreter, and the process's
# executable is /usr/bin/node rather than anything sealed. Shipping the runner
# image's own node as the executable brings the interpreter inside the seal and
# pins it to the runner image digest, which is a stronger guarantee than
# depending on whatever node the host happens to have.
[[artifacts]]
id = "node"
source_kind = "runner-output"
runner = "node-build"
source = "dist/node"
destination = "node"
executable = true
[[artifacts]]
id = "heimdall-bundle"
source_kind = "runner-output"
runner = "node-build"
source = "dist/heimdall"
destination = "heimdall.mjs"
executable = false
[service]
executable_artifact = "node"
arguments = [
{ kind = "literal", value = "heimdall.mjs" },
{ kind = "literal", value = "--state-root" },
{ kind = "binding", name = "state_root" },
]
transport = "http"
route_required = true
optional_environment = [
"CULTMESH_URI_ODIN_RUDP",
"GC_ACCESS_APP_BIFROST_BACKEND_CALLBACK_URLS",
"GC_ACCESS_APP_BIFROST_SHARED_SECRET",
"GC_ACCESS_APP_GHOSTLIGHT_BACKEND_CALLBACK_URLS",
"GC_ACCESS_APP_GHOSTLIGHT_RUNTIME_IDS",
"GC_ACCESS_APP_GHOSTLIGHT_SHARED_SECRET",
"GC_ACCESS_APPLY_SCHEMA_ON_STARTUP",
"GC_ACCESS_APP_SHARED_SECRET",
"GC_ACCESS_BIFROST_PATRON_SUPPORT_ENDPOINT",
"GC_ACCESS_BIFROST_PATRON_SUPPORT_SECRET",
"GC_ACCESS_COMPLETION_TTL_SECONDS",
"GC_ACCESS_IDUNN_HEALTH_CONTRACT",
"GC_ACCESS_IDUNN_RUDP_HEALTH",
"GC_ACCESS_ISSUER",
"GC_ACCESS_ODIN_CULTMESH_URI",
"GC_ACCESS_PRIVATE_COMMAND_HOST",
"GC_ACCESS_PRIVATE_COMMAND_PORT",
"GC_ACCESS_PROVIDER_DISCORD_CLIENT_ID",
"GC_ACCESS_PROVIDER_DISCORD_CLIENT_SECRET",
"GC_ACCESS_PROVIDER_PATREON_CLIENT_ID",
"GC_ACCESS_PROVIDER_PATREON_CLIENT_SECRET",
"GC_ACCESS_PROVIDER_TWITCH_CLIENT_ID",
"GC_ACCESS_PROVIDER_TWITCH_CLIENT_SECRET",
"GC_ACCESS_PROVIDER_YOUTUBE_CLIENT_ID",
"GC_ACCESS_PROVIDER_YOUTUBE_CLIENT_SECRET",
"GC_ACCESS_SESSION_TTL_SECONDS",
"GC_ACCESS_SIGNING_KEY_ID",
"GC_ACCESS_SIGNING_PRIVATE_KEY_BOOTSTRAP",
"GC_ACCESS_SIGNING_PRIVATE_KEY_PATH",
"GC_ACCESS_STATE_TTL_SECONDS",
"GC_ACCESS_STORAGE_BACKEND",
"GC_ACCESS_TOKEN_ENCRYPTION_KEY_BASE64",
]
required_environment = [
"GAMECULT_IDUNN_RUNTIME_BUNDLE",
"GAMECULT_IDUNN_CANDIDATE_BIND",
"GAMECULT_IDUNN_PROCESS_WRITE_LEASE",
"GC_ACCESS_BASE_URL",
"GC_ACCESS_DATABASE_URL",
]
[service.health]
contract = "heimdall.cultnet-rudp-access-health"
[state]
schema_generation = "heimdall-v1"
[[state.slots]]
id = "service"
relative_path = "heimdall.service.cc"
kind = "cultcache-file"
schema = "heimdall.service_state.v1"
writer = "process-bound-single-writer"
recovery = "preserve"
startup = "create-or-open-after-write-lease"
[[provides]]
capability = "gamecult.shared-auth"
schema = "heimdall.access-discovery.v1"
compatibility = "v1"