A-C5: mesh-build-node builds against the latest published SDK, the toolchain pins none

Recovered from cfee5f2d (feat/47-G-buildmods).
Builds: design 47 G §571 (the build writes a mesh package's lock entry from the latest published version), A-56

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 23:23:44 +01:00
parent ff07c3a668
commit c41a84ee95
10 changed files with 158 additions and 69 deletions
+3 -2
View File
@@ -18,11 +18,12 @@ one changed byte of the lock or another recipe moves it, and typescript and esbu
## The toolchain directory
`toolchain/package.json` and its committed `package-lock.json` name typescript 5.9.3, esbuild 0.25.12, @types/node 22.20.1 (the types tsc checks node imports against),
@novox/mesh-sdk 0.1.14 and nats 2.29.3. The mesh unpacks both, as the archive artifact `toolchain`, into `<home>/toolchain/` (resource `toolchain-dir`; a
and nats 2.29.3; it names no @novox/mesh-sdk: each TypeScript module is built against the latest release of the SDK the registry
holds, placed in its own `node_modules` by the fetch whatever its lock names, or the group's tagged version (ADR 0573 §2, ADR 0607 §3). The mesh unpacks both, as the archive artifact `toolchain`, into `<home>/toolchain/` (resource `toolchain-dir`; a
manifest names no host, and the lock does, so it is not a template); the program reads the lock from there. When `<cache dir>/toolchain/<recipe version>` is missing
the module runs `npm ci --ignore-scripts` there once (a directory without its `.installed` mark counts as missing).
**How the registry is reached** (the choice of issue 1488): the lock holds @novox/mesh-sdk at the forge's public
**How the registry is reached** (the choice of issue 1488): a lock names an @novox package at the forge's public
address, and npm 12 installs a locked tarball only from the registry it is configured with. The module therefore
requires `npm-package-registry` (provided by gitea, form none: reads are anonymous) and, like the lab module, takes its
binding as a file in its home. The install gets a private npm config file with the `@novox` scope at the binding's
@@ -5,7 +5,8 @@ package main
// TypeScript module in it the packages its package.json names beyond the SDK, installed from its committed lock.
//
// **Only what the module itself depends on is installed.** The SDK and nats come with the toolchain, so a bundle that
// names only @novox/mesh-sdk (and build-time types and typescript) resolves nothing. One that names more must carry a
// names only @novox/mesh-sdk (and build-time types and typescript) resolves nothing beyond the SDK, which is placed at
// its latest published version (ADR 0573 §2). One that names more must carry a
// package-lock.json, or the step fails naming its directory. The install is the build agent's (mesh-controller
// internal/builder/dependencies.go:49-95): in a scratch copy of package.json and the lock with the SDK, devDependencies
// and scripts taken out, `npm ci --omit=dev --omit=peer --ignore-scripts`, the install's copies of the SDK removed, and
@@ -64,6 +65,9 @@ type FetchResult struct {
// Group is the group's package the fetch packed and served to the TypeScript members (the call's `package`):
// "<name>@<version> <integrity>".
Group string `json:"group,omitempty"`
// SDK is the version of @novox/mesh-sdk placed in each TypeScript module, by directory: the latest release, the
// group's tagged version or the group's package (placeSDK; ADR 0573 §2).
SDK map[string]string `json:"sdk,omitempty"`
}
// narrowedMark is the file beside a workspace's tree saying the fetch narrowed it to a module's named paths.
@@ -197,10 +201,14 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group
return nil, err
}
if refused == "" {
// The SDK the module's own lock names, from the group or the registry (the lead's decision L10).
// The latest published SDK, or the group's (ADR 0573 §2, ADR 0607 §3).
if err := m.placeSDK(ctx, dir, d, served, group); err != nil {
return nil, err
}
if res.SDK == nil {
res.SDK = map[string]string{}
}
res.SDK[d] = placedSDK(dir)
}
if refused != "" {
res.Refused = append(res.Refused, Refusal{Module: d, Reason: refused})
@@ -368,7 +376,8 @@ func (m *Module) installOwn(ctx context.Context, root, dir, rel string) (install
return true, "", os.RemoveAll(scratch)
}
// removeSDKCopies removes every copy of the SDK the install pulled in: the toolchain's is the pin.
// removeSDKCopies removes every copy of the SDK the install pulled in: the one placeSDK places, the latest published
// or the group's, is the one the module is built against.
func removeSDKCopies(nm string) error {
var found []string
err := filepath.WalkDir(nm, func(p string, d fs.DirEntry, err error) error {
@@ -97,7 +97,7 @@ func TestAModuleNamingOnlyTheSdkAndTypesInstallsNothing(t *testing.T) {
}
// 1489/AC2: a module naming lodash with a lock gets npm ci --omit=dev --ignore-scripts (fake runner), its install put
// at its node_modules without the SDK and without any copy of it; without a lock the step fails naming the directory.
// at its node_modules without the install's SDK or any copy of it; without a lock the step fails naming the directory.
func TestAModuleNamingLodashWithALockIsInstalledWithoutTheSdkAndWithoutALockFails(t *testing.T) {
files := typeScriptModule(`{"dependencies":{"@novox/mesh-sdk":"^0.1.6","lodash":"^4"},"devDependencies":{"x":"1"},"scripts":{"test":"y"}}`)
files["modules/m/package-lock.json"] = `{"lockfileVersion":3}`
@@ -119,7 +119,8 @@ func TestAModuleNamingLodashWithALockIsInstalledWithoutTheSdkAndWithoutALockFail
if _, err := os.Stat(filepath.Join(nm, "lodash", "index.js")); err != nil {
t.Errorf("lodash is not at the module's node_modules: %v", err)
}
for _, gone := range []string{"@novox/mesh-sdk", "a/node_modules/@novox/mesh-sdk"} {
// The install's copies are gone; the one at the top is the latest release placed after it (placeSDK).
for _, gone := range []string{"@novox/mesh-sdk/index.js", "a/node_modules/@novox/mesh-sdk"} {
if _, err := os.Stat(filepath.Join(nm, gone)); err == nil {
t.Errorf("a copy of the SDK is left at %s", gone)
}
@@ -161,8 +161,8 @@ func TestTheModuleCarriesItsToolchainLockAndTheManifestPlacesIt(t *testing.T) {
}
}
// 1488/AC1: the lock names the versions of the recipe: typescript 5.9.3, esbuild 0.25.12, @novox/mesh-sdk 0.1.14
// and nats, all by the lock's own hash (read from the file the module carries).
// 1488/AC1: the lock names the versions of the recipe: typescript 5.9.3, esbuild 0.25.12 and nats, all by the lock's
// own hash (read from the file the module carries), and no @novox/mesh-sdk: the SDK is unpinned (ADR 0573 §2).
func TestTheLockNamesTheToolchainVersions(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "toolchain", "package-lock.json"))
if err != nil {
@@ -176,7 +176,7 @@ func TestTheLockNamesTheToolchainVersions(t *testing.T) {
if err := json.Unmarshal(raw, &lock); err != nil {
t.Fatal(err)
}
for pkg, want := range map[string]string{"typescript": "5.9.3", "esbuild": "0.25.12", "@novox/mesh-sdk": "0.1.14", "nats": "2.29.3", "@types/node": "22.20.1"} {
for pkg, want := range map[string]string{"typescript": "5.9.3", "esbuild": "0.25.12", "@novox/mesh-sdk": "", "nats": "2.29.3", "@types/node": "22.20.1"} {
if got := lock.Packages["node_modules/"+pkg].Version; got != want {
t.Errorf("%s = %q in the lock, want %s", pkg, got, want)
}
@@ -27,6 +27,7 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -292,7 +293,7 @@ func (m *Module) packGroup(ctx context.Context, root string, run forgekey.Runner
}
// sdkLocked is what a module's lock names of the SDK: its version, its integrity and where it was resolved from; ok is
// false when the lock names none (the toolchain's SDK is then the module's, as before).
// false when the lock names none (the latest release is then placed, placeSDK).
func sdkLocked(dir string) (version, integrity string, ok bool, err error) {
raw, err := os.ReadFile(filepath.Join(dir, "package-lock.json"))
if os.IsNotExist(err) {
@@ -317,21 +318,25 @@ func sdkLocked(dir string) (version, integrity string, ok bool, err error) {
return p.Version, p.Integrity, true, nil
}
// placeSDK places in the module's node_modules the SDK version its lock names (the lead's decision L10): the group's
// packed package when it is that version (its integrity must be the lock's), else the registry's; a version neither
// holds is a wait (`waits for:`) when no group package was given, and a failure naming both when one was. Given the
// group's tagged version (tagged, ADR 0607 §3), that version is placed from the registry whatever the lock names, its
// integrity the lock's when the lock names it, else the registry's own; one the registry lacks fails naming it.
// placeSDK places in the module's node_modules the SDK it is built against: the latest release the registry holds,
// whatever its lock names (novox/hq ADR 0573 §2: a module builds against the latest published version of each mesh
// package; the lead's decision L10's version from the lock is gone), or the group's packed package when the lock names
// its version. Every TypeScript module gets it, named in its package.json or not, since its launcher imports the SDK
// (writeLaunchers) and the toolchain holds none. The lock's integrity is checked when the lock names the version placed;
// a registry holding no release is a wait (`waits for:`) when no group package was given, and a failure naming the group
// when one was. Given the group's tagged version (tagged, ADR 0607 §3), that version is placed from the registry
// whatever the lock names, its integrity the lock's when the lock names it, else the registry's; one the registry lacks
// fails naming it.
func (m *Module) placeSDK(ctx context.Context, dir, rel string, group *packed, tagged *groupPackage) error {
version, integrity, ok, err := sdkLocked(dir)
if err != nil || !ok {
version, integrity, _, err := sdkLocked(dir)
if err != nil {
return err
}
var tgz []byte
switch {
case tagged.tagged():
if version != tagged.Version {
if m.Say != nil {
if m.Say != nil && version != "" {
m.Say("%s: its lock names %s@%s; the group's tagged version %s is placed (ADR 0607 §3)", rel, sdkPackage, version, tagged.Version)
}
version, integrity = tagged.Version, ""
@@ -351,23 +356,39 @@ func (m *Module) placeSDK(ctx context.Context, dir, rel string, group *packed, t
return fmt.Errorf("the registry's %s is %s, and %s names %s", want, integrityOf(got), rel, integrity)
}
tgz = got
case group != nil && group.Name == sdkPackage && group.Version == version:
case group != nil && group.Name == sdkPackage && version != "" && group.Version == version:
if integrity != "" && integrity != group.Integrity {
return fmt.Errorf("the lock of %s names %s with the integrity %s, and the group's package packs to %s", rel, sdkPackage+"@"+version, integrity, group.Integrity)
}
tgz = group.tgz
default:
latest, err := m.registryLatest(ctx, sdkPackage)
if err != nil {
return err
}
if latest == "" {
if group != nil {
return fmt.Errorf("the registry holds no release of %s and the group's package is %s@%s, which the lock of %s does not name", sdkPackage, group.Name, group.Version, rel)
}
return fmt.Errorf("%s%s", builds.WaitsFor, sdkPackage)
}
if version != latest {
if m.Say != nil && version != "" {
m.Say("%s: its lock names %s@%s; the latest published %s is placed (ADR 0573 §2)", rel, sdkPackage, version, latest)
}
version, integrity = latest, ""
}
want := sdkPackage + "@" + version
got, held, err := m.registryPackage(ctx, sdkPackage, version)
got, registered, held, err := m.registryPackageChecked(ctx, sdkPackage, version)
if err != nil {
return err
}
if !held {
if group != nil {
return fmt.Errorf("the lock of %s names %s, which neither the registry nor the group (%s@%s) holds", rel, want, group.Name, group.Version)
}
return fmt.Errorf("%s%s", builds.WaitsFor, want)
}
if integrity == "" {
integrity = registered
}
if integrity != "" && integrityOf(got) != integrity {
return fmt.Errorf("the registry's %s is %s, and the lock of %s names %s", want, integrityOf(got), rel, integrity)
}
@@ -380,6 +401,47 @@ func (m *Module) placeSDK(ctx context.Context, dir, rel string, group *packed, t
return unpackPackage(tgz, into)
}
// placedSDK is the version of the SDK placed in the module's node_modules ("" when none is).
func placedSDK(dir string) string {
p, err := readPackageJSON(filepath.Join(dir, "node_modules", filepath.FromSlash(sdkPackage)))
if err != nil {
return ""
}
return p.Version
}
// registryLatest is the latest release of name the registry holds: the highest version with no pre-release, so a
// group's tagged version is never taken for it; "" when the registry holds none.
func (m *Module) registryLatest(ctx context.Context, name string) (string, error) {
p, held, err := m.packumentOf(ctx, name)
if err != nil || !held {
return "", err
}
latest := ""
for v := range p.Versions {
if !npmVersionOK.MatchString(v) || strings.Contains(v, "-") {
continue
}
if latest == "" || releaseLess(latest, v) {
latest = v
}
}
return latest, nil
}
// releaseLess orders two releases (major.minor.patch, no pre-release) by their numbers.
func releaseLess(a, b string) bool {
pa, pb := strings.Split(a, "."), strings.Split(b, ".")
for i := 0; i < 3; i++ {
x, _ := strconv.Atoi(pa[i])
y, _ := strconv.Atoi(pb[i])
if x != y {
return x < y
}
}
return false
}
// registryPackageChecked is registryPackage with the integrity the registry's document names of name@version.
func (m *Module) registryPackageChecked(ctx context.Context, name, version string) (tgz []byte, integrity string, held bool, err error) {
_, integrity, held, err = m.registryVersion(ctx, name, version)
@@ -408,32 +470,41 @@ type packument struct {
} `json:"versions"`
}
// registryVersion reads what the registry holds of name@version: its packed file's address and integrity, and whether
// it holds it at all.
func (m *Module) registryVersion(ctx context.Context, name, version string) (tarball, integrity string, held bool, err error) {
// packumentOf reads the registry's document of name; held is false when the registry has none.
func (m *Module) packumentOf(ctx context.Context, name string) (packument, bool, error) {
reg, err := m.Toolchain.Registry()
if err != nil {
return "", "", false, err
return packument{}, false, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reg.URL+url.PathEscape(name), nil)
if err != nil {
return "", "", false, err
return packument{}, false, err
}
resp, err := m.httpClient().Do(req)
if err != nil {
return "", "", false, fmt.Errorf("asking the registry for %s: %w", name, err)
return packument{}, false, fmt.Errorf("asking the registry for %s: %w", name, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return "", "", false, nil
return packument{}, false, nil
}
if resp.StatusCode != http.StatusOK {
said, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return "", "", false, fmt.Errorf("the registry answered %s for %s: %s", resp.Status, name, strings.TrimSpace(string(said)))
return packument{}, false, fmt.Errorf("the registry answered %s for %s: %s", resp.Status, name, strings.TrimSpace(string(said)))
}
var p packument
if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<20)).Decode(&p); err != nil {
return "", "", false, fmt.Errorf("the registry's document of %s: %w", name, err)
return packument{}, false, fmt.Errorf("the registry's document of %s: %w", name, err)
}
return p, true, nil
}
// registryVersion reads what the registry holds of name@version: its packed file's address and integrity, and whether
// it holds it at all.
func (m *Module) registryVersion(ctx context.Context, name, version string) (tarball, integrity string, held bool, err error) {
p, held, err := m.packumentOf(ctx, name)
if err != nil || !held {
return "", "", false, err
}
v, ok := p.Versions[version]
if !ok {
@@ -91,29 +91,42 @@ func placedVersion(dir string) string {
return p.Version
}
// The lead's decision L10 (ADR 0500 §6): a member is built against the SDK version its own lock names, downloaded from
// the registry and checked against the lock's integrity; a version the registry does not hold is a wait, `waits for:`.
func TestAMemberGetsTheSDKVersionItsLockNamesOrWaitsForIt(t *testing.T) {
// ADR 0573 §2 (unpinned, the lead's decision L10's lock version gone): a member is built against the latest release the
// registry holds, whatever its lock names or when it names none, checked against the lock's integrity only when the lock
// names that version; a pre-release is never the latest; a registry holding no release is a wait, `waits for:`.
func TestAMemberGetsTheLatestPublishedSDKWhateverItsLockNames(t *testing.T) {
m, _, _ := newMachine(t)
held := tgzOf(t, sdkPackage, "0.1.15")
held, older, pre := tgzOf(t, sdkPackage, "0.1.15"), tgzOf(t, sdkPackage, "0.1.9"), tgzOf(t, sdkPackage, "0.1.16-feat-x.1")
fakeRegistry(t, m, map[string][]byte{"0.1.15": held, "0.1.9": older, "0.1.16-feat-x.1": pre})
for _, c := range []struct {
name, locks, integrity, fails string
}{
{name: "a lock naming the latest", locks: "0.1.15", integrity: integrityOf(held)},
{name: "a lock naming an older version (the pin)", locks: "0.1.9", integrity: integrityOf(older)},
{name: "a lock naming a version the registry lacks", locks: "0.1.16", integrity: "sha512-x"},
{name: "a lock naming none", locks: ""},
{name: "a lock naming the latest with another integrity", locks: "0.1.15", integrity: "sha512-another", fails: "sha512-another"},
} {
dir := memberLocking(t, c.locks, c.integrity)
err := m.placeSDK(context.Background(), dir, "modules/m", nil, nil)
switch {
case c.fails == "" && (err != nil || placedVersion(dir) != "0.1.15"):
t.Errorf("%s: %v, placed %q, want the latest release 0.1.15", c.name, err, placedVersion(dir))
case c.fails != "" && (err == nil || !strings.Contains(err.Error(), c.fails)):
t.Errorf("%s: %v, want it to fail naming %s", c.name, err, c.fails)
}
}
fakeRegistry(t, m, map[string][]byte{"0.1.16-feat-x.1": pre})
if err := m.placeSDK(context.Background(), memberLocking(t, "0.1.15", ""), "modules/m", nil, nil); err == nil || err.Error() != builds.WaitsFor+"@novox/mesh-sdk" {
t.Errorf("a registry with no release answered %v, want %s@novox/mesh-sdk", err, builds.WaitsFor)
}
held = tgzOf(t, sdkPackage, "0.1.15")
fakeRegistry(t, m, map[string][]byte{"0.1.15": held})
dir := memberLocking(t, "0.1.15", integrityOf(held))
if err := m.placeSDK(context.Background(), dir, "modules/m", nil, nil); err != nil || placedVersion(dir) != "0.1.15" {
t.Errorf("the registry's 0.1.15: %v, placed %q", err, placedVersion(dir))
}
dir = memberLocking(t, "0.1.16", "sha512-x")
err := m.placeSDK(context.Background(), dir, "modules/m", nil, nil)
if err == nil || err.Error() != builds.WaitsFor+"@novox/mesh-sdk@0.1.16" {
t.Errorf("a version the registry lacks answered %v, want %s@novox/mesh-sdk@0.1.16", err, builds.WaitsFor)
}
dir = memberLocking(t, "0.1.15", "sha512-another")
if err := m.placeSDK(context.Background(), dir, "modules/m", nil, nil); err == nil || !strings.Contains(err.Error(), "sha512-another") {
t.Errorf("a registry file of another integrity than the lock's answered %v", err)
}
var dir string
// ADR 0607 §3, §5 (design 51 *Tagged versions*): given the group's tagged version, the fetch places it from the
// registry whatever the lock names, against the lock's integrity when the lock names it, else the registry's; one the
// registry lacks fails naming it, no wait (the check waited for its published).
tagged := tgzOf(t, sdkPackage, "0.1.16-feat-x.1")
tagged := pre
fakeRegistry(t, m, map[string][]byte{"0.1.15": held, "0.1.16-feat-x.1": tagged})
group := &groupPackage{Name: sdkPackage, Version: "0.1.16-feat-x.1"}
for _, c := range []struct {
@@ -140,8 +153,8 @@ func TestAMemberGetsTheSDKVersionItsLockNamesOrWaitsForIt(t *testing.T) {
}
// ADR 0500 §5 (S3, S4): the group's packed package is served in place of the registry's when the member's lock names its
// version with its integrity; another integrity, or a version neither the group nor the registry holds, fails naming both.
// A member whose lock names no SDK keeps the toolchain's and asks the registry nothing.
// version with its integrity; another integrity fails naming both. A member locking another version, or none, gets the
// registry's latest release (ADR 0573 §2); with no release in the registry it fails naming the group's, no wait.
func TestTheGroupsPackageIsServedToAMemberLockingItsVersion(t *testing.T) {
m, _, _ := newMachine(t)
asked := fakeRegistry(t, m, map[string][]byte{"0.1.14": tgzOf(t, sdkPackage, "0.1.14")})
@@ -159,14 +172,15 @@ func TestTheGroupsPackageIsServedToAMemberLockingItsVersion(t *testing.T) {
if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err != nil || placedVersion(dir) != "0.1.14" {
t.Errorf("a member locking the registry's 0.1.14 in a group: %v, placed %q", err, placedVersion(dir))
}
dir = memberLocking(t, "0.1.13", "sha512-x")
if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err == nil || !strings.Contains(err.Error(), "0.1.13") || !strings.Contains(err.Error(), "0.1.15") || strings.HasPrefix(err.Error(), builds.WaitsFor) {
t.Errorf("a version neither holds answered %v, want both named and no wait", err)
for _, locks := range []string{"0.1.13", ""} {
dir = memberLocking(t, locks, "sha512-x")
if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err != nil || placedVersion(dir) != "0.1.14" {
t.Errorf("a member in a group locking %q: %v, placed %q, want the registry's latest 0.1.14", locks, err, placedVersion(dir))
}
}
before := asked.Load()
dir = memberLocking(t, "", "")
if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err != nil || placedVersion(dir) != "" || asked.Load() != before {
t.Errorf("a lock naming no SDK: %v, placed %q, the registry asked", err, placedVersion(dir))
fakeRegistry(t, m, nil)
if err := m.placeSDK(context.Background(), memberLocking(t, "0.1.13", "sha512-x"), "modules/m", group, nil); err == nil || !strings.Contains(err.Error(), "0.1.15") || strings.HasPrefix(err.Error(), builds.WaitsFor) {
t.Errorf("no release and a group answered %v, want the group's named and no wait", err)
}
}
@@ -14,7 +14,7 @@ import (
// RecipeSteps is the recipe's own version: changed whenever the steps of fetch, test or build change in a way that
// can change what they produce (ADR 0405 §7). It is one of the two values the recipe version hashes.
const RecipeSteps = "2" // 2: a member is built against the SDK its own lock names (R4e, the lead's decision L10)
const RecipeSteps = "3" // 3: a member is built against the latest published SDK, the toolchain holds none (ADR 0573 §2)
// Recipe is every value that makes up the recipe version.
type Recipe struct {
@@ -3,7 +3,7 @@ package main
// A workspace and the packages a step runs against (novox/hq ADR 0469 §2 and §5; issues 1490, 1491). A workspace is
// <work dir>/<step id>/tree, made by the fetch step. The toolchain's packages are copied to <work dir>/<step id>/
// node_modules, which lies above the tree: node, tsc and esbuild resolve an import by walking up from the importing
// file, so @novox/mesh-sdk and the types are found there while the tree itself, which a bundle is packed from, stays
// file, so the types are found there while the tree itself, which a bundle is packed from, stays
// what the commit holds. A copy and not a link, because the mesh makes no symlink.
import (
-6
View File
@@ -8,7 +8,6 @@
"name": "mesh-build-node-toolchain",
"version": "1.0.0",
"dependencies": {
"@novox/mesh-sdk": "0.1.14",
"nats": "2.29.3"
},
"devDependencies": {
@@ -459,11 +458,6 @@
"node": ">=18"
}
},
"node_modules/@novox/mesh-sdk": {
"version": "0.1.14",
"resolved": "https://git.novox.be/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz",
"integrity": "sha512-ICdIfVicQQXSw6fkLiwzoRx3weiWh0jsIEYG/m98CKgFIlziZzb7V+ZmT/TvPO9bTDFDtfqDLoFu+OXY5+ryQw=="
},
"node_modules/@types/node": {
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
@@ -4,7 +4,6 @@
"private": true,
"description": "The Node build module's toolchain: what npm ci installs into <cache>/toolchain/<recipe version> (issue 1488).",
"dependencies": {
"@novox/mesh-sdk": "0.1.14",
"nats": "2.29.3"
},
"devDependencies": {