From b14b0eac621a30e72947bb1d2821c9cffe0dc857 Mon Sep 17 00:00:00 2001 From: Martyn Janes Date: Mon, 26 Oct 2026 15:25:56 +0800 Subject: [PATCH 1/2] fix: seed authorization before the server listens --- .github/workflows/soak.yaml | 19 +++++++++++++++++ apps/node/soak/README.md | 9 +++++++++ apps/node/soak/run-soak.mjs | 36 ++++++++++++++++++++++++++++++--- packages/node-core/src/start.ts | 12 ++++++----- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/.github/workflows/soak.yaml b/.github/workflows/soak.yaml index 095363bb..91b82b53 100644 --- a/.github/workflows/soak.yaml +++ b/.github/workflows/soak.yaml @@ -64,6 +64,16 @@ on: required: false type: string default: '' + runtime: + description: 'Runtime for a locally started node, node or bun, ignored by the cloud profile (blank = node)' + required: false + type: string + default: '' + web_server_type: + description: 'Web server for a locally started node, fastify, hono or native, ignored by the cloud profile (blank = node default)' + required: false + type: string + default: '' profile: description: 'Soak profile (cloud, iota-testnet, ...)' required: false @@ -124,6 +134,11 @@ jobs: } done + # Only needed when the harness spawns the node under Bun. + - name: Setup Bun + if: ${{ inputs.runtime == 'bun' }} + uses: oven-sh/setup-bun@v2 + - name: Install k6 uses: grafana/setup-k6-action@v1 @@ -157,6 +172,8 @@ jobs: INPUT_P99_MS: ${{ inputs.p99_ms }} INPUT_MEM_GROWTH_MB_PER_HR: ${{ inputs.mem_growth_mb_per_hr }} INPUT_DISABLED_GROUPS: ${{ inputs.disabled_groups }} + INPUT_RUNTIME: ${{ inputs.runtime }} + INPUT_WEB_SERVER_TYPE: ${{ inputs.web_server_type }} # SOAK_BASE_URL, SOAK_TENANT_API_KEY, SOAK_ADMIN_EMAIL, SOAK_ADMIN_PASSWORD, # and SOAK_SLACK_WEBHOOK_URL come from the ORG_SOAK_ENV_VARS blob loaded above. # Credentials stay in env (not CLI args) so they never appear in the process list. @@ -175,6 +192,8 @@ jobs: [ -n "$INPUT_P99_MS" ] && export SOAK_P99_MS="$INPUT_P99_MS" [ -n "$INPUT_MEM_GROWTH_MB_PER_HR" ] && export SOAK_MEM_GROWTH_MB_PER_HR="$INPUT_MEM_GROWTH_MB_PER_HR" [ -n "$INPUT_DISABLED_GROUPS" ] && export SOAK_DISABLED_GROUPS="$INPUT_DISABLED_GROUPS" + [ -n "$INPUT_RUNTIME" ] && export SOAK_RUNTIME="$INPUT_RUNTIME" + [ -n "$INPUT_WEB_SERVER_TYPE" ] && export SOAK_WEB_SERVER_TYPE="$INPUT_WEB_SERVER_TYPE" pnpm --dir apps/node run test:soak - name: Upload soak artifacts diff --git a/apps/node/soak/README.md b/apps/node/soak/README.md index ca7a6bd8..a7035b8b 100644 --- a/apps/node/soak/README.md +++ b/apps/node/soak/README.md @@ -63,6 +63,13 @@ thresholds are breached. pnpm --dir apps/node run test:soak ``` +To compare runtimes and web servers, set `SOAK_RUNTIME` and `SOAK_WEB_SERVER_TYPE`. Both are +recorded in `report.json` under `config`: + +```bash +SOAK_RUNTIME=bun SOAK_WEB_SERVER_TYPE=hono pnpm --dir apps/node run test:soak +``` + Phase 1 prints progress and exits 0 once the node is healthy. Server stdout/stderr is captured to `apps/node/soak/.out/server.log`; file storage lives under `apps/node/soak/.out/data` (both gitignored, recreated per run). @@ -75,6 +82,8 @@ All knobs are `SOAK_*` for the harness; the node still uses `TWIN_*`. Defaults s | ------------------------------------------ | --------------------------------- | --------------------------------------------------------------------------- | | `SOAK_PORT` | `3210` | Port the spawned node binds to | | `SOAK_PROFILE` | `file-local` | Selects `config/soak..env` | +| `SOAK_RUNTIME` | _(orchestrator's runtime)_ | Runtime the node is spawned with, `node` or `bun` | +| `SOAK_WEB_SERVER_TYPE` | _(node default, fastify)_ | Sets `TWIN_WEB_SERVER_TYPE`: `fastify`, `hono` or `native` | | `SOAK_TENANT_MODE` | `multi` | `multi` or `single` | | `SOAK_BOOT_TIMEOUT` | `120s` | Max wait for `/readyz` 200 | | `SOAK_SKIP_BOOTSTRAP` | `false` | Reuse existing state instead of bootstrapping | diff --git a/apps/node/soak/run-soak.mjs b/apps/node/soak/run-soak.mjs index 5b70da6b..7c3c78b5 100644 --- a/apps/node/soak/run-soak.mjs +++ b/apps/node/soak/run-soak.mjs @@ -31,6 +31,8 @@ import { createRemoteMemorySampler } from './remote-memory-sampler.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const APP_DIR = path.resolve(__dirname, '..'); const ENTRY = path.join('src', 'index.js'); // relative to APP_DIR (the node's cwd) +const RUNTIMES = ['node', 'bun']; +const WEB_SERVER_TYPES = ['fastify', 'hono', 'native']; // --------------------------------------------------------------------------- // Config (env-driven, with defaults). See plan §9. @@ -46,6 +48,10 @@ const cfg = { // SOAK_BASE_URL points to an externally-deployed node (e.g. cloud CI runs). skipServer: bool(process.env.SOAK_SKIP_SERVER, false), skipLoad: bool(process.env.SOAK_SKIP_LOAD, false), + // Runtime the node is spawned with, defaulting to the one running this orchestrator. + runtime: process.env.SOAK_RUNTIME ?? (process.versions.bun ? 'bun' : 'node'), + // Web server implementation, unset leaves the node's own default (fastify) or the profile's. + webServerType: process.env.SOAK_WEB_SERVER_TYPE, // Load (k6) parameters. // 2m used to be enough to show a real (if unenforced) memory slope on the bare default // invocation. Since the warm-up discard rose to 2m (see warmupDiscardMs below), a 2m run @@ -127,9 +133,18 @@ async function main() { // time-range deep-link once the dashboard grows one (see feat-262 implementation plan). const startedIso = new Date().toISOString(); + if (!RUNTIMES.includes(cfg.runtime)) { + throw new Error(`SOAK_RUNTIME must be one of ${RUNTIMES.join(', ')}, got "${cfg.runtime}"`); + } + if (cfg.webServerType !== undefined && !WEB_SERVER_TYPES.includes(cfg.webServerType)) { + throw new Error( + `SOAK_WEB_SERVER_TYPE must be one of ${WEB_SERVER_TYPES.join(', ')}, got "${cfg.webServerType}"` + ); + } + log( 'info', - `Soak orchestrator — profile=${cfg.profile} tenantMode=${cfg.tenantMode} port=${cfg.port}` + `Soak orchestrator — profile=${cfg.profile} tenantMode=${cfg.tenantMode} port=${cfg.port} runtime=${cfg.runtime} webServerType=${cfg.webServerType ?? 'default'}` ); log('info', `Output dir: ${OUT_DIR}`); @@ -351,6 +366,10 @@ async function buildNodeEnv() { TWIN_STRICT_ENV: cfg.strictEnv }; + if (cfg.webServerType !== undefined) { + env.TWIN_WEB_SERVER_TYPE = cfg.webServerType; + } + if (cfg.tenantMode === 'multi') { env.TWIN_TENANT_ENABLED = 'true'; env.TWIN_TENANT_ID = cfg.tenantId; @@ -429,7 +448,7 @@ async function startServer(nodeEnv) { log('info', 'Starting node server...'); const logStream = createWriteStream(SERVER_LOG, { flags: 'a' }); - serverChild = spawn(process.execPath, [ENTRY], { + serverChild = spawn(runtimeBin(), [ENTRY], { cwd: APP_DIR, env: { ...process.env, ...nodeEnv }, stdio: ['ignore', 'pipe', 'pipe'] @@ -445,11 +464,20 @@ async function startServer(nodeEnv) { }); } +/** + * The executable for SOAK_RUNTIME: this process's own when it matches, so the exact running + * version is reused, otherwise the runtime's command on PATH. + */ +function runtimeBin() { + const current = process.versions.bun ? 'bun' : 'node'; + return cfg.runtime === current ? process.execPath : cfg.runtime; +} + /** Run the node entry with extra args to completion, returning the exit code. */ function runToCompletion(args, env) { return new Promise((resolve, reject) => { const logStream = createWriteStream(SERVER_LOG, { flags: 'a' }); - const child = spawn(process.execPath, [ENTRY, ...args], { + const child = spawn(runtimeBin(), [ENTRY, ...args], { cwd: APP_DIR, env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'] @@ -777,6 +805,8 @@ async function writeReport({ config: { profile: cfg.profile, tenantMode: cfg.tenantMode, + runtime: cfg.runtime, + webServerType: cfg.webServerType ?? 'default', duration: cfg.duration, vus: cfg.vus, startedIso, diff --git a/packages/node-core/src/start.ts b/packages/node-core/src/start.ts index 1bc55b16..65a5c4f8 100644 --- a/packages/node-core/src/start.ts +++ b/packages/node-core/src/start.ts @@ -1,4 +1,5 @@ // Copyright 2024 IOTA Stiftung. +// Copyright 2026 3Sixty Trade Ltd. // SPDX-License-Identifier: Apache-2.0. import type { ITenantAdminComponent } from "@3sixty/api-models"; import { ContextIdKeys, ContextIdStore } from "@3sixty/context"; @@ -130,11 +131,12 @@ export async function start( try { initialiseMigrations(engine, envVars); - // Start the server, which also starts the engine. - await server.start(); - - await seedAuthorizationDefaults(engine, server, envVars); - await finalizeMigrations(engine, envVars); + // Start the server, which also starts the engine. The authorization model is seeded + // before the server listens, so no request is checked against a partial model. + await server.start(async () => { + await seedAuthorizationDefaults(engine, server, envVars); + await finalizeMigrations(engine, envVars); + }); return { engine, From b2be5a13f6b017091b6a10d0e0c3bf90a93121e7 Mon Sep 17 00:00:00 2001 From: Martyn Janes Date: Mon, 26 Oct 2026 15:43:40 +0800 Subject: [PATCH 2/2] chore: update dependencies --- pnpm-lock.yaml | 66 +++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed3a7934..d44f1bea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -328,22 +328,22 @@ importers: version: 0.20.0-next.2(typescript@6.0.3) '@3sixty/engine': specifier: next - version: 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + version: 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/engine-core': specifier: next - version: 0.20.0-next.3 + version: 0.20.0-next.4 '@3sixty/engine-models': specifier: next - version: 0.20.0-next.3 + version: 0.20.0-next.4 '@3sixty/engine-server': specifier: next - version: 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + version: 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/engine-server-types': specifier: next - version: 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + version: 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/engine-types': specifier: next - version: 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + version: 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/entity': specifier: next version: 0.20.0-next.4 @@ -762,28 +762,28 @@ packages: resolution: {integrity: sha512-MjZFg8YnEFGcE/7efxeGm41sJDo6ShCDOpS9BaCDbpciFwg3CrEv/iEmqis5J3RBOQ6P7UFMg8ibm6AIaYj5dg==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} - '@3sixty/engine-core@0.20.0-next.3': - resolution: {integrity: sha512-VEQ57B6pNLr9dqSKSC/715Et56eCAS6rdPOY1IIibsj23Wvm0E3zuYyDyPQT0QZTsudf7YbHyPRvirFlFLIEaA==} + '@3sixty/engine-core@0.20.0-next.4': + resolution: {integrity: sha512-EmaNkUu9x0iZVVFAxBx5+Mn4rXU6NBB/MR9+XKulSAjPzZ/TnJnMNIPeeLSXNvgWAEI5VF/2tShZJ4PmpglCXg==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} - '@3sixty/engine-models@0.20.0-next.3': - resolution: {integrity: sha512-10XlpfwEyTj5EDC4nYuPQsbLDZRdpwz9u9GiXf9JHCZ7Lr61/iwx0s3PjfsmBNjhF7p/ICaSrm5mvB6cwICX9A==} + '@3sixty/engine-models@0.20.0-next.4': + resolution: {integrity: sha512-4sd2V7RVmbNkNEPshxJxIP0lQdArqQIv5j+qds5GeCkkcF4+3yVSTLD79tcurhYPcfbhzl02KbWM61dIjqs46g==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} - '@3sixty/engine-server-types@0.20.0-next.3': - resolution: {integrity: sha512-v06XYmVGXH8CA2ksq2oI8sYJKAkYnizek4OQ37QzHiDHPCv+b6Hruz2rjjLnQXYYYao9vbrAbqwiZYXJpituQA==} + '@3sixty/engine-server-types@0.20.0-next.4': + resolution: {integrity: sha512-WvkpWibGHwHGHNdkJ+jJzuQWva+oRk38koG6OV2zmDq3TJP1qPhPSZxNAZIEsGD9Ki3aUU5yXQivRSPyYdCJxg==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} - '@3sixty/engine-server@0.20.0-next.3': - resolution: {integrity: sha512-R4ZkQpZOpdNWpEAkA9OohXDGE8ANignra4rgbO1D+S09q8dAJyuP/pk5om1FXOHoFuN5KKC2HM+DnA9kWOAeow==} + '@3sixty/engine-server@0.20.0-next.4': + resolution: {integrity: sha512-V4BCpzmdfXs1vxmY9qFZl3sgyuZ5eP8E33L1ybjEhSjFkuvxP3KY1DO2q8lLvlrag15cHeEDHXufTaqQxrHlXA==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} - '@3sixty/engine-types@0.20.0-next.3': - resolution: {integrity: sha512-zXMV/1+HGRBbAlwSFhGr0S/cf0Y7HF7ahi9QpKVA1DbcdtFrBXcU7wPPdOOqzPA11K9zR6xMzkgfBp99QLtD6A==} + '@3sixty/engine-types@0.20.0-next.4': + resolution: {integrity: sha512-4GYWk19QjVkrRG6l4jx0qmU254uPeN7Bq2AiRQs/0BtBn3LBCIJIwh5Sb+k5MWGzRatA64msrjMwuX9uENSreg==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} - '@3sixty/engine@0.20.0-next.3': - resolution: {integrity: sha512-DW3JkX1EcYuwIScb2ngfjIqjom4wJSImQONCF6i0W4a0q7KXZT6QvWk5LYZLRBmO6ETfsLZNYHqecL0CRXUhNQ==} + '@3sixty/engine@0.20.0-next.4': + resolution: {integrity: sha512-qjKiUDhj/Ks0zj5e16hKs6a76uernM62cfi0o9blJwj0rTJBaLyYNjeg2E+gHlxe6qNoX+kyCU6RErRUvSlL6w==} engines: {bun: '>=1.4.4', node: '>=24.0.0'} '@3sixty/entity-storage-connector-cosmosdb@0.20.0-next.2': @@ -6535,12 +6535,12 @@ snapshots: '@3sixty/telemetry-models': 0.20.0-next.2 '@3sixty/web': 0.20.0-next.4 - '@3sixty/engine-core@0.20.0-next.3': + '@3sixty/engine-core@0.20.0-next.4': dependencies: '@3sixty/context': 0.20.0-next.4 '@3sixty/core': 0.20.0-next.4 '@3sixty/data-core': 0.20.0-next.4 - '@3sixty/engine-models': 0.20.0-next.3 + '@3sixty/engine-models': 0.20.0-next.4 '@3sixty/entity': 0.20.0-next.4 '@3sixty/logging-connector-console': 0.20.0-next.2 '@3sixty/logging-models': 0.20.0-next.2 @@ -6548,7 +6548,7 @@ snapshots: '@3sixty/modules': 0.20.0-next.4 '@3sixty/nameof': 0.20.0-next.4 - '@3sixty/engine-models@0.20.0-next.3': + '@3sixty/engine-models@0.20.0-next.4': dependencies: '@3sixty/api-models': 0.20.0-next.2 '@3sixty/context': 0.20.0-next.4 @@ -6556,7 +6556,7 @@ snapshots: '@3sixty/entity': 0.20.0-next.4 '@3sixty/nameof': 0.20.0-next.4 - '@3sixty/engine-server-types@0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': + '@3sixty/engine-server-types@0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@3sixty/api-auth-entity-storage-rest-client': 0.20.0-next.2(supports-color@10.2.2) '@3sixty/api-auth-entity-storage-service': 0.20.0-next.2 @@ -6569,8 +6569,8 @@ snapshots: '@3sixty/authorization-service': 0.20.0-next.2 '@3sixty/context': 0.20.0-next.4 '@3sixty/core': 0.20.0-next.4 - '@3sixty/engine-models': 0.20.0-next.3 - '@3sixty/engine-types': 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + '@3sixty/engine-models': 0.20.0-next.4 + '@3sixty/engine-types': 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/nameof': 0.20.0-next.4 '@3sixty/telemetry-processors': 0.20.0-next.2 '@3sixty/tracing-processors': 0.20.0-next.2 @@ -6597,7 +6597,7 @@ snapshots: - typescript - utf-8-validate - '@3sixty/engine-server@0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': + '@3sixty/engine-server@0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@3sixty/api-models': 0.20.0-next.2 '@3sixty/api-server-fastify': 0.20.0-next.2(supports-color@10.2.2) @@ -6605,8 +6605,8 @@ snapshots: '@3sixty/api-server-native': 0.20.0-next.2(supports-color@10.2.2) '@3sixty/context': 0.20.0-next.4 '@3sixty/core': 0.20.0-next.4 - '@3sixty/engine-models': 0.20.0-next.3 - '@3sixty/engine-server-types': 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + '@3sixty/engine-models': 0.20.0-next.4 + '@3sixty/engine-server-types': 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/modules': 0.20.0-next.4 '@3sixty/nameof': 0.20.0-next.4 transitivePeerDependencies: @@ -6632,7 +6632,7 @@ snapshots: - typescript - utf-8-validate - '@3sixty/engine-types@0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': + '@3sixty/engine-types@0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@3sixty/api-models': 0.20.0-next.2 '@3sixty/api-rest-client': 0.20.0-next.2(supports-color@10.2.2) @@ -6682,7 +6682,7 @@ snapshots: '@3sixty/dlt-iota': 0.20.0-next.2(typescript@6.0.3) '@3sixty/document-management-rest-client': 0.20.0-next.2(supports-color@10.2.2) '@3sixty/document-management-service': 0.20.0-next.2 - '@3sixty/engine-models': 0.20.0-next.3 + '@3sixty/engine-models': 0.20.0-next.4 '@3sixty/entity-storage-connector-cosmosdb': 0.20.0-next.2(supports-color@10.2.2) '@3sixty/entity-storage-connector-dynamodb': 0.20.0-next.2 '@3sixty/entity-storage-connector-file': 0.20.0-next.2 @@ -6800,12 +6800,12 @@ snapshots: - typescript - utf-8-validate - '@3sixty/engine@0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': + '@3sixty/engine@0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@3sixty/core': 0.20.0-next.4 - '@3sixty/engine-core': 0.20.0-next.3 - '@3sixty/engine-models': 0.20.0-next.3 - '@3sixty/engine-types': 0.20.0-next.3(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) + '@3sixty/engine-core': 0.20.0-next.4 + '@3sixty/engine-models': 0.20.0-next.4 + '@3sixty/engine-types': 0.20.0-next.4(@iota/iota-sdk@1.15.0(typescript@6.0.3))(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(buffer@6.0.3)(socks@2.8.10)(supports-color@10.2.2)(typescript@6.0.3) '@3sixty/entity': 0.20.0-next.4 '@3sixty/nameof': 0.20.0-next.4 transitivePeerDependencies: