From 273fefff30e9c7b64ad4740f9240a8deed22bb72 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Fri, 15 Nov 2024 09:20:11 +0100 Subject: [PATCH 01/14] chore: intro of local cloud tests chore: cloud now generated pre-signed-urls chore: fix signature chore: remove env version chore: remove console.log chore: now we have cloud-backend tests chore: the ng redirecting backend chore: everything except subscript --- .dockerignore | 2 +- .gitignore | 1 + package-cloud-backend.json | 22 + package.json | 14 +- pnpm-lock.yaml | 113 +++- setup.cloud.ts | 14 + src/aws/gateway.ts | 2 +- src/connection-from-store.ts | 16 +- src/netlify/server.ts | 2 +- src/v2-cloud/backend/cf-dobj-abstract-sql.ts | 31 + src/v2-cloud/backend/cf-hono-server.ts | 194 ++++++ src/v2-cloud/backend/env.d.ts | 59 ++ src/v2-cloud/backend/fp-meta-groups.ts-off | 47 ++ src/v2-cloud/backend/server.ts | 72 +++ src/v2-cloud/backend/wrangler.toml | 143 +++++ src/v2-cloud/client/README.md | 58 ++ src/v2-cloud/client/cli-pre-signed-url.ts | 119 ++++ src/v2-cloud/client/cloud-gateway.test.ts | 123 ++++ src/v2-cloud/client/gateway.ts | 605 ++++++++++++++++++ src/v2-cloud/client/index.ts | 125 ++++ src/v2-cloud/cloud.test.ts-off | 533 +++++++++++++++ src/v2-cloud/connection.test.ts | 362 +++++++++++ src/v2-cloud/hono-server.ts | 265 ++++++++ src/v2-cloud/http-connection.ts | 178 ++++++ src/v2-cloud/meta-merger/abstract-sql.ts | 53 ++ .../meta-merger/bettersql-abstract-sql.ts | 36 ++ .../meta-merger/cf-worker-abstract-sql.ts | 31 + src/v2-cloud/meta-merger/create-schema-cli.ts | 9 + .../meta-merger/meta-by-tenant-ledger.ts | 173 +++++ src/v2-cloud/meta-merger/meta-merger.test.ts | 245 +++++++ src/v2-cloud/meta-merger/meta-merger.ts | 116 ++++ src/v2-cloud/meta-merger/meta-send.ts | 128 ++++ src/v2-cloud/meta-merger/tenant-ledger.ts | 62 ++ src/v2-cloud/meta-merger/tenant.ts | 51 ++ src/v2-cloud/msg-dispatch.ts | 139 ++++ src/v2-cloud/msg-dispatcher-impl.ts | 127 ++++ src/v2-cloud/msg-processor.ts-off | 261 ++++++++ src/v2-cloud/msg-raw-connection-base.ts | 31 + src/v2-cloud/msg-request.ts | 220 +++++++ src/v2-cloud/msg-type-meta.ts | 160 +++++ src/v2-cloud/msg-types-data.ts | 109 ++++ src/v2-cloud/msg-types-wal.ts | 130 ++++ src/v2-cloud/msg-types.ts | 567 ++++++++++++++++ src/v2-cloud/msger.ts | 274 ++++++++ src/v2-cloud/new-websocket.ts | 11 + src/v2-cloud/node-hono-server.ts | 142 ++++ src/v2-cloud/pre-signed-url.ts | 80 +++ src/v2-cloud/test-helper.ts | 217 +++++++ src/v2-cloud/ws-connection.ts | 211 ++++++ src/v2-cloud/ws-room.ts | 5 + tests/Dockerfile.connect-cloud | 19 + .../app/netlify/edge-functions/fireproof.ts | 2 +- tests/start-cloud.sh | 13 + tests/waiton.ts | 8 +- version-copy-package.ts | 147 +++-- vitest.v1-cloud.config.ts | 9 +- wrangler.toml | 5 - 57 files changed, 6791 insertions(+), 100 deletions(-) create mode 100644 package-cloud-backend.json create mode 100644 setup.cloud.ts create mode 100644 src/v2-cloud/backend/cf-dobj-abstract-sql.ts create mode 100644 src/v2-cloud/backend/cf-hono-server.ts create mode 100644 src/v2-cloud/backend/env.d.ts create mode 100644 src/v2-cloud/backend/fp-meta-groups.ts-off create mode 100644 src/v2-cloud/backend/server.ts create mode 100644 src/v2-cloud/backend/wrangler.toml create mode 100644 src/v2-cloud/client/README.md create mode 100644 src/v2-cloud/client/cli-pre-signed-url.ts create mode 100644 src/v2-cloud/client/cloud-gateway.test.ts create mode 100644 src/v2-cloud/client/gateway.ts create mode 100644 src/v2-cloud/client/index.ts create mode 100644 src/v2-cloud/cloud.test.ts-off create mode 100644 src/v2-cloud/connection.test.ts create mode 100644 src/v2-cloud/hono-server.ts create mode 100644 src/v2-cloud/http-connection.ts create mode 100644 src/v2-cloud/meta-merger/abstract-sql.ts create mode 100644 src/v2-cloud/meta-merger/bettersql-abstract-sql.ts create mode 100644 src/v2-cloud/meta-merger/cf-worker-abstract-sql.ts create mode 100644 src/v2-cloud/meta-merger/create-schema-cli.ts create mode 100644 src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts create mode 100644 src/v2-cloud/meta-merger/meta-merger.test.ts create mode 100644 src/v2-cloud/meta-merger/meta-merger.ts create mode 100644 src/v2-cloud/meta-merger/meta-send.ts create mode 100644 src/v2-cloud/meta-merger/tenant-ledger.ts create mode 100644 src/v2-cloud/meta-merger/tenant.ts create mode 100644 src/v2-cloud/msg-dispatch.ts create mode 100644 src/v2-cloud/msg-dispatcher-impl.ts create mode 100644 src/v2-cloud/msg-processor.ts-off create mode 100644 src/v2-cloud/msg-raw-connection-base.ts create mode 100644 src/v2-cloud/msg-request.ts create mode 100644 src/v2-cloud/msg-type-meta.ts create mode 100644 src/v2-cloud/msg-types-data.ts create mode 100644 src/v2-cloud/msg-types-wal.ts create mode 100644 src/v2-cloud/msg-types.ts create mode 100644 src/v2-cloud/msger.ts create mode 100644 src/v2-cloud/new-websocket.ts create mode 100644 src/v2-cloud/node-hono-server.ts create mode 100644 src/v2-cloud/pre-signed-url.ts create mode 100644 src/v2-cloud/test-helper.ts create mode 100644 src/v2-cloud/ws-connection.ts create mode 100644 src/v2-cloud/ws-room.ts create mode 100644 tests/Dockerfile.connect-cloud create mode 100644 tests/start-cloud.sh delete mode 100644 wrangler.toml diff --git a/.dockerignore b/.dockerignore index e11eebdf..3c8af178 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,2 @@ -**/node_modules/* **/.wrangler/* +**/node_modules/ diff --git a/.gitignore b/.gitignore index ef92d8f2..738c10ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Files +.wrangler .vscode .idea .DS_Store diff --git a/package-cloud-backend.json b/package-cloud-backend.json new file mode 100644 index 00000000..cd4c3be0 --- /dev/null +++ b/package-cloud-backend.json @@ -0,0 +1,22 @@ +{ + "name": "@fireproof/fireproof-cloud", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy --keep-vars", + "dev": "wrangler dev", + "start": "(env | grep -vi npm) > .dev.vars && wrangler dev --port 1968 --ip '::'", + "test": "vitest", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@adviser/cement": "from-main-package-json", + "@cloudflare/workers-types": "from-main-package-json", + "aws4fetch": "from-main-package-json", + "partyserver": "from-main-package-json", + "partysocket": "from-main-package-json", + "valibot": "from-main-package-json", + "wrangler": "from-main-package-json" + }, + "devDependencies": {} +} diff --git a/package.json b/package.json index 66ef9737..d8e8c842 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,9 @@ "url": "https://github.com/fireproof-storage/connect/issues" }, "homepage": "https://github.com/fireproof-storage/connect#readme", + "peerDependencies": { + "@adviser/cement": "^0.4.0" + }, "dependencies": { "@adviser/cement": "^0.4.0", "@aws-sdk/client-dynamodb": "^3.758.0", @@ -102,6 +105,7 @@ "@fireproof/core": "0.20.0-dev-preview-53", "@fireproof/vendor": "~2.0.0", "@ipld/dag-ucan": "^3.4.5", + "@hono/node-ws": "^1.0.4", "@jspm/core": "^2.1.0", "@netlify/blobs": "^8.1.1", "@ucanto/client": "^9.0.1", @@ -120,17 +124,23 @@ "aws-lambda": "^1.0.7", "aws4fetch": "^1.0.20", "better-sqlite3": "^11.8.1", + "cmd-ts": "^0.13.0", + "dotenv": "^16.4.5", "events": "^3.3.0", + "hono": "^4.6.11", "idb-keyval": "^6.2.1", "is-deep-strict-equal-x": "^1.1.2", "jose": "^6.0.8", "multiformats": "^13.3.2", "node-sqlite3-wasm": "^0.8.36", "partykit": "^0.0.111", - "partysocket": "^1.0.3", + "partyserver": "^0.0.65", + "partysocket": "^1.0.2", "path": "^0.12.7", + "smol-toml": "^1.3.1", "util": "^0.12.5", - "wait-on": "^8.0.2", + "valibot": "1.0.0-beta.7", + "vitest-pool-workers": "^0.0.1", "ws": "^8.18.1" }, "exports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03c6d422..909375d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@fireproof/vendor': specifier: ~2.0.0 version: 2.0.1 + '@hono/node-ws': + specifier: ^1.0.4 + version: 1.1.0(@hono/node-server@1.13.8(hono@4.7.4))(hono@4.7.4) '@ipld/dag-ucan': specifier: ^3.4.5 version: 3.4.5 @@ -95,9 +98,18 @@ importers: better-sqlite3: specifier: ^11.8.1 version: 11.8.1 + cmd-ts: + specifier: ^0.13.0 + version: 0.13.0 + dotenv: + specifier: ^16.4.5 + version: 16.4.7 events: specifier: ^3.3.0 version: 3.3.0 + hono: + specifier: ^4.6.11 + version: 4.7.4 idb-keyval: specifier: ^6.2.1 version: 6.2.1 @@ -116,18 +128,27 @@ importers: partykit: specifier: ^0.0.111 version: 0.0.111 + partyserver: + specifier: ^0.0.65 + version: 0.0.65(@cloudflare/workers-types@4.20250303.0) partysocket: - specifier: ^1.0.3 + specifier: ^1.0.2 version: 1.0.3 path: specifier: ^0.12.7 version: 0.12.7 + smol-toml: + specifier: ^1.3.1 + version: 1.3.1 util: specifier: ^0.12.5 version: 0.12.5 - wait-on: - specifier: ^8.0.2 - version: 8.0.2 + valibot: + specifier: 1.0.0-beta.7 + version: 1.0.0-beta.7(typescript@5.7.3) + vitest-pool-workers: + specifier: ^0.0.1 + version: 0.0.1 ws: specifier: ^8.18.1 version: 8.18.1 @@ -180,9 +201,6 @@ importers: eslint: specifier: ^9.22.0 version: 9.22.0 - partyserver: - specifier: ^0.0.65 - version: 0.0.65(@cloudflare/workers-types@4.20250303.0) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -210,6 +228,9 @@ importers: vitest: specifier: ^3.0.8 version: 3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + wait-on: + specifier: ^8.0.2 + version: 8.0.2 wrangler: specifier: ^3.112.0 version: 3.114.0(@cloudflare/workers-types@4.20250303.0) @@ -991,6 +1012,19 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hono/node-server@1.13.8': + resolution: {integrity: sha512-fsn8ucecsAXUoVxrUil0m13kOEq4mkX4/4QozCqmY+HpGfKl74OYSn8JcMA8GnG0ClfdRI4/ZSeG7zhFaVg+wg==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@hono/node-ws@1.1.0': + resolution: {integrity: sha512-uHaz1EPguJqsUmA+Jmhdi/DTRAMs2Fvcy7qno9E48rlK3WBtyGQw4u4DKlc+o18Nh1DGz2oA1n9hCzEyhVBeLw==} + engines: {node: '>=18.14.1'} + peerDependencies: + '@hono/node-server': ^1.11.1 + hono: ^4.6.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2025,6 +2059,9 @@ packages: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} engines: {node: '>=18'} + cmd-ts@0.13.0: + resolution: {integrity: sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g==} + collections-x@3.1.2: resolution: {integrity: sha512-UBj0YBtdMhubwaYnbfmCA8PHS79mbvCPYTw7YzUGhQzuyH3jf7Bc8RXQHqiZrTDtcl8+4oSbehFyu4jUH8PGag==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2143,10 +2180,17 @@ packages: devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dot-prop@7.2.0: resolution: {integrity: sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2534,6 +2578,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.7.4: + resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} + engines: {node: '>=16.9.0'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -3496,6 +3544,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + smol-toml@1.3.1: + resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3856,6 +3908,14 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + valibot@1.0.0-beta.7: + resolution: {integrity: sha512-8CsDu3tqyg7quEHMzCOYdQ/d9NlmVQKtd4AlFje6oJpvqo70EIZjSakKIeWltJyNAiUtdtLe0LAk4625gavoeQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} @@ -3912,6 +3972,9 @@ packages: yaml: optional: true + vitest-pool-workers@0.0.1: + resolution: {integrity: sha512-h8kyBR+e3lh0b2iGg56MEFSlHiByRiLIBJwEifvkacrzh38sB16tsGn6cfuIYF23iH72hE+EHM3aqvt3sqdInA==} + vitest@3.0.8: resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -5064,6 +5127,19 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hono/node-server@1.13.8(hono@4.7.4)': + dependencies: + hono: 4.7.4 + + '@hono/node-ws@1.1.0(@hono/node-server@1.13.8(hono@4.7.4))(hono@4.7.4)': + dependencies: + '@hono/node-server': 1.13.8(hono@4.7.4) + hono: 4.7.4 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -6394,6 +6470,15 @@ snapshots: is-wsl: 3.1.0 is64bit: 2.0.0 + cmd-ts@0.13.0: + dependencies: + chalk: 4.1.2 + debug: 4.4.0 + didyoumean: 1.2.2 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + collections-x@3.1.2: dependencies: array-for-each-x: 3.1.2 @@ -6525,10 +6610,14 @@ snapshots: devalue@4.3.3: {} + didyoumean@1.2.2: {} + dot-prop@7.2.0: dependencies: type-fest: 2.19.0 + dotenv@16.4.7: {} + eastasianwidth@0.2.0: {} electron-fetch@1.9.1: @@ -7012,6 +7101,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.7.4: {} + human-signals@5.0.0: {} iconv-lite@0.6.3: @@ -8148,6 +8239,8 @@ snapshots: is-arrayish: 0.3.2 optional: true + smol-toml@1.3.1: {} + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -8533,6 +8626,10 @@ snapshots: uuid@9.0.1: {} + valibot@1.0.0-beta.7(typescript@5.7.3): + optionalDependencies: + typescript: 5.7.3 + varint@6.0.0: {} vite-node@3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0): @@ -8578,6 +8675,8 @@ snapshots: tsx: 4.19.3 yaml: 2.7.0 + vitest-pool-workers@0.0.1: {} + vitest@3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.8 diff --git a/setup.cloud.ts b/setup.cloud.ts new file mode 100644 index 00000000..07650fdb --- /dev/null +++ b/setup.cloud.ts @@ -0,0 +1,14 @@ +import { BuildURI } from "@adviser/cement"; +import { registerFireproofCloudStoreProtocol } from "./src/cloud/client/gateway.ts"; +import dotenv from "dotenv"; + +registerFireproofCloudStoreProtocol(); + +dotenv.config(); + +process.env.FP_STORAGE_URL = BuildURI.from("fireproof://localhost:1968") + // .setParam("testMode", "true") + // .setParam("getBaseUrl", "https://storage.fireproof.direct/") + .setParam("protocol", "ws") + .toString(); +process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-fireproof-cloud?extractKey=_deprecated_internal_api"; diff --git a/src/aws/gateway.ts b/src/aws/gateway.ts index 77f4cf24..cdec3677 100644 --- a/src/aws/gateway.ts +++ b/src/aws/gateway.ts @@ -155,7 +155,7 @@ export class AWSGateway implements bs.Gateway { return this.logger.Error().Any({ resp: done }).Msg("failed to upload meta").ResultError(); } - const doneJson = (await done.json()) as { uploadURL: string }; + const doneJson = await done.json<{ uploadURL?: string }>(); if (!doneJson.uploadURL) { return this.logger.Error().Url(fetchUrl).Msg("Upload URL not found in the response").ResultError(); } diff --git a/src/connection-from-store.ts b/src/connection-from-store.ts index 89a9d50e..059d37d8 100644 --- a/src/connection-from-store.ts +++ b/src/connection-from-store.ts @@ -1,5 +1,5 @@ -import { BuildURI, runtimeFn } from "@adviser/cement"; -import { bs, Database, SuperThis } from "@fireproof/core"; +import { BuildURI } from "@adviser/cement"; +import { rt, SuperThis } from "@fireproof/core"; // export interface StoreOptions { // readonly data: bs.DataStore; @@ -74,18 +74,8 @@ import { bs, Database, SuperThis } from "@fireproof/core"; // } export function makeKeyBagUrlExtractable(sthis: SuperThis) { - let base = sthis.env.get("FP_KEYBAG_URL"); - if (!base) { - if (runtimeFn().isBrowser) { - base = "indexdb://fp-keybag"; - } else { - base = "file://./dist/kb-dir-partykit"; - } - } - const kbUrl = BuildURI.from(base); + const kbUrl = BuildURI.from(rt.kb.defaultKeyBagUrl(sthis)); kbUrl.defParam("extractKey", "_deprecated_internal_api"); sthis.env.set("FP_KEYBAG_URL", kbUrl.toString()); sthis.logger.Debug().Url(kbUrl, "keyBagUrl").Msg("Make keybag url extractable"); } - -export type ConnectFunction = (db: Database, name?: string, url?: string) => bs.Connection; diff --git a/src/netlify/server.ts b/src/netlify/server.ts index 564a63e8..5c566131 100644 --- a/src/netlify/server.ts +++ b/src/netlify/server.ts @@ -23,7 +23,7 @@ export default async (req: Request) => { return new Response(JSON.stringify({ ok: true }), { status: 201 }); } else if (metaDb) { const meta = getStore("meta"); - const x = (await req.json()) as CRDTEntry[]; + const x = await req.json(); // fixme, marty changed to [0] as it is a slice of the structure we expected const { data, cid, parents } = x[0]; await meta.setJSON(`${metaDb}/${cid}`, { data, parents }); diff --git a/src/v2-cloud/backend/cf-dobj-abstract-sql.ts b/src/v2-cloud/backend/cf-dobj-abstract-sql.ts new file mode 100644 index 00000000..37e44ab6 --- /dev/null +++ b/src/v2-cloud/backend/cf-dobj-abstract-sql.ts @@ -0,0 +1,31 @@ +// import { DurableObject } from "cloudflare:workers"; +import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "../meta-merger/abstract-sql.js"; +// import { Env } from "./env.js"; +import { ExecSQLResult, FPBackendDurableObject } from "./server.js"; + +export class CFDObjSQLStatement implements SQLStatement { + readonly sql: string; + readonly db: CFDObjSQLDatabase; + constructor(db: CFDObjSQLDatabase, sql: string) { + this.db = db; + this.sql = sql; + } + async run(...params: SQLParams): Promise { + const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; + return res.rawResults[0] as T; + } + async all(...params: SQLParams): Promise { + const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; + return res.rawResults as T[]; + } +} + +export class CFDObjSQLDatabase implements SQLDatabase { + readonly dobj: DurableObjectStub; + constructor(dobj: DurableObjectStub) { + this.dobj = dobj; + } + prepare(sql: string): SQLStatement { + return new CFDObjSQLStatement(this, sql); + } +} diff --git a/src/v2-cloud/backend/cf-hono-server.ts b/src/v2-cloud/backend/cf-hono-server.ts new file mode 100644 index 00000000..c190a7bb --- /dev/null +++ b/src/v2-cloud/backend/cf-hono-server.ts @@ -0,0 +1,194 @@ +import { HttpHeader, KeyedResolvOnce, Logger, LoggerImpl, URI } from "@adviser/cement"; +import { Context, Hono } from "hono"; +import { ConnMiddleware, HonoServerFactory, RunTimeParams, HonoServerBase } from "../hono-server.js"; +import { WSContext, WSContextInit, WSEvents } from "hono/ws"; +import { buildErrorMsg, defaultGestalt, EnDeCoder, Gestalt } from "../msg-types.js"; +// import { RequestInfo as CFRequestInfo } from "@cloudflare/workers-types"; +import { defaultMsgParams, jsonEnDe } from "../msger.js"; +import { ensureLogger, ensureSuperThis, SuperThis } from "@fireproof/core"; +import { SQLDatabase } from "../meta-merger/abstract-sql.js"; +import { CFWorkerSQLDatabase } from "../meta-merger/cf-worker-abstract-sql.js"; +import { CFDObjSQLDatabase } from "./cf-dobj-abstract-sql.js"; +import { Env } from "./env.js"; +import { WSRoom } from "../ws-room.js"; +import { FPBackendDurableObject, FPRoomDurableObject } from "./server.js"; + +const startedChs = new KeyedResolvOnce(); + +export function getBackendDurableObject(env: Env) { + // console.log("getDurableObject", env); + const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; + const rany = env as unknown as Record>; + const dObjNs = rany[cfBackendKey]; + const id = dObjNs.idFromName(env.FP_BACKEND_DO_ID ?? cfBackendKey); + return dObjNs.get(id); +} + +export function getRoomDurableObject(env: Env) { + // console.log("getDurableObject", env); + const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_WS_ROOM"; + const rany = env as unknown as Record>; + const dObjNs = rany[cfBackendKey]; + const id = dObjNs.idFromName(cfBackendKey); + return dObjNs.get(id); +} + +class CFWSRoom implements WSRoom { + readonly dobj: DurableObjectStub; + constructor(dobj: DurableObjectStub) { + this.dobj = dobj; + } + async acceptConnection(ws: WebSocket, wse: WSEvents): Promise { + const ret = await this.dobj.acceptWebSocket(ws, wse); + const wsCtx = new WSContext(ws as WSContextInit); + wse.onOpen?.({} as Event, wsCtx); + // return Promise.resolve(); + // ws.accept(); + return ret; + } +} + +export class CFHonoFactory implements HonoServerFactory { + readonly _onClose: () => void; + constructor( + onClose: () => void = () => { + /* */ + } + ) { + this._onClose = onClose; + } + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { + // this._env = c.env + const sthis = ensureSuperThis({ + logger: new LoggerImpl(), + }); + sthis.env.sets(c.env); + const logger = ensureLogger(sthis, `CFHono[${URI.from(c.req.url).pathname}]`); + const ende = jsonEnDe(sthis); + // this.sthis.env. + const fpProtocol = sthis.env.get("FP_PROTOCOL"); + const msgP = defaultMsgParams(sthis, { + hasPersistent: true, + protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], + }); + const gs = defaultGestalt(msgP, { + id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", + }); + + const wsRoom = new CFWSRoom(c.env); + const cfBackendMode = c.env.CF_BACKEND_MODE && c.env.CF_BACKEND_MODE === "DURABLE_OBJECT" ? "DURABLE_OBJECT" : "D1"; + let db: SQLDatabase; + switch (cfBackendMode) { + case "DURABLE_OBJECT": { + db = new CFDObjSQLDatabase(getBackendDurableObject(c.env)); + const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + // TODO WE NEED TO START THE DURABLE OBJECT + // but then on every request we import the schema + return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs })); + } + // break; + case "D1": + default: { + const cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"; + return startedChs + .get(cfBackendKey) + .once(async () => { + db = new CFWorkerSQLDatabase(c.env[cfBackendKey] as D1Database); + const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + await chs.start(); + return chs; + }) + .then((chs) => fn({ sthis, logger, ende, impl: chs })); + } + // break; + } + // return ret; // .then((v) => sthis.logger.Flush().then(() => v)) + } + + async start(_app: Hono): Promise { + // const { upgradeWebSocket } = await import("hono/cloudflare-workers"); + // this._upgradeWebSocket = upgradeWebSocket; + } + + async serve(_app: Hono, _port?: number): Promise { + return {} as T; + } + async close(): Promise { + this._onClose(); + return; + } +} + +export class CFHonoServer extends HonoServerBase { + // _upgradeWebSocket?: UpgradeWebSocket + + readonly ende: EnDeCoder; + // readonly env: Env; + // readonly wsConnections = new Map() + constructor( + sthis: SuperThis, + logger: Logger, + ende: EnDeCoder, + gs: Gestalt, + sqlDb: SQLDatabase, + wsRoom: WSRoom, + headers?: HttpHeader + ) { + super(sthis, logger, gs, sqlDb, wsRoom, headers); + this.ende = ende; + // this.env = env; + } + + // getDurableObject(conn: Connection) { + // const id = env.FP_META_GROUPS.idFromName("fireproof"); + // const stub = env.FP_META_GROUPS.get(id); + // } + + upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { + // if (!this._upgradeWebSocket) { + // throw new Error("upgradeWebSocket not implemented"); + // } + return async (conn, c, _next) => { + const upgradeHeader = c.req.header("Upgrade"); + if (!upgradeHeader || upgradeHeader !== "websocket") { + return new Response( + this.ende.encode(buildErrorMsg(this.sthis, this.logger, {}, new Error("expected Upgrade: websocket"))), + { status: 426 } + ); + } + // const env = c.env as Env; + // const id = env.FP_META_GROUPS.idFromName([conn.key.tenant, conn.key.ledger].join(":")); + // const dObj = env.FP_META_GROUPS.get(id); + // c.env.WS_EVENTS = createEvents(c); + // return dObj.fetch(c.req.raw as unknown as CFRequestInfo) as unknown as Promise; + // this._upgradeWebSocket!(createEvents)(c, next); + + const { 0: client, 1: server } = new WebSocketPair(); + conn.attachWSPair({ client, server }); + + const wsEvents = await createEvents(c); + // console.log("upgradeWebSocket", c.req.url); + + // const wsCtx = new WSContext(server as WSContextInit); + + // server.onopen = (ev) => { + // console.log("onopen", ev); + // wsEvents.onOpen?.(ev, wsCtx); + // } + + await this.wsRoom.acceptConnection(server, wsEvents); + + // server.send("Hello from server"); + + // this.wsConnections.set(this.sthis.nextId().str, { client, server }); + // const client = webSocketPair[0], + // server = webSocketPair[1]; + + return new Response(null, { + status: 101, + webSocket: client, + }); + }; + } +} diff --git a/src/v2-cloud/backend/env.d.ts b/src/v2-cloud/backend/env.d.ts new file mode 100644 index 00000000..a3f757e1 --- /dev/null +++ b/src/v2-cloud/backend/env.d.ts @@ -0,0 +1,59 @@ +// Generated by Wrangler on Fri Aug 16 2024 13:55:06 GMT+0200 (Central European Summer Time) +// by running `wrangler types` + +import type { DurableObjectNamespace } from "@cloudflare/workers-types"; +// import { WSEvents } from "hono/ws"; +import { FPRoomDurableObject, FPBackendDurableObject } from "./server.ts"; + +export interface Env { + // bucket: R2Bucket; + // kv_store: KVNamespace; + + /** AWS/S3 access key ID for storage backend */ + ACCESS_KEY_ID: string; + ACCOUNT_ID: string; + BUCKET_NAME: string; + CLOUDFLARE_API_TOKEN: string; + EMAIL: string; + FIREPROOF_SERVICE_PRIVATE_KEY: string; + POSTMARK_TOKEN: string; + SECRET_ACCESS_KEY: string; + SERVICE_ID: string; + STORAGE_URL: string; + REGION: string; + VERSION: string; + FP_DEBUG: string; + FP_STACK: string; + FP_FORMAT: string; + FP_PROTOCOL: string; + /** Test date in ISO8601 format (YYYYMMDD'T'HHmmss'Z'). Optional. */ + TEST_DATE?: string; + /** Maximum idle time in seconds before connection timeout. Optional. */ + MAX_IDLE_TIME?: string; + + // default D1 + CF_BACKEND_MODE: "D1" | "DURABLE_OBJECT"; + // default D1 "FP_BACKEND_D1" + // default DURABLE_OBJECT "FP_BACKEND_DO" + CF_BACKEND_KEY?: string; + + FP_BACKEND_D1: D1Database; + + FP_BACKEND_DO: DurableObjectNamespace; + // default CF_BACKEND_KEY + FP_BACKEND_DO_ID: string; + + // default "FP_WS_ROOM" + CF_WS_ROOM_KEY: string; + + FP_WS_ROOM: DurableObjectNamespace; + + // WS_EVENTS: WSEvents; +} + +// declare module "cloudflare:test" { +// // ...or if you have an existing `Env` type... +// interface ProvidedEnv extends Env { +// readonly test: boolean; +// } +// } diff --git a/src/v2-cloud/backend/fp-meta-groups.ts-off b/src/v2-cloud/backend/fp-meta-groups.ts-off new file mode 100644 index 00000000..d18d3aae --- /dev/null +++ b/src/v2-cloud/backend/fp-meta-groups.ts-off @@ -0,0 +1,47 @@ +import { DurableObject } from "cloudflare:workers"; +import { Env } from "./env.js"; + +export class FPMetaGroups extends DurableObject { + currentlyConnectedWebSockets: number; + + constructor(ctx: DurableObjectState, env: Env) { + // This is reset whenever the constructor runs because + // regular WebSockets do not survive Durable Object resets. + // + // WebSockets accepted via the Hibernation API can survive + // a certain type of eviction, but we will not cover that here. + super(ctx, env); + this.currentlyConnectedWebSockets = 0; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async fetch(request: Request): Promise { + // Creates two ends of a WebSocket connection. + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + // Calling `accept()` tells the runtime that this WebSocket is to begin terminating + // request within the Durable Object. It has the effect of "accepting" the connection, + // and allowing the WebSocket to send and receive messages. + server.accept(); + this.currentlyConnectedWebSockets += 1; + + // Upon receiving a message from the client, the server replies with the same message, + // and the total number of connections with the "[Durable Object]: " prefix + // eslint-disable-next-line @typescript-eslint/no-unused-vars + server.addEventListener("message", (event: MessageEvent) => { + server.send(`[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`); + }); + + // If the client closes the connection, the runtime will close the connection too. + server.addEventListener("close", (cls: CloseEvent) => { + this.currentlyConnectedWebSockets -= 1; + server.close(cls.code, "Durable Object is closing WebSocket"); + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); + } +} diff --git a/src/v2-cloud/backend/server.ts b/src/v2-cloud/backend/server.ts new file mode 100644 index 00000000..c0f100b6 --- /dev/null +++ b/src/v2-cloud/backend/server.ts @@ -0,0 +1,72 @@ +// / +// import { Logger } from "@adviser/cement"; +// import { Hono } from "hono"; +import { DurableObject } from "cloudflare:workers"; +import { HonoServer } from "../hono-server.js"; +import { Hono } from "hono"; +import { Env } from "./env.js"; +import { CFHonoFactory } from "./cf-hono-server.js"; +import { WSContext, WSContextInit, WSEvents } from "hono/ws"; + +const app = new Hono(); +const honoServer = new HonoServer(new CFHonoFactory()); + +export default { + fetch: async (req, env, ctx): Promise => { + await honoServer.register(app); + return app.fetch(req, env, ctx); + }, +} satisfies ExportedHandler; +/* + async fetch(req, env, _ctx): Promise { + const id = env.FP_META_GROUPS.idFromName("fireproof"); + const stub = env.FP_META_GROUPS.get(id); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return stub.fetch(req as any) as unknown as Promise; + }, +} satisfies ExportedHandler; +*/ + +export interface ExecSQLResult { + readonly rowsRead: number; + readonly rowsWritten: number; + readonly rawResults: unknown[]; +} + +export class FPBackendDurableObject extends DurableObject { + async execSql(sql: string, params: unknown[]): Promise { + const cursor = await this.ctx.storage.sql.exec(sql, ...params); + const rawResults = cursor.toArray(); + const res = { + rowsRead: cursor.rowsRead, + rowsWritten: cursor.rowsWritten, + rawResults, + }; + // console.log("execSql", sql, params, res); + return res; + } +} + +export class FPRoomDurableObject extends DurableObject { + private wsEvents?: WSEvents; + + async acceptWebSocket(ws: WebSocket, wsEvents: WSEvents): Promise { + this.ctx.acceptWebSocket(ws); + this.wsEvents = wsEvents; + } + + webSocketError(ws: WebSocket, error: unknown): void | Promise { + const wsCtx = new WSContext(ws as WSContextInit); + this.wsEvents?.onError?.(error as Event, wsCtx); + } + + async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer): Promise { + const wsCtx = new WSContext(ws as WSContextInit); + this.wsEvents?.onMessage?.({ data: msg } as MessageEvent, wsCtx); + } + + webSocketClose(ws: WebSocket, code: number, reason: string): void | Promise { + const wsCtx = new WSContext(ws as WSContextInit); + this.wsEvents?.onClose?.({ code, reason } as CloseEvent, wsCtx); + } +} diff --git a/src/v2-cloud/backend/wrangler.toml b/src/v2-cloud/backend/wrangler.toml new file mode 100644 index 00000000..dfdee3a2 --- /dev/null +++ b/src/v2-cloud/backend/wrangler.toml @@ -0,0 +1,143 @@ +name = "fireproof-cloud" +main = "server.ts" +compatibility_date = "2024-04-19" +compatibility_flags = ["nodejs_compat"] +# upload_source_maps = true + +# [durable_objects] +# bindings = [ +# { name = "FP_DO", class_name = "FPDurableObject"}, +# ] + +# [[d1_databases]] +# binding = "DB" +# database_name = "test-meta-merge" +# database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + +[durable_objects] +bindings = [ + # { name = "FP_DO", class_name = "FPDurableObject", script_name = "cf-dobj-abstract-sql" } + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[[migrations]] +tag = "v1" # Should be unique for each entry +new_sqlite_classes = ["FPBackendDurableObject"] + +[observability] +enabled = true +head_sampling_rate = 1 + + +[env.test.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "http" +CF_BACKEND_MODE = "DURABLE_OBJECT" + +# [env.test.services] +# bindings = [ +# { binding = "FP_DO", service = "FP_DO" } +# ] + +[[env.test.d1_databases]] +binding = "FP_BACKEND_D1" +database_name = "test-meta-merge" +database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + +[[env.test.migrations]] +tag = "v1" # Should be unique for each entry +new_sqlite_classes = ["FPBackendDurableObject"] + +[env.test.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + + +[env.test-reqRes-D1.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "http" + +[[env.test-reqRes-D1.d1_databases]] +binding = "FP_BACKEND_D1" +database_name = "test-meta-merge" +database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + +[env.test-reqRes-D1.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[env.test-reqRes-DO.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "http" +CF_BACKEND_MODE = "DURABLE_OBJECT" + +[env.test-reqRes-DO.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[env.test-stream-D1.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "ws" + + +[env.test-stream-D1.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[[env.test-stream-D1.d1_databases]] +binding = "FP_BACKEND_D1" +database_name = "test-meta-merge" +database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + + +[env.test-stream-DO.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "ws" +CF_BACKEND_MODE = "DURABLE_OBJECT" + +[env.test-stream-DO.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + + diff --git a/src/v2-cloud/client/README.md b/src/v2-cloud/client/README.md new file mode 100644 index 00000000..ec73ad43 --- /dev/null +++ b/src/v2-cloud/client/README.md @@ -0,0 +1,58 @@ +# Fireproof Cloud + +This gateway intended for use with Fireproof Cloud. + +## Usage + +You can call the `connect` function with a database and it will provision a remote UUID for the database, and sync the database to the remote. It will also log a URL to the console that you can open in a browser to connect to the database, as well as try to open the URL in a new tab. Tell us what you think about this workflow! + +```typescript +import { fireproof } from "@fireproof/core"; +import { connect } from "@fireproof/cloud"; + +const database = await fireproof("my-db-name"); +const connection = await connect(database); +``` + +### With React Hooks + +In a React component, you can use the `useFireproof` hook to get the database and then call `connect` (it is safe to call `connect` multiple times, but in this example we're using a state variable to store the dashboard URL). + +```typescript +import { useFireproof } from "use-fireproof"; +import { connect } from "@fireproof/cloud"; + +const { database } = useFireproof("my-db-name"); +const [dashboardUrl, setDashboardUrl] = useState(); + +// there is a useConnection hook coming soon +useEffect(() => { + connect(database).then((connection) => { + setDashboardUrl(connection.dashboardUrl?.toString()); + }); +}, [database]); +``` + +## The Second Argument + +The second argument to `connect` is the remote database name. This will be assigned for you if you don't provide one, and the created name will be persisted locally. + +The most common way to use this is if you want to sync to a remote database. The UUID will have been assigned when on first sync, and now you want to connect a new client to that remote. + +```typescript +const connection = await connect(database, "my-remote-uuid"); +``` + +If you provide a name, it will be used as the remote database name. If you want to control the name, you should use a prefix unique to your app, so no one else uses your endpoint. This is useful if you want the database name to come from your URL slug, like `/my-app/my-db-name`. + +```typescript +const connection = await connect(database, `com.my-app.v1.${database.name}`); +``` + +Note: if your database already has data in it, connecting to a new remote will do nothing. To prevent data lost, you need to rename the local database to an unused name and the connect. + +## No Warranty, For Evaluation Purposes + +This preview of Fireproof Cloud doesn't even have login, so don't expect your data to be persisted, etc. Please give us feedback on the workflow! We'll be adding login and access control soon. + +The source of truth on this stuff is the team. Join us on [Discord](https://discord.gg/cCryrNHePH) if you want to chat! diff --git a/src/v2-cloud/client/cli-pre-signed-url.ts b/src/v2-cloud/client/cli-pre-signed-url.ts new file mode 100644 index 00000000..8419d631 --- /dev/null +++ b/src/v2-cloud/client/cli-pre-signed-url.ts @@ -0,0 +1,119 @@ +// small tool to generate pre-signed url for cloud storage +// curl $(npx tsx src/cloud/client/cli-pre-signed-url.ts GET) +// curl -X PUT --data-binary @/etc/protocols $(npx tsx src/cloud/client/cli-pre-signed-url.ts) +import { BuildURI } from "@adviser/cement"; +import { AwsClient } from "aws4fetch"; +import dotenv from "dotenv"; +import { command, run, option, oneOf, string } from "cmd-ts"; +import { ensureSuperThis } from "@fireproof/core"; +// import * as t from 'io-ts'; + +(async () => { + dotenv.config(); + const sthis = ensureSuperThis(); + const cmd = command({ + name: "cli-pre-signed-url", + description: "sign a url for cloud storage", + version: "1.0.0", + args: { + method: option({ + long: "method", + type: oneOf(["GET", "PUT", "POST", "DELETE"]), + defaultValue: () => "PUT", + defaultValueIsSerializable: true, + }), + accessKeyId: option({ + long: "accessKeyId", + type: string, + defaultValue: () => sthis.env.get("CF_ACCESS_KEY_ID") || "accessKeyId", + defaultValueIsSerializable: true, + }), + secretAccessKey: option({ + long: "secretAccessKey", + type: string, + defaultValue: () => sthis.env.get("CF_SECRET_ACCESS_KEY") || "secretAccessKey", + defaultValueIsSerializable: true, + }), + region: option({ + long: "region", + type: string, + defaultValue: () => "us-east-1", + defaultValueIsSerializable: true, + }), + service: option({ + long: "service", + type: string, + defaultValue: () => "s3", + defaultValueIsSerializable: true, + }), + storageURL: option({ + long: "storageURL", + type: string, + defaultValue: () => sthis.env.get("CF_STORAGE_URL") || "https://bucket.example.com/db/main", + defaultValueIsSerializable: true, + }), + path: option({ + long: "path", + type: string, + defaultValue: () => "db/main", + defaultValueIsSerializable: true, + }), + expires: option({ + long: "expires", + type: string, + defaultValue: () => "3600", + defaultValueIsSerializable: true, + }), + now: option({ + long: "now", + type: { + async from(str): Promise { + const decoded = new Date(str); + if (isNaN(decoded.getTime())) { + throw new Error("invalid date"); + } + // 2021-09-01T12:34:56Z + return decoded + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d+Z$/, "Z"); + }, + displayName: "WithoutMillis", + description: "without milliseconds", + }, + // 2021-09-01T12:34:56Z + // 2024-11-17T07:21:10.958Z + defaultValue: () => + new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d+Z$/, "Z"), + defaultValueIsSerializable: true, + }), + }, + handler: async (args) => { + const a4f = new AwsClient({ + accessKeyId: args.accessKeyId, + secretAccessKey: args.secretAccessKey, + region: args.region, + service: args.service, + }); + const buildUrl = BuildURI.from(args.storageURL).appendRelative(args.path).setParam("X-Amz-Expires", args.expires); + + // eslint-disable-next-line no-console + console.log( + await a4f + .sign(new Request(buildUrl.toString(), { method: args.method }), { + aws: { + signQuery: true, + datetime: args.now, + }, + }) + .then((res) => res.url) + ); + }, + }); + + await run(cmd, process.argv.slice(2)); + // eslint-disable-next-line no-console +})().catch(console.error); diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts new file mode 100644 index 00000000..b30f07fe --- /dev/null +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -0,0 +1,123 @@ +import { Hono } from "hono"; +import { HonoServer } from "../hono-server.js"; +import { defaultGestalt } from "../msg-types.js"; +import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle } from "../test-helper.js"; +import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; +import { defaultMsgParams } from "../msger.js"; +import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; +import { BuildURI } from "@adviser/cement"; + +const sthis = ensureSuperThis(); +const msgP = defaultMsgParams(sthis, { hasPersistent: true }); +const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); + +describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gateway", ({ factory }) => { + const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); + const style = wsStyle(sthis, port, msgP, my); + + let server: HonoServer; + let gw: bs.Gateway; + let unregister: () => void; + let url: BuildURI; + beforeAll(async () => { + const app = new Hono(); + server = await factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.register(app, port)); + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + gw = new FireproofCloudGateway(sthis); + url = BuildURI.from(`fireproof://localhost:${port}/`) + .setParam("protocol", "http") + .setParam("name", "ledger-name") + .setParam("tenant", "tendant"); + }); + afterAll(async () => { + await server.close(); + unregister(); + }); + describe("data", () => { + it("get not found", async () => { + await Promise.all( + Array(20) + .fill(async () => { + url.setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + + it("put - get - del - get", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const resStart = await gw.start(url.URI()); + expect(resStart.isOk()).toBeTruthy(); + + url.setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + }); + + describe("WAL", () => { + it("get not found", async () => { + await Promise.all( + Array(20) + .fill(async () => { + url.setParam("store", "wal"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + + it("put - get - del - get", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const resStart = await gw.start(url.URI()); + expect(resStart.isOk()).toBeTruthy(); + + url.setParam("store", "wal"); + const key = `theWALKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + }); +}); diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts new file mode 100644 index 00000000..a221fcef --- /dev/null +++ b/src/v2-cloud/client/gateway.ts @@ -0,0 +1,605 @@ +// import PartySocket, { PartySocketOptions } from "partysocket"; +import { Result, URI, KeyedResolvOnce, exception2Result, key } from "@adviser/cement"; +import { bs, ensureLogger, Logger, NotFoundError, rt, SuperThis } from "@fireproof/core"; +import { + buildErrorMsg, + buildReqOpen, + FPStoreTypes, + HttpMethods, + MsgBase, + MsgIsError, + ReqSignedUrl, + MsgWithError, + ResSignedUrl, +} from "../msg-types.js"; +import { to_uint8 } from "../../coerce-binary.js"; +import { MsgConnected, Msger } from "../msger.js"; +import { MsgIsResGetData, MsgIsResPutData, ResDelData, ResGetData, ResPutData } from "../msg-types-data.js"; + +const VERSION = "v0.1-fp-cloud"; + +export interface StoreTypeGateway { + get(uri: URI, conn: Promise>): Promise>; + put(uri: URI, body: Uint8Array, conn: Promise>): Promise>; + delete(uri: URI, conn: Promise>): Promise>; +} + +abstract class BaseGateway { + readonly logger: Logger; + readonly sthis: SuperThis; + constructor(sthis: SuperThis, module: string) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, module); + } + + abstract getConn(uri: URI, conn: MsgConnected): Promise>; + async get(uri: URI, prConn: Promise>): Promise> { + const rConn = await prConn; + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in getConn").ResultError(); + } + const conn = rConn.Ok(); + // this.logger.Debug().Any("conn", conn.key).Msg("get"); + return this.getConn(uri, conn); + } + + abstract putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise>; + async put(uri: URI, body: Uint8Array, prConn: Promise>): Promise> { + const rConn = await prConn; + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); + } + const conn = rConn.Ok(); + // this.logger.Debug().Any("conn", conn.key).Msg("put"); + return this.putConn(uri, body, conn); + } + + abstract delConn(uri: URI, conn: MsgConnected): Promise>; + async delete(uri: URI, prConn: Promise>): Promise> { + const rConn = await prConn; + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); + } + const conn = rConn.Ok(); + // this.logger.Debug().Any("conn", conn.key).Msg("del"); + return this.delConn(uri, conn); + } + + // prepareReqSignedUrl(type: string, method: HttpMethods, store: FPStoreTypes, uri: URI, conn: Connection): Result { + + // const sig = { + // conn, + // params: { + // method, + // store, + // key: uri.getParam(" + // } satisfies ReqSignedUrl; + // return Result.Ok(buildReqSignedUrl(this.sthis, type, sig, conn)) + // } + + async getResSignedUrl( + type: string, + method: HttpMethods, + store: FPStoreTypes, + waitForFn: (msg: MsgBase) => boolean, + uri: URI, + conn: MsgConnected + ): Promise> { + const rParams = uri.getParamsResult({ + key: key.REQUIRED, + store: key.REQUIRED, + path: key.OPTIONAL, + tenant: key.REQUIRED, + name: key.REQUIRED, + index: key.OPTIONAL, + }); + if (rParams.isErr()) { + return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, rParams.Err()); + } + const params = rParams.Ok(); + if (store !== params.store) { + return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, new Error("store mismatch")); + } + const rsu = { + tid: this.sthis.nextId().str, + type, + // conn: conn.conn, + tenant: { + tenant: params.tenant, + ledger: params.name, + }, + // tenant: conn.tenant, + params: { + method, + store, + ...params, + key: params.key, + }, + version: VERSION, + } as ReqSignedUrl; + return conn.request(rsu, { waitFor: waitForFn }); + } + + async putObject(uri: URI, uploadUrl: string, body: Uint8Array): Promise> { + this.logger.Debug().Any("url", { uploadUrl, uri }).Msg("put-fetch-url"); + const rUpload = await exception2Result(async () => fetch(uploadUrl, { method: "PUT", body })); + if (rUpload.isErr()) { + return this.logger.Error().Url(uploadUrl, "uploadUrl").Err(rUpload).Msg("Error in put fetch").ResultError(); + } + if (!rUpload.Ok().ok) { + return this.logger.Error().Url(uploadUrl, "uploadUrl").Http(rUpload.Ok()).Msg("Error in put fetch").ResultError(); + } + if (uri.getParam("testMode")) { + trackPuts.add(uri.toString()); + } + return Result.Ok(undefined); + } + + async getObject(uri: URI, downloadUrl: string): Promise> { + this.logger.Debug().Any("url", { downloadUrl, uri }).Msg("get-fetch-url"); + const rDownload = await exception2Result(async () => fetch(downloadUrl.toString(), { method: "GET" })); + if (rDownload.isErr()) { + return this.logger + .Error() + .Url(downloadUrl, "uploadUrl") + .Err(rDownload) + .Msg("Error in get downloadUrl") + .ResultError(); + } + const download = rDownload.Ok(); + if (!download.ok) { + if (download.status === 404) { + return Result.Err(new NotFoundError("Not found")); + } + return this.logger.Error().Url(downloadUrl, "uploadUrl").Err(rDownload).Msg("Error in get fetch").ResultError(); + } + return Result.Ok(to_uint8(await download.arrayBuffer())); + } + + async delObject(uri: URI, deleteUrl: string): Promise> { + this.logger.Debug().Any("url", { deleteUrl, uri }).Msg("get-fetch-url"); + const rDelete = await exception2Result(async () => fetch(deleteUrl.toString(), { method: "DELETE" })); + if (rDelete.isErr()) { + return this.logger.Error().Url(deleteUrl, "deleteUrl").Err(rDelete).Msg("Error in get deleteURL").ResultError(); + } + const download = rDelete.Ok(); + if (!download.ok) { + if (download.status === 404) { + return Result.Err(new NotFoundError("Not found")); + } + return this.logger.Error().Url(deleteUrl, "deleteUrl").Err(rDelete).Msg("Error in del fetch").ResultError(); + } + return Result.Ok(undefined); + } +} + +class DataGateway extends BaseGateway implements StoreTypeGateway { + constructor(sthis: SuperThis) { + super(sthis, "DataGateway"); + } + async getConn(uri: URI, conn: MsgConnected): Promise> { + // type: string, method: HttpMethods, store: FPStoreTypes, waitForFn: + const rResSignedUrl = await this.getResSignedUrl( + "reqGetData", + "GET", + "data", + MsgIsResGetData, + uri, + conn + ); + if (MsgIsError(rResSignedUrl)) { + return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); + } + const { signedUrl: downloadUrl } = rResSignedUrl; + return this.getObject(uri, downloadUrl); + } + async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { + const rResSignedUrl = await this.getResSignedUrl( + "reqPutData", + "PUT", + "data", + MsgIsResPutData, + uri, + conn + ); + if (MsgIsError(rResSignedUrl)) { + return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); + } + const { signedUrl: uploadUrl } = rResSignedUrl; + return this.putObject(uri, uploadUrl, body); + } + async delConn(uri: URI, conn: MsgConnected): Promise> { + const rResSignedUrl = await this.getResSignedUrl( + "reqDelData", + "DELETE", + "data", + MsgIsResPutData, + uri, + conn + ); + if (MsgIsError(rResSignedUrl)) { + return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); + } + const { signedUrl: deleteUrl } = rResSignedUrl; + return this.delObject(uri, deleteUrl); + } +} + +class MetaGateway extends BaseGateway implements StoreTypeGateway { + constructor(sthis: SuperThis) { + super(sthis, "MetaGateway"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getConn(uri: URI, conn: MsgConnected): Promise> { + // const rkey = uri.getParamResult("key"); + // if (rkey.isErr()) { + // return Result.Err(rkey.Err()); + // } + // const rsu = buildReqGetMeta(this.sthis, conn.key, { + // ...conn.key, + // method: "GET", + // store: "meta", + // key: rkey.Ok(), + // }); + // const rRes = await conn.request(rsu, { + // waitType: "resGetMeta", + // }); + // if (rRes.isErr()) { + // return Result.Err(rRes.Err()); + // } + // const res = rRes.Ok(); + // if (MsgIsError(res)) { + // return Result.Err(res); + // } + // if (res.signedGetUrl) { + // return this.getObject(uri, res.signedGetUrl); + // } + // return Result.Ok(this.sthis.txt.encode(JSON.stringify(res.metas))); + return Result.Ok(new Uint8Array()); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { + // const bodyRes = Result.Ok(body); // await bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body); + // if (bodyRes.isErr()) { + // return this.logger.Error().Err(bodyRes).Msg("Error in addCryptoKeyToGatewayMetaPayload").ResultError(); + // } + // const rsu = this.prepareReqSignedUrl(uri, "PUT", conn.key); + // if (rsu.isErr()) { + // return Result.Err(rsu.Err()); + // } + // const dbMetas = JSON.parse(this.sthis.txt.decode(bodyRes.Ok())) as CRDTEntry[]; + // this.logger.Debug().Any("dbMetas", dbMetas).Msg("putMeta"); + // const req = buildReqPutMeta(this.sthis, conn.key, rsu.Ok().params, dbMetas); + // const res = await conn.request(req, { waitType: "resPutMeta" }); + // if (res.isErr()) { + // return Result.Err(res.Err()); + // } + // // console.log("putMeta", JSON.stringify({dbMetas, res})); + // this.logger.Debug().Any("qs", { req, res: res.Ok() }).Msg("putMeta"); + // this.putObject(uri, res.Ok().signedPutUrl, bodyRes.Ok()); + // return res; + return Result.Ok(undefined); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async delConn(uri: URI, conn: MsgConnected): Promise> { + // const rsu = this.prepareReqSignedUrl(uri, "DELETE", conn.key); + // if (rsu.isErr()) { + // return Result.Err(rsu.Err()); + // } + // const res = await conn.request(buildReqDelMeta(this.sthis, conn.key, rsu.Ok().params), { + // waitType: "resDelMeta", + // }); + // if (res.isErr()) { + // return Result.Err(res.Err()); + // } + // const { signedDelUrl } = res.Ok(); + // if (signedDelUrl) { + // return this.delObject(uri, signedDelUrl); + // } + // return Result.Ok(undefined); + return Result.Ok(undefined); + } +} + +class WALGateway extends BaseGateway implements StoreTypeGateway { + // WAL will not pollute to the cloud + readonly wals = new Map(); + constructor(sthis: SuperThis) { + super(sthis, "WALGateway"); + } + getWalKeyFromUri(uri: URI): Result { + const rKey = uri.getParamsResult({ + key: 0, + name: 0, + }); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + const { name, key } = rKey.Ok(); + return Result.Ok(`${name}:${key}`); + } + async getConn(uri: URI): Promise> { + const rKey = this.getWalKeyFromUri(uri); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + const wal = this.wals.get(rKey.Ok()); + if (!wal) { + return Result.Err(new NotFoundError("Not found")); + } + return Result.Ok(wal); + } + async putConn(uri: URI, body: Uint8Array): Promise> { + const rKey = this.getWalKeyFromUri(uri); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + this.wals.set(rKey.Ok(), body); + return Result.Ok(undefined); + } + async delConn(uri: URI): Promise> { + const rKey = this.getWalKeyFromUri(uri); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + this.wals.delete(rKey.Ok()); + return Result.Ok(undefined); + } +} + +const storeTypedGateways = new KeyedResolvOnce(); +function getStoreTypeGateway(sthis: SuperThis, uri: URI): StoreTypeGateway { + const store = uri.getParam("store"); + switch (store) { + case "data": + return storeTypedGateways.get(store).once(() => new DataGateway(sthis)); + case "meta": + return storeTypedGateways.get(store).once(() => new MetaGateway(sthis)); + case "wal": + return storeTypedGateways.get(store).once(() => new WALGateway(sthis)); + default: + throw ensureLogger(sthis, "getStoreTypeGateway") + .Error() + .Str("store", store) + .Msg("Invalid store type") + .ResultError(); + } +} + +// const keyedConnections = new KeyedResolvOnce(); +interface Subscription { + readonly sid: string; + readonly uri: string; // optimization + readonly callback: (msg: Uint8Array) => void; + readonly unsub: () => void; +} +const subscriptions = new Map(); +// const doServerSubscribe = new KeyedResolvOnce(); +const trackPuts = new Set(); +export class FireproofCloudGateway implements bs.Gateway { + readonly logger: Logger; + readonly sthis: SuperThis; + + constructor(sthis: SuperThis) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "FireproofCloudGateway", { + this: true, + }); + } + + async buildUrl(baseUrl: URI, key: string): Promise> { + return Result.Ok(baseUrl.build().setParam("key", key).URI()); + } + + async start(uri: URI): Promise> { + await this.sthis.start(); + const ret = uri.build().defParam("version", VERSION); + const rName = uri.getParamResult("name"); + if (rName.isErr()) { + return this.logger.Error().Err(rName).Msg("name not found").ResultError(); + } + ret.defParam("protocol", "wss"); + return Result.Ok(ret.URI()); + } + + async get(uri: URI): Promise { + return getStoreTypeGateway(this.sthis, uri).get(uri, this.getCloudConnection(uri)); + } + + async put(uri: URI, body: Uint8Array): Promise> { + const ret = await getStoreTypeGateway(this.sthis, uri).put(uri, body, this.getCloudConnection(uri)); + if (ret.isOk()) { + if (uri.getParam("testMode")) { + trackPuts.add(uri.toString()); + } + } + return ret; + } + + async delete(uri: URI): Promise { + trackPuts.delete(uri.toString()); + return getStoreTypeGateway(this.sthis, uri).delete(uri, this.getCloudConnection(uri)); + } + + async close(uri: URI): Promise { + const uriStr = uri.toString(); + // CAUTION here is my happen a mutation of subscriptions caused by unsub + for (const sub of Array.from(subscriptions.values())) { + for (const s of sub) { + if (s.uri.toString() === uriStr) { + s.unsub(); + } + } + } + const rConn = await this.getCloudConnection(uri); + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in getCloudConnection").ResultError(); + } + const conn = rConn.Ok(); + await conn.close(); + return Result.Ok(undefined); + } + + // fireproof://localhost:1999/?name=test-public-api&protocol=ws&store=meta + async getCloudConnection(uri: URI): Promise> { + const rParams = uri.getParamsResult({ + name: key.REQUIRED, + protocol: "https", + store: key.REQUIRED, + storekey: key.OPTIONAL, + tenant: key.REQUIRED, + }); + if (rParams.isErr()) { + return this.logger.Error().Url(uri).Err(rParams).Msg("getCloudConnection:err").ResultError(); + } + const params = rParams.Ok(); + // let tenant: string; + // if (params.tenant) { + // tenant = params.tenant; + // } else { + // if (!params.storekey) { + // return this.logger.Error().Url(uri).Msg("no tendant or storekey given").ResultError(); + // } + // const dataKey = params.storekey.replace(/:(meta|wal)@$/, `:data@`); + // const kb = await rt.kb.getKeyBag(this.sthis); + // const rfingerprint = await kb.getNamedKey(dataKey); + // if (rfingerprint.isErr()) { + // return this.logger.Error().Err(rfingerprint).Msg("Error in getNamedKey").ResultError(); + // } + // tenant = rfingerprint.Ok().fingerPrint; + // } + const qOpen = buildReqOpen(this.sthis, {}); + + let cUrl = uri.build().protocol(params.protocol).cleanParams().URI(); + if (cUrl.pathname === "/") { + cUrl = cUrl.build().pathname("/fp").URI(); + } + return Msger.connect(this.sthis, cUrl, qOpen); + // keyedConnections.get(keyTenantLedger(qOpen.conn.key)).once(async () => Msger.open(this.sthis, cUrl, qOpen)); + } + + // private notifySubscribers(data: Uint8Array, callbacks: ((msg: Uint8Array) => void)[] = []): void { + // for (const cb of callbacks) { + // try { + // cb(data); + // } catch (error) { + // this.logger.Error().Err(error).Msg("Error in subscriber callback execution"); + // } + // } + // } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async subscribe(uri: URI, callback: (meta: Uint8Array) => void): Promise { + return Result.Err(new Error("Not implemented")); + // const rParams = uri.getParamsResult({ + // store: 0, + // storekey: 0, + // }); + // if (rParams.isErr()) { + // return this.logger.Error().Err(rParams).Msg("Error in subscribe").ResultError(); + // } + // const { store } = rParams.Ok(); + // if (store !== "meta") { + // return Result.Err(new Error("store must be meta")); + // } + // const rConn = await this.getCloudConnection(uri); + // if (rConn.isErr()) { + // return this.logger.Error().Err(rConn).Msg("Error in subscribe:getCloudConnection").ResultError(); + // } + // const conn = rConn.Ok(); + // const rResSubscribeMeta = await doServerSubscribe.get(pkKey(conn.key)).once(async () => { + // const subId = this.sthis.nextId().str; + // const fn = (subId: string) => (msg: MsgBase) => { + // if (MsgIsUpdateMetaEvent(msg) && subId === msg.subscriberId) { + // // console.log("onMessage", subId, conn.key, msg.metas); + // const s = subscriptions.get(subId); + // if (!s) { + // return; + // } + // console.log("msg", JSON.stringify(msg)); + // this.notifySubscribers( + // this.sthis.txt.encode(JSON.stringify(msg.metas)), + // s.map((s) => s.callback) + // ); + // } + // }; + // conn.onMessage(fn(subId)); + // return conn.request(buildReqSubscriptMeta(this.sthis, conn.key, subId), { + // waitType: "resSubscribeMeta", + // }); + // }); + // if (rResSubscribeMeta.isErr()) { + // return this.logger.Error().Err(rResSubscribeMeta).Msg("Error in subscribe:request").ResultError(); + // } + // const subId = rResSubscribeMeta.Ok().subscriberId; + // let callbacks = subscriptions.get(subId); + // if (!callbacks) { + // callbacks = []; + // subscriptions.set(subId, callbacks); + // } + // const sid = this.sthis.nextId().str; + // const unsub = () => { + // const idx = callbacks.findIndex((c) => c.sid === sid); + // if (idx !== -1) { + // callbacks.splice(idx, 1); + // } + // if (callbacks.length === 0) { + // subscriptions.delete(subId); + // } + // }; + // callbacks.push({ uri: uri.toString(), callback, sid, unsub }); + // return Result.Ok(unsub); + } + + async destroy(_uri: URI): Promise> { + await Promise.all(Array.from(trackPuts).map(async (k) => this.delete(URI.from(k)))); + return Result.Ok(undefined); + } +} + +// function pkKey(set?: ConnectionKey): string { +// const ret = JSON.stringify( +// Object.entries(set || {}) +// .sort(([a], [b]) => a.localeCompare(b)) +// .filter(([k]) => k !== "id") +// .map(([k, v]) => ({ [k]: v })) +// ); +// return ret; +// } + +export class FireproofCloudTestStore implements bs.TestGateway { + readonly logger: Logger; + readonly sthis: SuperThis; + readonly gateway: bs.Gateway; + constructor(gw: bs.Gateway, sthis: SuperThis) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "FireproofCloudTestStore"); + this.gateway = gw; + } + async get(uri: URI, key: string): Promise { + const url = uri.build().setParam("key", key).URI(); + const dbFile = this.sthis.pathOps.join(rt.getPath(url, this.sthis), rt.getFileName(url, this.sthis)); + this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get"); + const buffer = await this.gateway.get(url); + this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got"); + return buffer.Ok(); + } +} + +const onceRegisterFireproofCloudStoreProtocol = new KeyedResolvOnce<() => void>(); +export function registerFireproofCloudStoreProtocol(protocol = "fireproof:", overrideBaseURL?: string) { + return onceRegisterFireproofCloudStoreProtocol.get(protocol).once(() => { + URI.protocolHasHostpart(protocol); + return bs.registerStoreProtocol({ + protocol, + overrideBaseURL, + gateway: async (sthis) => { + return new FireproofCloudGateway(sthis); + }, + test: async (sthis: SuperThis) => { + const gateway = new FireproofCloudGateway(sthis); + return new FireproofCloudTestStore(gateway, sthis); + }, + }); + }); +} diff --git a/src/v2-cloud/client/index.ts b/src/v2-cloud/client/index.ts new file mode 100644 index 00000000..48804cd3 --- /dev/null +++ b/src/v2-cloud/client/index.ts @@ -0,0 +1,125 @@ +import { BuildURI, CoerceURI, KeyedResolvOnce, runtimeFn, URI } from "@adviser/cement"; +import { bs, Database, fireproof } from "@fireproof/core"; +import { ConnectFunction, connectionFactory, makeKeyBagUrlExtractable } from "../../connection-from-store.js"; +import { registerFireproofCloudStoreProtocol } from "./gateway.js"; + +interface ConnectData { + readonly remoteName: string; + firstConnect: boolean; + endpoint?: string; +} + +const SYNC_DB_NAME = "fp_sync"; + +// Usage: +// +// import { useFireproof } from 'use-fireproof' +// import { connect } from '@fireproof/cloud' +// +// const { db } = useFireproof('test') +// +// const cx = connect(db); + +// TODO need to set the keybag url automatically + +// if (!process.env.FP_KEYBAG_URL) { +// process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-fireproof?fs=mem"; +// } + +// if (!runtimeFn().isBrowser) { +// const url = BuildURI.from(process.env.FP_KEYBAG_URL || rt.kb.defaultKeyBagUrl()); +// url.setParam("extractKey", "_deprecated_internal_api"); +// process.env.FP_KEYBAG_URL = url.toString(); +// } + +registerFireproofCloudStoreProtocol(); + +const connectionCache = new KeyedResolvOnce(); +export const rawConnect: ConnectFunction = ( + db: Database, + remoteDbName = "", + url = "fireproof://cloud.fireproof.direct" +) => { + const { sthis, blockstore, name: dbName } = db; + if (!dbName) { + throw new Error("dbName is required"); + } + const urlObj = BuildURI.from(url); + urlObj.protocol("fireproof:"); + const existingName = urlObj.getParam("name"); + urlObj.defParam("name", remoteDbName || existingName || dbName); + urlObj.defParam("localName", dbName); + urlObj.defParam("storekey", `@${dbName}:data@`); + urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/"); + // const fpUrl = urlObj + // .toString() + // .replace(/^http:\/\//, "fireproof://") + // .replace(/^https:\/\//, "fireproof://"); + // eslint-disable-next-line no-console + console.log("Config URL: " + urlObj.toString()); + return connectionCache.get(urlObj.toString()).once(() => { + makeKeyBagUrlExtractable(sthis); + const connection = connectionFactory(sthis, urlObj); + connection.connect_X(blockstore); + return connection; + }); +}; + +async function getOrCreateRemoteName(dbName: string, remoteName?: string) { + const syncDb = fireproof(SYNC_DB_NAME); + + const result = await syncDb.query("localName", { key: dbName, includeDocs: true }); + if (result.rows.length === 0) { + const doc = { + remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str, + localName: dbName, + firstConnect: !remoteName, + } as ConnectData; + const { id } = await syncDb.put(doc); + return { ...doc, _id: id }; + } + const doc = result.rows[0].doc; + return doc; +} + +export function connect( + db: Database, + remoteName?: string, + dashboardURI: CoerceURI = "https://dashboard.fireproof.storage/", + remoteURI: CoerceURI = "fireproof://cloud.fireproof.direct" +): Promise { + const dbName = db.name as string; + if (!dbName) { + throw new Error("Database name is required for cloud connection"); + } + + return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => { + if (!doc) { + throw new Error("Failed to get or create remote name"); + } + doc.endpoint = URI.from(remoteURI).toString(); + const connection = rawConnect(db, doc.remoteName, URI.from(doc.endpoint).toString()); + const connectURI = URI.from(dashboardURI).build().pathname("/fp/databases/connect"); + connectURI.defParam("localName", dbName); + connectURI.defParam("remoteName", doc.remoteName); + if (doc.endpoint) { + connectURI.defParam("endpoint", doc.endpoint); + } + // eslint-disable-next-line no-console + console.log("Fireproof Cloud: " + connectURI.toString()); + if ( + doc.firstConnect && + runtimeFn().isBrowser && + window.location.href.indexOf(URI.from(dashboardURI).toString()) === -1 + ) { + // Set firstConnect to false after opening the window, so we don't constantly annoy with the dashboard + const syncDb = fireproof(SYNC_DB_NAME); + doc.firstConnect = false; + await syncDb.put(doc); + + // window.open(connectURI.toString(), "_blank"); + } + connection.dashboardUrl = URI.from(connectURI); + return connection; + }); +} diff --git a/src/v2-cloud/cloud.test.ts-off b/src/v2-cloud/cloud.test.ts-off new file mode 100644 index 00000000..a0811ec5 --- /dev/null +++ b/src/v2-cloud/cloud.test.ts-off @@ -0,0 +1,533 @@ +// import { env } from "cloudflare:test" +import { BuildURI, Future, URI } from "@adviser/cement"; +import { ReqSignedUrl, ResSignedUrl } from "./msg-types.js"; +import { Env } from "./backend/env.js"; +import { $ } from "zx"; +import fs from "fs/promises"; +import * as toml from "smol-toml"; +import { bs, CRDTEntry, Database, ensureSuperThis, fireproof, isNotFoundError, rt } from "@fireproof/core"; +import { AwsClient } from "aws4fetch"; +import { smokeDB } from "../../tests/helper.js"; +// import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./client/gateway.ts-off"; +import { calculatePreSignedUrl } from "./pre-signed-url.js"; +import { newWebSocket } from "./new-websocket.js"; +import { registerFireproofCloudStoreProtocol } from "./client/gateway.js"; + +// function testReqSignedUrl(tid = "test") { +// return { +// tid: tid, +// type: "reqSignedUrl", +// params: { +// // protocol: "ws", +// path: "/hallo", +// store: "wal", +// key: "main", +// }, +// version: "test", +// } satisfies ReqSignedUrl; +// } + +// async function testResSignedUrl(env: Env, tid?: string, amzDate?: string): Promise { +// const req = testReqSignedUrl(tid); +// const rSignedUrl = await calculatePreSignedUrl(req, env, amzDate); +// if (rSignedUrl.isErr()) { +// throw rSignedUrl.Err(); +// } +// return { +// params: req.params, +// signedUrl: rSignedUrl.Ok().toString(), +// // `http://localhost:8080/tenantId/test-name/wal/main.json?tid=${tid}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKeyId%2F20241121%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241121T225359Z&X-Amz-Expires=86400&X-Amz-Signature=f52d5ecfbb6be93210dd57cb49ba1e426a8aee24a0738aedb636ae5722fcdded&X-Amz-SignedHeaders=host`, +// tid: tid || "test", +// type: "resSignedUrl", +// version: env.VERSION, +// } satisfies ResSignedUrl; +// } + +// describe("CloudBackendTest", () => { +// const sthis = ensureSuperThis(); +// let env: Env; +// let pid: number; +// const port = +(process.env.FP_WRANGLER_PORT || 0) || ~~(1024 + Math.random() * (0x10000 - 1024)); +// const wrangler = BuildURI.from("http://localhost") +// .port("" + port) +// .URI(); +// async function cfFetch(relative: string, init: RequestInit) { +// return fetch(wrangler.build().appendRelative(relative).asURL(), init); +// } +// beforeAll(async () => { +// const tomlFile = "src/cloud/backend/wrangler.toml"; +// const tomeStr = await fs.readFile(tomlFile, "utf-8"); +// const wranglerFile = toml.parse(tomeStr) as unknown as { +// env: { test: { vars: Env } }; +// }; +// env = wranglerFile.env.test.vars; +// if (process.env.FP_WRANGLER_PORT) { +// return; +// } +// $.verbose = !!process.env.FP_DEBUG; +// const runningWrangler = $` +// wrangler dev -c ${tomlFile} --port ${port} --env test --no-show-interactive-dev-session & +// waitPid=$! +// echo "PID:$waitPid" +// wait $waitPid`; +// const waitReady = new Future(); +// runningWrangler.stdout.on("data", (chunk) => { +// // console.log(">>", chunk.toString()) +// const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; +// if (mightPid) { +// pid = +mightPid; +// } +// if (chunk.includes("Ready on http")) { +// waitReady.resolve(true); +// } +// }); +// runningWrangler.stderr.on("data", (chunk) => { +// // eslint-disable-next-line no-console +// console.error("!!", chunk.toString()); +// }); +// await waitReady.asPromise(); +// // await f.asPromise() +// // wrangler dev -c src/cloud/backend/wrangler.toml --port 4711 --env test +// }); + +// afterAll(async () => { +// // console.log("kill", runningWrangler.pid, runningWrangler) +// // process.kill(runningWrangler.pid) +// // process.stdin.write(Array(4).fill("x\n\r").join("")) +// if (pid) process.kill(pid); +// }); + +// describe("raw tests", () => { +// it("return 404", async () => { +// const res = await cfFetch("/posts", {}); +// expect(res.status).toBe(404); +// expect(await res.json()).toEqual({ +// message: "Notfound:/posts", +// tid: "internal", +// type: "error", +// version: env.VERSION, +// }); +// }); +// it("return 422 invalid json", async () => { +// const res = await cfFetch("/fp", { method: "PUT" }); +// expect(res.status).toBe(422); +// expect(await res.json()).toEqual({ +// message: "Unexpected end of JSON input", +// tid: "internal", +// type: "error", +// version: env.VERSION, +// }); +// }); + +// it("return 422 illegal msg", async () => { +// const res = await cfFetch("/fp", { +// method: "PUT", +// body: JSON.stringify({ +// bucket: "test", +// key: "test", +// }), +// }); +// expect(res.status).toBe(422); +// expect(await res.json()).toEqual({ +// message: "unknown msg.type=undefined", +// tid: "internal", +// type: "error", +// version: env.VERSION, +// }); +// }); + +// it("return 200 msg", async () => { +// const res = await cfFetch("/fp", { +// method: "PUT", +// body: JSON.stringify(testReqSignedUrl()), +// }); +// expect(res.status).toBe(200); +// expect(await res.json()).toEqual(await testResSignedUrl(env)); +// }); + +// // it("reqOpen without websocket", async () => { +// // const conn = await msgOpen(cfURL, { } +// // }); + +// // it("reqOpen with websocket", async () => { +// // }); + +// it("use websockets SignedUrl", async () => { +// await Promise.all( +// Array(100) +// .fill(null) +// .map(async () => { +// const url = wrangler.build().appendRelative("/ws").protocol("ws:"); +// const so = await newWebSocket(url); +// const done = new Future(); +// let total = 10; +// let tid = `${total}-test-${Math.random()}`; +// so.onopen = () => { +// so.send(JSON.stringify(testReqSignedUrl(tid))); +// }; +// so.onmessage = async (msg) => { +// try { +// const res = JSON.parse(msg.data.toString()) as ResSignedUrl; +// expect(res).toEqual(await testResSignedUrl(env, tid, URI.from(res.signedUrl).getParam("X-Amz-Date"))); +// if (--total === 0) { +// done.resolve(true); +// } else { +// tid = `${total}-test-${Math.random()}`; +// so.send(JSON.stringify(testReqSignedUrl(tid))); +// } +// } catch (err) { +// done.reject(err); +// } +// }; +// so.onerror = (ev) => { +// assert.fail(`WebSocket error: ${ev}`); +// }; +// return done.asPromise().then(() => so.close(1000, "done")); +// }) +// ); +// }); +// }); + +describe("FireproofCloudGateway", () => { + let db: Database; + let unregister: () => void; + interface ExtendedGateway extends bs.Gateway { + headerSize: number; + subscribe?: (url: URI, callback: (meta: Uint8Array) => void) => Promise; // Changed VoidResult to UnsubscribeResult + } + + // has to leave + interface ExtendedStore { + gateway: ExtendedGateway; + _url: URI; + name: string; + } + + beforeAll(() => { + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + }); + + beforeEach(() => { + const config = { + store: { + stores: { + base: wrangler.build().protocol("fireproof:").setParam("protocol", "ws").setParam("testMode", "true"), + // process.env.FP_STORAGE_URL, // || "fireproof://localhost:1968", + }, + }, + }; + const name = "fireproof-cloud-test-db-" + sthis.nextId().str; + db = fireproof(name, config); + }); + + afterEach(async () => { + // Clear the database before each test + if (db) { + await db.close(); + await db.destroy(); + } + }); + + afterAll(() => { + unregister(); + }); + + // it("env setup is ok", () => { + // // expect(process.env.FP_STORAGE_URL).toMatch(/fireproof:\/\/localhost:1999/); + // }); + + it("should have loader and options", () => { + const loader = db.blockstore.loader; + expect(loader).toBeDefined(); + if (!loader) { + throw new Error("Loader is not defined"); + } + expect(loader.ebOpts).toBeDefined(); + expect(loader.ebOpts.store).toBeDefined(); + expect(loader.ebOpts.store.stores).toBeDefined(); + if (!loader.ebOpts.store.stores) { + throw new Error("Loader stores is not defined"); + } + if (!loader.ebOpts.store.stores.base) { + throw new Error("Loader stores.base is not defined"); + } + + const baseUrl = URI.from(loader.ebOpts.store.stores.base); + expect(baseUrl.protocol).toBe("fireproof:"); + // expect(baseUrl.hostname).toBe("localhost"); + // expect(baseUrl.port || "").toBe("1999"); + }); + + it("should initialize and perform basic operations", async () => { + const docs = await smokeDB(db); + + // // get a new db instance + // db = new Database(name, config); + + // Test update operation + const updateDoc = await db.get<{ content: string }>(docs[0]._id); + updateDoc.content = "Updated content"; + const updateResult = await db.put(updateDoc); + expect(updateResult.id).toBe(updateDoc._id); + + const updatedDoc = await db.get<{ content: string }>(updateDoc._id); + expect(updatedDoc.content).toBe("Updated content"); + + // Test delete operation + await db.del(updateDoc._id); + try { + await db.get(updateDoc._id); + throw new Error("Document should have been deleted"); + } catch (e) { + const error = e as Error; + expect(error.message).toContain("Not found"); + } + }); + + it("should subscribe to changes", async () => { + // Extract stores from the loader + const metaStore = (await db.blockstore.loader?.metaStore()) as unknown as ExtendedStore; + + const metaGateway = metaStore?.gateway; + + const metaUrl = await metaGateway?.buildUrl(metaStore?._url, "main"); + await metaGateway?.start(metaStore?._url); + + let didCall = false; + + expect(metaGateway.subscribe).toBeTypeOf("function"); + if (metaGateway.subscribe) { + const future = new Future(); + + const metaSubscribeResult = await metaGateway.subscribe(metaUrl?.Ok(), (data: Uint8Array) => { + // console.log("data", data); + const decodedData = sthis.txt.decode(data); + expect(decodedData).toContain("parents"); + didCall = true; + future.resolve(); + }); + expect(metaSubscribeResult.isOk()).toBeTruthy(); + const ok = await db.put({ _id: "key1", hello: "world1" }); + expect(ok).toBeTruthy(); + expect(ok.id).toBe("key1"); + await future.asPromise(); + expect(didCall).toBeTruthy(); + metaSubscribeResult.Ok()(); + } + }); +}); +describe("AwsClient R2", () => { + it("make presigned url", async () => { + const sthis = ensureSuperThis(); + const a4f = new AwsClient({ + accessKeyId: sthis.env.get("CF_ACCESS_KEY_ID") || "accessKeyId", + secretAccessKey: sthis.env.get("CF_SECRET_ACCESS_KEY") || "secretAccessKey", + region: "us-east-1", + service: "s3", + }); + const buildUrl = BuildURI.from(sthis.env.get("CF_STORAGE_URL") || "https://bucket.example.com/db/main") + .appendRelative("db/main") + .setParam("X-Amz-Expires", "22"); + const signedUrl = await a4f + .sign(new Request(buildUrl.toString(), { method: "PUT" }), { + aws: { + signQuery: true, + datetime: "2021-09-01T12:34:56Z", + }, + }) + .then((res) => res.url); + expect(URI.from(signedUrl).asObj()).toEqual( + buildUrl + .setParam("X-Amz-Date", "2021-09-01T12:34:56Z") + .setParam("X-Amz-Algorithm", "AWS4-HMAC-SHA256") + .setParam("X-Amz-Credential", `${a4f.accessKeyId}/2021-09-/${a4f.region}/${a4f.service}/aws4_request`) + .setParam("X-Amz-SignedHeaders", "host") + .setParam( + "X-Amz-Signature", + sthis.env.get("CF_PRESIGNED_SIGNATURE") || "bbae4604fbe51a4ce9972183d8871a8a187ab0f4d2415afd6dc728f8ccc9900f" + ) + .asObj() + ); + }); +}); + +describe(`store=meta`, () => { + const store = "meta"; + let gw: bs.Gateway; + const sthis = ensureSuperThis(); + let uri: URI; + beforeAll(async () => { + gw = new FireproofCloudGateway(sthis); + const id = sthis.nextId().str; + uri = BuildURI.from("fireproof://localhost") + .port("" + port) + .setParam("store", store) + .setParam("name", id) + .setParam("protocol", "ws") + .setParam("storekey", id) + .setParam("testMode", "true") + .URI(); + + const last: Uint8Array[] = []; + const cnt = 4; + Array(cnt) + .fill(null) + .map(async () => { + const rOk = (await gw.subscribe?.(uri, (meta: Uint8Array) => { + last.push(meta); + if (last.length === cnt) { + expect(last[0]).toEqual(last[1]); + expect(last[1]).toEqual(last[2]); + expect(last[2]).toEqual(last[3]); + last.length = 0; + } + })) as bs.VoidResult; + expect(rOk.isOk()).toBeTruthy(); + }); + + const keyBag = await rt.kb.getKeyBag(sthis); + await keyBag.getNamedKey(`@${id}:data@`); + }); + + afterAll(async () => { + const rOk = await gw.close(uri); + expect(rOk.isOk()).toBeTruthy(); + }); + + const subscribeCallbacks: { + connId: string; + uri: URI; + cb: ReturnType void>>; + unsub: bs.UnsubscribeResult; + }[] = []; + beforeEach(async () => { + await Promise.all( + Array(1) + .fill(null) + .map(async () => { + const cb = vitest.fn(); + const connId = sthis.nextId().str; + const uriConnId = uri.build().setParam("connId", connId).URI(); + const unsub = (await gw.subscribe?.(uriConnId, (meta: Uint8Array) => + cb(sthis.txt.decode(meta), connId) + )) as bs.UnsubscribeResult; + subscribeCallbacks.push({ cb, unsub, connId, uri: uriConnId }); + }) + ); + }); + afterEach(() => { + subscribeCallbacks.forEach(({ unsub }) => unsub.Ok()()); + subscribeCallbacks.length = 0; + }); + + function crdtEntry(connId = "default"): Uint8Array { + return sthis.txt.encode( + JSON.stringify([ + { + cid: `${connId}:bafyreidjlylxmmb3yuz7levzzbso3g7ql54zovxl3mkhbbqxmmfnfbkoym`, + data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWdvdHM3dmFzeHhhdmdoY3FjeHo3ZXJibTdtY21ramQybTV0bXpzcGdhbG91d2lpcjYzZnkifV19Z3BhcmVudHOA", + parents: [], + }, + { + cid: `${connId}:bafyreie7izpgpmxd6heoiweoyblgyzoxt74xrp5wcpqo66bmjv2plgmceq`, + data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWQyZ2l1c2t2YWJoZTZ5ZHdsdXo0aGx4Z3lyNTZ5dmZmbjVpdndqdmhlYXl3cWJ4bHFmeGEifV19Z3BhcmVudHOA", + parents: [], + }, + ] satisfies CRDTEntry[]) + ); + } + + it(`buildUrl`, async () => { + const rOk = await gw.buildUrl(uri, "KEY"); + const url = rOk.Ok(); + expect(url.getParam("store")).toBe(store); + expect(url.getParam("key")).toBe("KEY"); + }); + it(`start`, async () => { + const rOk = await gw.start(uri); + const url = rOk.Ok(); + expect(url.getParam("store")).toBe(store); + expect(url.getParam("version")).toBeTruthy(); + }); + + it(`unsubscribe`, async () => { + subscribeCallbacks.forEach((sub) => sub.unsub.Ok()()); + const rOk = await gw.put(uri.build().setParam("key", "main").URI(), crdtEntry()); + // console.log(rOk); + expect(rOk.isOk()).toBeTruthy(); + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + }); + + it(`get-put-delete`, async () => { + async function getNotFound() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.get(u.build().setParam("key", key).URI()); + expect(rOk.isErr()).toBeTruthy(); + expect(isNotFoundError(rOk.Err())).toBeTruthy(); + } + } + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + } + console.log("getNotFound-pre"); + + subscribeCallbacks.forEach(({ cb }) => { + expect(cb).toHaveBeenCalledTimes(0); + }); + // get not found + await getNotFound(); + // put + async function put() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.put( + u.build().setParam("key", key).URI(), + crdtEntry(`${key}:${u.getParam("connId", "default")}`) + ); + expect(rOk.isOk()).toBeTruthy(); + } + } + // console.log('put', subscribeCallbacks.map(({ cb }) => cb.mock.calls)); + subscribeCallbacks.forEach(({ cb, connId }) => { + // expect(cb).toHaveBeenCalledTimes(subscribeCallbacks.length * 2); + for (const key of ["KEY1", "KEY2"]) { + expect(cb).toHaveBeenCalledWith(sthis.txt.decode(crdtEntry(`${key}:${connId}`)), connId); + } + }); + } + // console.log('put-pre') + await put(); + subscribeCallbacks.forEach(({ cb }) => cb.mockClear()); + + async function get() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.get(u.build().setParam("key", key).URI()); + const data = JSON.parse(sthis.txt.decode(rOk.Ok())) as CRDTEntry[]; + expect(data).toEqual(subscribeCallbacks.map(({ connId }) => crdtEntry(connId))); + } + } + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + } + console.log("get-pre"); + await get(); + async function del() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.delete(u.build().setParam("key", key).URI()); + expect(rOk.isOk()).toBeTruthy(); + } + } + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + } + console.log("del-pre"); + await del(); + // get not found + console.log("getNotFound-pre"); + await getNotFound(); + }); + it(`close`, async () => { + const rOk = await gw.close(uri); + expect(rOk.isOk()).toBeTruthy(); + }); +}); diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts new file mode 100644 index 00000000..43c4fa3a --- /dev/null +++ b/src/v2-cloud/connection.test.ts @@ -0,0 +1,362 @@ +import { ensureSuperThis } from "@fireproof/core"; +import { URI } from "@adviser/cement"; +import { + buildReqGestalt, + buildReqOpen, + MsgIsError, + MsgIsResGestalt, + MsgIsResOpen, + defaultGestalt, + ReqSignedUrlParam, + GwCtx, + MsgWithError, + ResOptionalSignedUrl, +} from "./msg-types.js"; +import { + MsgIsResGetData, + MsgIsResPutData, + MsgIsResDelData, + buildReqPutData, + buildReqDelData, + buildReqGetData, +} from "./msg-types-data.js"; +import { + buildReqGetWAL, + buildReqPutWAL, + buildReqDelWAL, + MsgIsResGetWAL, + MsgIsResPutWAL, + MsgIsResDelWAL, +} from "./msg-types-wal.js"; +import { applyStart, defaultMsgParams, MsgConnected, Msger } from "./msger.js"; +import { HonoServer } from "./hono-server.js"; +import { Hono } from "hono"; +import { calculatePreSignedUrl } from "./pre-signed-url.js"; +import { CFHonoServerFactory, httpStyle, NodeHonoServerFactory, resolveToml, wsStyle } from "./test-helper.js"; +import { + buildReqDelMeta, + buildBindGetMeta, + buildReqPutMeta, + MsgIsResDelMeta, + ResDelMeta, + ReqDelMeta, + BindGetMeta, + EventGetMeta, + MsgIsEventGetMeta, + MsgIsResPutMeta, +} from "./msg-type-meta.js"; + +async function refURL(sp: ResOptionalSignedUrl) { + const { env } = await resolveToml("D1"); + return ( + await calculatePreSignedUrl(sp, { + storageUrl: URI.from(env.STORAGE_URL), + aws: { + accessKeyId: env.ACCESS_KEY_ID, + secretAccessKey: env.SECRET_ACCESS_KEY, + region: env.REGION, + }, + test: { + amzDate: URI.from(sp.signedUrl).getParam("X-Amz-Date"), + }, + }) + ) + .Ok() + .asObj(); +} + +describe("Connection", () => { + const sthis = ensureSuperThis(); + const msgP = defaultMsgParams(sthis, { hasPersistent: true }); + + beforeAll(async () => { + sthis.env.sets((await resolveToml("D1")).env as unknown as Record); + }); + + describe.each([NodeHonoServerFactory(), CFHonoServerFactory("DO"), CFHonoServerFactory("D1")])( + "$name - Connection", + (honoServer) => { + const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); + const qOpen = buildReqOpen(sthis, { reqId: "req-open-test" }); + const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); + describe.each([httpStyle(sthis, port, msgP, my), wsStyle(sthis, port, msgP, my)])( + `${honoServer.name} - $name`, + (style) => { + let server: HonoServer; + beforeAll(async () => { + const app = new Hono(); + server = await honoServer + .factory(sthis, msgP, style.remoteGestalt, port) + .then((srv) => srv.register(app, port)); + }); + afterAll(async () => { + // console.log("closing server"); + await server.close(); + }); + it(`conn refused`, async () => { + const rC = await applyStart(style.connRefused.open()); + expect(rC.isErr()).toBeTruthy(); + expect(rC.Err().message).toMatch(/ECONNREFUSED/); + }); + + it(`timeout`, async () => { + const rC = await applyStart(style.timeout.open()); + expect(rC.isErr()).toBeTruthy(); + expect(rC.Err().message).toMatch(/Timeout/i); + }); + + describe(`connection`, () => { + let c: MsgConnected; + beforeEach(async () => { + const rC = await style.ok.open().then((r) => MsgConnected.connect(r, { reqId: "req-open-testx" })); + expect(rC.isOk()).toBeTruthy(); + c = rC.Ok(); + expect(c.conn).toEqual({ + reqId: "req-open-testx", + resId: c.conn.resId, + }); + }); + afterEach(async () => { + await c.close(); + }); + + it("kaputt url http", async () => { + const r = await c.raw.request( + { + tid: "test", + type: "kaputt", + version: "FP-MSG-1.0", + }, + { waitFor: () => true } + ); + if (!MsgIsError(r)) { + assert.fail("expected MsgError"); + return; + } + expect(r).toEqual({ + message: "unexpected message", + tid: "test", + type: "error", + version: "FP-MSG-1.0", + src: { + tid: "test", + type: "kaputt", + version: "FP-MSG-1.0", + }, + }); + }); + it("gestalt url http", async () => { + const msgP = defaultMsgParams(sthis, {}); + const req = buildReqGestalt(sthis, defaultGestalt(msgP, { id: "test" })); + const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); + if (!MsgIsResGestalt(r)) { + assert.fail("expected MsgError", JSON.stringify(r)); + } + expect(r.gestalt).toEqual(c.exchangedGestalt?.remote); + }); + + it("openConnection", async () => { + const req = buildReqOpen(sthis, { ...c.conn }); + const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); + if (!MsgIsResOpen(r)) { + assert.fail(JSON.stringify(r)); + } + expect(r).toEqual({ + conn: { ...c.conn, resId: r.conn?.resId }, + tid: req.tid, + type: "resOpen", + version: "FP-MSG-1.0", + }); + }); + }); + + it("open", async () => { + const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, { + reqId: "req-open-testy", + }); + expect(rC.isOk()).toBeTruthy(); + const c = rC.Ok(); + expect(c.conn).toEqual({ + reqId: "req-open-testy", + resId: c.conn.resId, + }); + expect(c.raw).toBeInstanceOf(style.cInstance); + expect(c.exchangedGestalt).toEqual({ + my, + remote: style.remoteGestalt, + }); + await c.close(); + }); + describe(`${honoServer.name} - Msgs`, () => { + let gwCtx: GwCtx; + let conn: MsgConnected; + beforeAll(async () => { + const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); + expect(rC.isOk()).toBeTruthy(); + conn = rC.Ok(); + gwCtx = { + conn: conn.conn, + tenant: { + tenant: "Tenant", + ledger: "Ledger", + }, + }; + }); + afterAll(async () => { + await conn.close(); + }); + it("Open", async () => { + const res = await conn.raw.request(buildReqOpen(sthis, conn.conn), { waitFor: MsgIsResOpen }); + if (!MsgIsResOpen(res)) { + assert.fail("expected MsgResOpen", JSON.stringify(res)); + } + expect(MsgIsResOpen(res)).toBeTruthy(); + expect(res.conn).toEqual({ ...qOpen.conn, resId: res.conn.resId }); + }); + + function sup() { + return { + path: "test/me", + key: "key-test", + } satisfies ReqSignedUrlParam; + } + describe("Data", async () => { + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildReqGetData(sthis, sp, gwCtx), { waitFor: MsgIsResGetData }); + if (MsgIsResGetData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResGetData", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const res = await conn.request(buildReqPutData(sthis, sp, gwCtx), { waitFor: MsgIsResPutData }); + if (MsgIsResPutData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResPutData", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelData(sthis, sp, gwCtx), { waitFor: MsgIsResDelData }); + if (MsgIsResDelData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelData", JSON.stringify(res)); + } + }); + }); + + describe("Meta", async () => { + it("bind stop", async () => { + const sp = sup(); + expect(conn.raw.activeBinds.size).toBe(0); + const streams: ReadableStream>[] = Array(5) + .fill(0) + .map(() => { + return conn.bind(buildBindGetMeta(sthis, sp, gwCtx), { + waitFor: MsgIsEventGetMeta, + }); + }); + for await (const stream of streams) { + const reader = stream.getReader(); + while (true) { + const { done, value: msg } = await reader.read(); + if (done) { + break; + } + if (MsgIsEventGetMeta(msg)) { + // expect(msg.params).toEqual(sp); + expect(URI.from(msg.signedUrl).asObj()).toEqual(await refURL(msg)); + } else { + assert.fail("expected MsgEventGetMeta", JSON.stringify(msg)); + } + await reader.cancel(); + } + } + expect(conn.raw.activeBinds.size).toBe(0); + // await Promise.all(streams.map((s) => s.cancel())); + }); + + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildBindGetMeta(sthis, sp, gwCtx), { waitFor: MsgIsEventGetMeta }); + if (MsgIsEventGetMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgIsEventGetMeta", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const metas = Array(5) + .fill({ cid: "x", parents: [], data: "MomRkYXRho" }) + .map((data) => { + return { ...data, cid: sthis.timeOrderedNextId().str }; + }); + const res = await conn.request(buildReqPutMeta(sthis, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta }); + if (MsgIsResPutMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgIsResPutMeta", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelMeta(sthis, sp, gwCtx), { + waitFor: MsgIsResDelMeta, + }); + if (MsgIsResDelMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + } + }); + }); + describe("WAL", async () => { + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildReqGetWAL(sthis, sp, gwCtx), { waitFor: MsgIsResGetWAL }); + if (MsgIsResGetWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResGetWAL", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const res = await conn.request(buildReqPutWAL(sthis, sp, gwCtx), { waitFor: MsgIsResPutWAL }); + if (MsgIsResPutWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResPutWAL", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelWAL(sthis, sp, gwCtx), { waitFor: MsgIsResDelWAL }); + if (MsgIsResDelWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + } + }); + }); + }); + } + ); + } + ); +}); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts new file mode 100644 index 00000000..b04fe3b1 --- /dev/null +++ b/src/v2-cloud/hono-server.ts @@ -0,0 +1,265 @@ +import { exception2Result, HttpHeader, param, ResolveOnce, Result, URI } from "@adviser/cement"; +import { Logger, SuperThis } from "@fireproof/core"; +import { Context, Hono, Next } from "hono"; +import { top_uint8 } from "../coerce-binary.js"; +import { + Gestalt, + buildErrorMsg, + MsgBase, + EnDeCoder, + ErrorMsg, + MsgWithError, + buildRes, + MsgWithConn, + GwCtx, + MsgIsError, +} from "./msg-types.js"; +import { MsgDispatcher, WSConnection } from "./msg-dispatch.js"; +import { WSEvents } from "hono/ws"; +import { calculatePreSignedUrl, PreSignedMsg } from "./pre-signed-url.js"; +import { buildMsgDispatcher } from "./msg-dispatcher-impl.js"; +import { + BindGetMeta, + buildEventGetMeta, + buildResDelMeta, + buildResPutMeta, + EventGetMeta, + ReqDelMeta, + ReqPutMeta, + ResDelMeta, + ResPutMeta, +} from "./msg-type-meta.js"; +import { MetaMerger } from "./meta-merger/meta-merger.js"; +import { SQLDatabase } from "./meta-merger/abstract-sql.js"; +import { WSRoom } from "./ws-room.js"; + +export interface RunTimeParams { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly ende: EnDeCoder; + readonly impl: HonoServerImpl; +} +// eslint-disable-next-line @typescript-eslint/no-invalid-void-type +export type ConnMiddleware = (conn: WSConnection, c: Context, next: Next) => Promise; +export interface HonoServerImpl { + start(): Promise; + gestalt(): Gestalt; + calculatePreSignedUrl(p: PreSignedMsg): Promise>; + upgradeWebSocket: (createEvents: (c: Context) => WSEvents | Promise) => ConnMiddleware; + handleBindGetMeta(sthis: SuperThis, logger: Logger, msg: BindGetMeta): Promise>; + handleReqPutMeta(sthis: SuperThis, logger: Logger, msg: ReqPutMeta): Promise>; + handleReqDelMeta(sthis: SuperThis, logger: Logger, msg: ReqDelMeta): Promise>; + readonly headers: HttpHeader; +} + +export abstract class HonoServerBase implements HonoServerImpl { + readonly _gs: Gestalt; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly metaMerger: MetaMerger; + readonly headers: HttpHeader; + readonly wsRoom: WSRoom; + constructor(sthis: SuperThis, logger: Logger, gs: Gestalt, sqlDb: SQLDatabase, wsRoom: WSRoom, headers?: HttpHeader) { + this.logger = logger; + this._gs = gs; + this.sthis = sthis; + this.wsRoom = wsRoom; + this.metaMerger = new MetaMerger(sqlDb); + this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); + } + + abstract upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware; + + start(drop = false): Promise { + return this.metaMerger.createSchema(drop).then(() => this); + } + + gestalt(): Gestalt { + return this._gs; + } + + async handleReqPutMeta( + sthis: SuperThis, + logger: Logger, + msg: MsgWithConn + ): Promise> { + const rUrl = await buildRes("PUT", "meta", "resPutMeta", sthis, logger, msg, this); + if (MsgIsError(rUrl)) { + return rUrl; + } + await this.metaMerger.addMeta({ + logger, + connection: msg, + metas: msg.metas, + }); + return buildResPutMeta(sthis, logger, msg, { ...rUrl, metas: await this.metaMerger.metaToSend(msg) }); + } + + async handleReqDelMeta( + sthis: SuperThis, + logger: Logger, + msg: MsgWithConn + ): Promise> { + const rUrl = await buildRes("DELETE", "meta", "resDelMeta", sthis, logger, msg, this); + if (MsgIsError(rUrl)) { + return rUrl; + } + await this.metaMerger.delMeta({ + logger, + connection: msg, + }); + return buildResDelMeta(sthis, logger, msg, rUrl.signedUrl); + } + + async handleBindGetMeta( + sthis: SuperThis, + logger: Logger, + msg: MsgWithConn, + gwCtx: GwCtx = msg + ): Promise> { + const rUrl = await buildRes("GET", "meta", "eventGetMeta", sthis, logger, msg, this); + if (MsgIsError(rUrl)) { + return rUrl; + } + return buildEventGetMeta( + sthis, + logger, + msg, + { + ...rUrl, + metas: await this.metaMerger.metaToSend(msg), + }, + gwCtx + ); + } + + calculatePreSignedUrl(p: PreSignedMsg): Promise> { + const rRes = this.sthis.env.gets({ + STORAGE_URL: param.REQUIRED, + ACCESS_KEY_ID: param.REQUIRED, + SECRET_ACCESS_KEY: param.REQUIRED, + REGION: "us-east-1", + }); + if (rRes.isErr()) { + return Promise.resolve(Result.Err(rRes.Err())); + } + const res = rRes.Ok(); + return calculatePreSignedUrl(p, { + storageUrl: URI.from(res.STORAGE_URL), + aws: { + accessKeyId: res.ACCESS_KEY_ID, + secretAccessKey: res.SECRET_ACCESS_KEY, + region: res.REGION, + }, + }); + } +} + +export interface HonoServerFactory { + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise; + + start(app: Hono): Promise; + serve(app: Hono, port?: number): Promise; + close(): Promise; +} + +export const CORS = HttpHeader.from({ + // "Accept": "application/json", + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,POST,OPTIONS,PUT,DELETE", + "Access-Control-Max-Age": "86400", // Cache pre-flight response for 24 hours +}); + +export class HonoServer { + // readonly sthis: SuperThis; + // readonly msgP: MsgerParams; + // readonly gestalt: Gestalt; + // readonly logger: Logger; + readonly factory: HonoServerFactory; + constructor(/* sthis: SuperThis, msgP: MsgerParams, gestalt: Gestalt, */ factory: HonoServerFactory) { + // this.sthis = sthis; + // this.logger = ensureLogger(sthis, "HonoServer"); + // this.msgP = msgP; + // this.gestalt = gestalt; + this.factory = factory; + } + readonly _register = new ResolveOnce(); + async register(app: Hono, port?: number): Promise { + return this._register.once(async () => { + await this.factory.start(app); + // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) + // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) + app.put("/fp", (c) => + this.factory.inject(c, async ({ sthis, logger, impl }) => { + impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); + const rMsg = await exception2Result(() => c.req.json() as Promise); + if (rMsg.isErr()) { + c.status(400); + return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); + } + const dispatcher = buildMsgDispatcher(sthis, impl.gestalt()); + return dispatcher.dispatch(impl, rMsg.Ok(), (msg) => c.json(msg)); + }) + ); + app.get("/ws", (c, next) => + this.factory.inject(c, async ({ sthis, logger, ende, impl }) => { + return impl.upgradeWebSocket((_c) => { + let dp: MsgDispatcher; + return { + onOpen: (_e, _ws) => { + dp = buildMsgDispatcher(sthis, impl.gestalt()); + }, + onError: (error) => { + logger.Error().Err(error).Msg("WebSocket error"); + }, + onMessage: async (event, ws) => { + const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); + if (rMsg.isErr()) { + ws.send( + ende.encode( + buildErrorMsg( + sthis, + logger, + { + message: event.data, + } as ErrorMsg, + rMsg.Err() + ) + ) + ); + } else { + await dp.dispatch(impl, rMsg.Ok(), (msg) => { + const str = ende.encode(msg); + ws.send(str); + return new Response(str); + }); + } + }, + onClose: () => { + dp = undefined as unknown as MsgDispatcher; + // console.log('Connection closed') + }, + }; + })(new WSConnection(), c, next); + }) + ); + await this.factory.serve(app, port); + return this; + }); + } + async close() { + const ret = await this.factory.close(); + return ret; + } +} + +// export async function honoServer(_sthis: SuperThis, _msgP: MsgerParams, _gestalt: Gestalt) { +// const rt = runtimeFn(); +// if (rt.isNodeIsh) { +// // const { NodeHonoServer } = await import("./node-hono-server.js"); +// // return new HonoServer(sthis, msgP, gestalt, new NodeHonoServer()); +// } +// throw new Error("Not implemented"); +// } diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts new file mode 100644 index 00000000..17a07b34 --- /dev/null +++ b/src/v2-cloud/http-connection.ts @@ -0,0 +1,178 @@ +import { HttpHeader, Logger, Result, URI, exception2Result } from "@adviser/cement"; +import { SuperThis, ensureLogger } from "@fireproof/core"; +import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError } from "./msg-types.js"; +import { + ActiveStream, + ExchangedGestalt, + MsgerParamsWithEnDe, + MsgRawConnection, + OnMsgFn, + selectRandom, + timeout, + UnReg, +} from "./msger.js"; +import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; + +export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnection { + readonly logger: Logger; + readonly msgP: MsgerParamsWithEnDe; + + readonly baseURIs: URI[]; + + readonly #onMsg = new Map(); + + constructor(sthis: SuperThis, uris: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { + super(sthis, exGestalt); + this.logger = ensureLogger(sthis, "HttpConnection"); + // this.msgParam = msgP; + this.baseURIs = uris; + this.msgP = msgP; + } + + async start(): Promise> { + // if (this._qsOpen.req) { + // const sOpen = await this.request(this._qsOpen.req, { waitFor: MsgIsResOpen }); + // if (!MsgIsResOpen(sOpen)) { + // return Result.Err(this.logger.Error().Any("Err", sOpen).Msg("unexpected response").AsError()); + // } + // this._qsOpen.res = sOpen; + // } + return Result.Ok(undefined); + } + + async close(): Promise> { + await Promise.all(Array.from(this.activeBinds.values()).map((state) => state.controller?.close())); + this.#onMsg.clear(); + return Result.Ok(undefined); + } + + toMsg(msg: MsgWithError): MsgWithError { + this.#onMsg.forEach((fn) => fn(msg)); + return msg; + } + + onMsg(fn: OnMsgFn): UnReg { + const key = this.sthis.nextId().str; + this.#onMsg.set(key, fn); + return () => this.#onMsg.delete(key); + } + + #poll(state: ActiveStream): void { + this.request(state.bind.msg, state.bind.opts) + .then((msg) => { + try { + state.controller?.enqueue(msg); + if (MsgIsError(msg)) { + state.controller?.close(); + } else { + state.timeout = setTimeout(() => this.#poll(state), state.bind.opts.pollInterval ?? 1000); + } + } catch (err) { + console.log("poll error", err); + state.controller?.error(err); + state.controller?.close(); + } + }) + .catch((err) => { + console.log("poll catch error", err); + state.controller?.error(err); + // state.controller?.close(); + }); + } + + readonly activeBinds = new Map>(); + bind(req: Q, opts: RequestOpts): ReadableStream> { + const state: ActiveStream = { + id: this.sthis.nextId().str, + bind: { + msg: req, + opts, + }, + } satisfies ActiveStream; + this.activeBinds.set(state.id, state); + return new ReadableStream>({ + cancel: () => { + clearTimeout(state.timeout as number); + this.activeBinds.delete(state.id); + }, + start: (controller) => { + state.controller = controller; + this.#poll(state); + }, + }); + } + + async request(req: Q, _opts: RequestOpts): Promise> { + const headers = HttpHeader.from(); + headers.Set("Content-Type", this.msgP.mime); + headers.Set("Accept", this.msgP.mime); + + const rReqBody = exception2Result(() => this.msgP.ende.encode(req)); + if (rReqBody.isErr()) { + return this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + req, + this.logger.Error().Err(rReqBody.Err()).Any("req", req).Msg("encode error").AsError() + ) + ); + } + headers.Set("Content-Length", rReqBody.Ok().byteLength.toString()); + const url = selectRandom(this.baseURIs); + this.logger.Debug().Url(url).Any("body", req).Msg("request"); + const rRes = await exception2Result(() => + timeout( + this.msgP.timeout, + fetch(url.toString(), { + method: "PUT", + headers: headers.AsHeaderInit(), + body: rReqBody.Ok(), + }) + ) + ); + this.logger.Debug().Url(url).Any("body", rRes).Msg("response"); + if (rRes.isErr()) { + return this.toMsg( + buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Err(rRes).Msg("fetch error").AsError()) + ); + } + const res = rRes.Ok(); + if (!res.ok) { + return this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + req, + this.logger + .Error() + .Url(url) + .Str("status", res.status.toString()) + .Str("statusText", res.statusText) + .Msg("HTTP Error") + .AsError(), + await res.text() + ) + ); + } + const data = new Uint8Array(await res.arrayBuffer()); + const ret = await exception2Result(async () => this.msgP.ende.decode(data) as S); + if (ret.isErr()) { + return this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + req, + this.logger.Error().Err(ret.Err()).Msg("decode error").AsError(), + this.sthis.txt.decode(data) + ) + ); + } + return this.toMsg(ret.Ok()); + } + + // toOnMessage(msg: WithErrorMsg): Result> { + // this.mec.msgFn?.(msg as unknown as MessageEvent); + // return Result.Ok(msg); + // } +} diff --git a/src/v2-cloud/meta-merger/abstract-sql.ts b/src/v2-cloud/meta-merger/abstract-sql.ts new file mode 100644 index 00000000..445c5d2a --- /dev/null +++ b/src/v2-cloud/meta-merger/abstract-sql.ts @@ -0,0 +1,53 @@ +// import { RunResult } from "better-sqlite3"; + +// export function now() { +// return new Date().toISOString(); +// } + +// export interface SqlLiteStmt { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// bind(...args: any[]): any; +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// run(...args: any[]): Promise; +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// get(...args: any[]): Promise; +// } + +// export interface SqlLite { +// prepare(sql: string): SqlLiteStmt; +// } + +// export interface SqlLiteDBDialect { + +// } + +// export type SQLLiteFlavor = BaseSQLiteDatabase<'async', unknown>; + +export interface SQLDatabase { + prepare(sql: string): SQLStatement; +} + +export type SQLParams = (string | number | Date)[]; + +// export type SQLRow = Record; + +export interface SQLStatement { + run(...params: SQLParams): Promise; + all(...params: SQLParams): Promise; +} + +export function conditionalDrop(drop: boolean, tabName: string, create: string): string[] { + if (!drop) { + return [create]; + } + return [`DROP TABLE IF EXISTS ${tabName}`, create]; +} + +export function sqliteCoerceParams(params: SQLParams): (string | number)[] { + return params.map((i) => { + if (i instanceof Date) { + return i.toISOString(); + } + return i; + }); +} diff --git a/src/v2-cloud/meta-merger/bettersql-abstract-sql.ts b/src/v2-cloud/meta-merger/bettersql-abstract-sql.ts new file mode 100644 index 00000000..e28bf869 --- /dev/null +++ b/src/v2-cloud/meta-merger/bettersql-abstract-sql.ts @@ -0,0 +1,36 @@ +import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "./abstract-sql.js"; + +import Database from "better-sqlite3"; + +export class BetterSQLStatement implements SQLStatement { + readonly stmt: Database.Statement; + constructor(stmt: Database.Statement) { + this.stmt = stmt; + } + + async run(...iparams: SQLParams): Promise { + const res = (await this.stmt.run(...sqliteCoerceParams(iparams))) as T; + // console.log("run", res); + return res; + } + async all(...params: SQLParams): Promise { + const res = (await this.stmt.all(...sqliteCoerceParams(params))) as T[]; + // console.log("all", res); + return res; + } +} + +export class BetterSQLDatabase implements SQLDatabase { + readonly db: Database.Database; + constructor(dbOrPath: Database.Database | string) { + if (typeof dbOrPath === "string") { + this.db = new Database(dbOrPath); + } else { + this.db = dbOrPath; + } + } + + prepare(sql: string): SQLStatement { + return new BetterSQLStatement(this.db.prepare(sql)); + } +} diff --git a/src/v2-cloud/meta-merger/cf-worker-abstract-sql.ts b/src/v2-cloud/meta-merger/cf-worker-abstract-sql.ts new file mode 100644 index 00000000..f1cabf1e --- /dev/null +++ b/src/v2-cloud/meta-merger/cf-worker-abstract-sql.ts @@ -0,0 +1,31 @@ +import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "./abstract-sql.js"; + +import type { D1Database } from "@cloudflare/workers-types"; + +export class CFWorkerSQLStatement implements SQLStatement { + readonly stmt: D1PreparedStatement; + constructor(stmt: D1PreparedStatement) { + this.stmt = stmt; + } + + async run(...iparams: SQLParams): Promise { + const bound = this.stmt.bind(...sqliteCoerceParams(iparams)); + // console.log("cf-run", sqliteCoerceParams(iparams), bound); + return bound.run() as T; + } + async all(...params: SQLParams): Promise { + const rows = await this.stmt.bind(...sqliteCoerceParams(params)).run(); + return rows.results as T[]; + } +} + +export class CFWorkerSQLDatabase implements SQLDatabase { + readonly db: D1Database; + constructor(db: D1Database) { + this.db = db; + } + + prepare(sql: string): SQLStatement { + return new CFWorkerSQLStatement(this.db.prepare(sql)); + } +} diff --git a/src/v2-cloud/meta-merger/create-schema-cli.ts b/src/v2-cloud/meta-merger/create-schema-cli.ts new file mode 100644 index 00000000..7ebd858e --- /dev/null +++ b/src/v2-cloud/meta-merger/create-schema-cli.ts @@ -0,0 +1,9 @@ +import { MetaSendSql } from "./meta-send.js"; + +async function main() { + // eslint-disable-next-line no-console + console.log(MetaSendSql.schema(true).join(";\n")); +} + +// eslint-disable-next-line no-console +main().catch(console.error); diff --git a/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts b/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts new file mode 100644 index 00000000..b8f0e94b --- /dev/null +++ b/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts @@ -0,0 +1,173 @@ +import { ResolveOnce } from "@adviser/cement"; +import { CRDTEntry } from "@fireproof/core"; +import { TenantLedgerSql } from "./tenant-ledger.js"; +import { ByConnection } from "./meta-merger.js"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; + +export interface MetaByTenantLedgerRow { + readonly tenant: string; + readonly ledger: string; + readonly reqId: string; + readonly resId: string; + readonly metaCID: string; + readonly meta: CRDTEntry; + readonly updateAt: Date; +} + +interface SQLMetaByTenantLedgerRow { + readonly tenant: string; + readonly ledger: string; + readonly reqId: string; + readonly resId: string; + readonly metaCID: string; + readonly meta: string; + readonly updateAt: string; +} + +/* +SELECT * FROM Mitarbeiter e1 +WHERE NOT EXISTS +( + SELECT 1 FROM Mitarbeiter e2 + WHERE e1.employee_id=e2.employee_id und e2.employee_name LIKE 'A%' +); + */ + +export class MetaByTenantLedgerSql { + static schema(drop = false) { + return [ + ...TenantLedgerSql.schema(drop), + ...conditionalDrop( + drop, + "MetaByTenantLedger", + ` + CREATE TABLE IF NOT EXISTS MetaByTenantLedger( + tenant TEXT NOT NULL, + ledger TEXT NOT NULL, + reqId TEXT NOT NULL, + resId TEXT NOT NULL, + metaCID TEXT NOT NULL, + meta TEXT NOT NULL, + updatedAt TEXT NOT NULL, + PRIMARY KEY (tenant, ledger, reqId, resId, metaCID), + UNIQUE(metaCID), + FOREIGN KEY (tenant, ledger) REFERENCES TenantLedger(tenant, ledger) + ) + ` + ), + ]; + } + + readonly db: SQLDatabase; + readonly tenantLedgerSql: TenantLedgerSql; + constructor(db: SQLDatabase, tenantLedgerSql: TenantLedgerSql) { + this.db = db; + this.tenantLedgerSql = tenantLedgerSql; + } + + readonly #sqlCreateMetaByTenantLedger = new ResolveOnce(); + sqlCreateMetaByTenantLedger(): SQLStatement[] { + return this.#sqlCreateMetaByTenantLedger.once(() => { + return MetaByTenantLedgerSql.schema().map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertMetaByTenantLedger = new ResolveOnce(); + sqlEnsureMetaByTenantLedger(): SQLStatement { + return this.#sqlInsertMetaByTenantLedger.once(() => { + return this.db.prepare(` + INSERT INTO MetaByTenantLedger(tenant, ledger, reqId, resId, metaCID, meta, updatedAt) + SELECT ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS ( + SELECT 1 FROM MetaByTenantLedger WHERE metaCID = ? + ) + `); + }); + } + + readonly #sqlDeleteByConnection = new ResolveOnce(); + sqlDeleteByConnection(): SQLStatement { + return this.#sqlDeleteByConnection.once(() => { + return this.db.prepare(` + DELETE FROM MetaByTenantLedger + WHERE + tenant = ? + AND + ledger = ? + AND + reqId = ? + AND + resId = ? + AND + metaCID NOT IN (SELECT value FROM json_each(?)) + `); + }); + } + + /* + * select * from MetaByTenantLedger where tenant = 'tenant' and ledger = 'ledger' group by metaCID + */ + + // readonly #sqlSelectByMetaCIDs = new ResolveOnce() + // sqlSelectByMetaCIDs(): Statement { + // return this.#sqlSelectByMetaCIDs.once(() => { + // return this.db.prepare(` + // SELECT tenant, ledger, reqId, resId, metaCID, meta, updatedAt + // FROM MetaByTenantLedger + // WHERE metaCID in ? + // `); + // }) + // } + // async selectByMetaCIDs(metaCIDs: string[]): Promise { + // const stmt = this.sqlSelectByMetaCIDs(); + // const rows = await stmt.all(metaCIDs) + // return rows.map(row => ({ + // ...row, + // meta: JSON.parse(row.meta), + // updateAt: new Date(row.updateAt) + // } satisfies MetaByTenantLedgerRow)) + // } + + async deleteByConnection(t: ByConnection & { metaCIDs: string[] }) { + const stmt = this.sqlDeleteByConnection(); + return stmt.run(t.tenant, t.ledger, t.reqId, t.resId, JSON.stringify(t.metaCIDs)); + } + + async ensure(t: MetaByTenantLedgerRow) { + const stmt = this.sqlEnsureMetaByTenantLedger(); + return stmt.run( + t.tenant, + t.ledger, + t.reqId, + t.resId, + t.metaCID, + JSON.stringify(t.meta), + t.updateAt.toISOString(), + t.metaCID + ); + } + + readonly #sqlSelectByConnection = new ResolveOnce(); + sqlSelectByConnection(): SQLStatement { + return this.#sqlSelectByConnection.once(() => { + return this.db.prepare(` + SELECT tenant, ledger, reqId, resId, metaCID, meta, updatedAt + FROM MetaByTenantLedger + WHERE tenant = ? AND ledger = ? AND reqId = ? AND resId = ? + ORDER BY updatedAt + `); + }); + } + + async selectByConnection(conn: ByConnection): Promise { + const stmt = this.sqlSelectByConnection(); + const rows = await stmt.all(conn.tenant, conn.ledger, conn.reqId, conn.resId); + return rows.map( + (row) => + ({ + ...row, + meta: JSON.parse(row.meta), + updateAt: new Date(row.updateAt), + }) satisfies MetaByTenantLedgerRow + ); + } +} diff --git a/src/v2-cloud/meta-merger/meta-merger.test.ts b/src/v2-cloud/meta-merger/meta-merger.test.ts new file mode 100644 index 00000000..621715eb --- /dev/null +++ b/src/v2-cloud/meta-merger/meta-merger.test.ts @@ -0,0 +1,245 @@ +// import type { Database } from "better-sqlite3"; +import { Connection, MetaMerger } from "./meta-merger.js"; +import { CRDTEntry, ensureSuperThis } from "@fireproof/core"; +import { runtimeFn } from "@adviser/cement"; +import { SQLDatabase } from "./abstract-sql.js"; +import type { Env } from "../backend/env.js"; +import { getBackendDurableObject } from "../backend/cf-hono-server.js"; + +function sortCRDTEntries(rows: CRDTEntry[]) { + return rows.sort((a, b) => a.cid.localeCompare(b.cid)); +} + +interface MetaConnection { + readonly metas: CRDTEntry[]; + readonly connection: Connection; +} + +function toCRDTEntries(rows: MetaConnection[]) { + return rows.reduce((r, i) => [...r, ...i.metas], [] as CRDTEntry[]); +} + +// function filterConnection(ref: MetaConnection[], connection: Connection) { +// return toCRDTEntries(ref.filter((r) => +// (r.connection.tenant.tenant === connection.tenant.tenant && +// r.connection.tenant.ledger === connection.tenant.ledger && +// r.connection.conn.reqId === connection.conn.reqId && +// r.connection.conn.resId === connection.conn.resId))) +// } + +function getSQLFlavours(): { name: string; factory: () => Promise }[] { + if (runtimeFn().isCFWorker) { + return [ + { + name: "cf-worker-d1", + factory: async () => { + const { CFWorkerSQLDatabase } = await import("./cf-worker-abstract-sql.js"); + const { env } = await import("cloudflare:test"); + return new CFWorkerSQLDatabase((env as Env).FP_BACKEND_D1); + }, + }, + { + name: "cf-worker-do", + factory: async () => { + const { CFDObjSQLDatabase } = await import("../backend/cf-dobj-abstract-sql.js"); + const { env } = await import("cloudflare:test"); + return new CFDObjSQLDatabase(getBackendDurableObject(env as Env)); + }, + }, + ]; + } else { + return [ + { + name: "bettersql", + factory: async () => { + const { BetterSQLDatabase } = await import("./bettersql-abstract-sql.js"); + return new BetterSQLDatabase("./dist/test.db"); + }, + }, + ]; + } +} + +describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { + // let db: SQLDatabase; + const sthis = ensureSuperThis(); + const logger = sthis.logger; + let mm: MetaMerger; + beforeAll(async () => { + // db = new Database(':memory:'); + const db = await flavour.factory(); + mm = new MetaMerger(db); + await mm.createSchema(); + }); + + let connection: Connection; + beforeEach(() => { + connection = { + tenant: { + tenant: `tenant${sthis.timeOrderedNextId().str}`, + ledger: "ledger", + }, + conn: { + reqId: "reqId", + resId: `resId-${sthis.timeOrderedNextId().str}`, + }, + } satisfies Connection; + }); + + afterEach(async () => { + await mm.delMeta({ + logger, + connection, + }); + }); + + it("insert nothing", async () => { + await mm.addMeta({ + logger, + connection, + metas: [], + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + expect(rows).toEqual([]); + }); + + it("insert one multiple", async () => { + const cid = sthis.timeOrderedNextId().str; + for (let i = 0; i < 10; i++) { + const metas = Array(i).fill({ + cid: cid, + parents: [], + data: "MomRkYXRho", + }); + await mm.addMeta({ + logger, + connection, + metas, + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + if (i === 1) { + expect(rows).toEqual(metas); + } else { + expect(rows).toEqual([]); + } + } + }); + + it("insert multiple", async () => { + const conns = []; + for (let i = 0; i < 10; i++) { + const metas = Array(i) + .fill({ + cid: "x", + parents: [], + data: "MomRkYXRho", + }) + .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); + const conn = { + ...connection.conn, + reqId: sthis.timeOrderedNextId().str, + }; + conns.push(conn); + await mm.addMeta({ + logger, + connection: { + ...connection, + conn, + } satisfies Connection, + metas, + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(metas)); + } + await Promise.all( + conns.map(async (conn) => + mm.delMeta({ + logger, + connection: { ...connection, conn }, + metas: [], + }) + ) + ); + }); + + it("metaToSend to sink", async () => { + const connections = Array(2) + .fill(connection) + .map((c) => ({ ...c, conn: { ...c.conn, reqId: sthis.timeOrderedNextId().str } })); + const ref: MetaConnection[] = []; + for (const connection of connections) { + const metas = Array(2) + .fill({ + cid: "x", + parents: [], + data: "MomRkYXRho", + }) + .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); + ref.push({ metas, connection }); + await mm.addMeta({ + logger, + connection, + metas, + now: new Date(), + }); + } + // wrote 10 connections with 3 metas each + for (const connection of connections) { + const rows = await mm.metaToSend(connection); + expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(toCRDTEntries(ref))); + const rowsEmpty = await mm.metaToSend(connection); + expect(sortCRDTEntries(rowsEmpty)).toEqual([]); + } + const newConnections = Array(2) + .fill(connection) + .map((c) => ({ ...c, conn: { ...c.conn, reqId: sthis.timeOrderedNextId().str } })); + for (const connection of newConnections) { + const rows = await mm.metaToSend(connection); + expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(toCRDTEntries(ref))); + const rowsEmpty = await mm.metaToSend(connection); + expect(sortCRDTEntries(rowsEmpty)).toEqual([]); + } + await Promise.all( + connections.map(async (connection) => + mm.delMeta({ + logger, + connection, + metas: [], + }) + ) + ); + }); + + it("delMeta", async () => { + await mm.addMeta({ + logger, + connection, + metas: [ + { + cid: `del-${sthis.timeOrderedNextId().str}`, + parents: [], + data: "MomRkYXRho", + }, + { + cid: `del-${sthis.timeOrderedNextId().str}`, + parents: [], + data: "MomRkYXRho", + }, + ], + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + expect(rows.length).toBe(2); + await mm.delMeta({ + logger, + connection, + metas: rows, + now: new Date(), + }); + const rowsDel = await mm.metaToSend(connection); + expect(rowsDel.length).toBe(0); + }); +}); diff --git a/src/v2-cloud/meta-merger/meta-merger.ts b/src/v2-cloud/meta-merger/meta-merger.ts new file mode 100644 index 00000000..5b7a62d5 --- /dev/null +++ b/src/v2-cloud/meta-merger/meta-merger.ts @@ -0,0 +1,116 @@ +import { CRDTEntry, Logger } from "@fireproof/core"; +import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; +import { MetaSendSql } from "./meta-send.js"; +import { TenantLedgerSql } from "./tenant-ledger.js"; +import { TenantSql } from "./tenant.js"; +import { SQLDatabase } from "./abstract-sql.js"; +import { QSId, TenantLedger } from "../msg-types.js"; + +export interface Connection { + readonly tenant: TenantLedger; + readonly conn: QSId; +} + +export interface MetaMerge { + readonly logger: Logger; + readonly connection: Connection; + readonly metas: CRDTEntry[]; + readonly now?: Date; +} + +export interface ByConnection { + readonly tenant: string; + readonly ledger: string; + readonly reqId: string; + readonly resId: string; +} + +function toByConnection(connection: Connection): ByConnection { + return { + ...connection.conn, + ...connection.tenant, + }; +} + +export class MetaMerger { + readonly db: SQLDatabase; + // readonly sthis: SuperThis; + readonly sql: { + readonly tenant: TenantSql; + readonly tenantLedger: TenantLedgerSql; + readonly metaByTenantLedger: MetaByTenantLedgerSql; + readonly metaSend: MetaSendSql; + }; + + constructor(db: SQLDatabase) { + this.db = db; + // this.sthis = sthis; + const tenant = new TenantSql(db); + const tenantLedger = new TenantLedgerSql(db, tenant); + this.sql = { + tenant, + tenantLedger, + metaByTenantLedger: new MetaByTenantLedgerSql(db, tenantLedger), + metaSend: new MetaSendSql(db), + }; + } + + async createSchema(drop = false) { + for (const i of this.sql.metaSend.sqlCreateMetaSend(drop)) { + await i.run(); + } + } + + async delMeta( + mm: Omit & { readonly metas?: CRDTEntry[] } + ): Promise<{ now: Date; byConnection: ByConnection }> { + const now = mm.now || new Date(); + const byConnection = toByConnection(mm.connection); + const metaCIDs = (mm.metas ?? []).map((meta) => meta.cid); + const connCIDs = { + ...byConnection, + // needs something with is not empty to delete + metaCIDs: metaCIDs.length ? metaCIDs : [new Date().toISOString()], + }; + await this.sql.metaSend.deleteByConnection(connCIDs); + await this.sql.metaByTenantLedger.deleteByConnection(connCIDs); + return { now, byConnection }; + } + + async addMeta(mm: MetaMerge) { + if (!mm.metas.length) { + return; + } + const { now, byConnection } = await this.delMeta(mm); + await this.sql.tenantLedger.ensure({ + ...mm.connection.tenant, + createdAt: now, + }); + for (const meta of mm.metas) { + try { + await this.sql.metaByTenantLedger.ensure({ + ...byConnection, + metaCID: meta.cid, + meta: meta, + updateAt: now, + }); + } catch (e) { + mm.logger.Warn().Err(e).Str("metaCID", meta.cid).Msg("addMeta"); + } + } + } + + async metaToSend(sink: Connection, now = new Date()): Promise { + const bySink = toByConnection(sink); + const rows = await this.sql.metaSend.selectToAddSend({ ...bySink, now }); + await this.sql.metaSend.insert( + rows.map((row) => ({ + metaCID: row.metaCID, + reqId: row.reqId, + resId: row.resId, + sendAt: row.sendAt, + })) + ); + return rows.map((row) => row.meta); + } +} diff --git a/src/v2-cloud/meta-merger/meta-send.ts b/src/v2-cloud/meta-merger/meta-send.ts new file mode 100644 index 00000000..2debc00a --- /dev/null +++ b/src/v2-cloud/meta-merger/meta-send.ts @@ -0,0 +1,128 @@ +import { ResolveOnce } from "@adviser/cement"; +import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; +import { ByConnection } from "./meta-merger.js"; +import { CRDTEntry } from "@fireproof/core"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; + +export interface MetaSendRow { + readonly metaCID: string; + readonly reqId: string; + readonly resId: string; + readonly sendAt: Date; +} + +type SQLMetaSendRowWithMeta = MetaSendRow & { meta: string }; +export type MetaSendRowWithMeta = MetaSendRow & { meta: CRDTEntry }; + +export class MetaSendSql { + static schema(drop = false) { + return [ + ...MetaByTenantLedgerSql.schema(drop), + ...conditionalDrop( + drop, + "MetaSend", + ` + CREATE TABLE IF NOT EXISTS MetaSend ( + metaCID TEXT NOT NULL, + reqId TEXT NOT NULL, + resId TEXT NOT NULL, + sendAt TEXT NOT NULL, + PRIMARY KEY(metaCID,reqId,resId), + FOREIGN KEY(metaCID) REFERENCES MetaByTenantLedger(metaCID) + ); + ` + ), + ]; + } + + readonly db: SQLDatabase; + constructor(db: SQLDatabase) { + this.db = db; + } + + readonly #sqlCreateMetaSend = new ResolveOnce(); + sqlCreateMetaSend(drop: boolean): SQLStatement[] { + return this.#sqlCreateMetaSend.once(() => { + return MetaSendSql.schema(drop).map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertMetaSend = new ResolveOnce(); + sqlInsertMetaSend(): SQLStatement { + return this.#sqlInsertMetaSend.once(() => { + return this.db.prepare(` + INSERT INTO MetaSend(metaCID, reqId, resId, sendAt) VALUES(?, ?, ?, ?) + `); + }); + } + + readonly #sqlSelectToAddSend = new ResolveOnce(); + sqlSelectToAddSend(): SQLStatement { + return this.#sqlSelectToAddSend.once(() => { + return this.db.prepare(` + SELECT t.metaCID, ? as reqId, ? as resId, ? as sendAt, t.meta FROM MetaByTenantLedger as t + WHERE + t.tenant = ? + AND + t.ledger = ? + AND + NOT EXISTS (SELECT 1 FROM MetaSend AS s WHERE t.metaCID = s.metaCID and s.reqId = ? and s.resId = ?) + `); + }); + } + + async selectToAddSend(conn: ByConnection & { now: Date }): Promise { + const stmt = this.sqlSelectToAddSend(); + const rows = await stmt.all( + conn.reqId, + conn.resId, + conn.now, + conn.tenant, + conn.ledger, + conn.reqId, + conn.resId + ); + return rows.map( + (i) => + ({ + metaCID: i.metaCID, + reqId: i.reqId, + resId: i.resId, + sendAt: new Date(i.sendAt), + meta: JSON.parse(i.meta) as CRDTEntry, + }) satisfies MetaSendRowWithMeta + ); + } + + async insert(t: MetaSendRow[]) { + const stmt = this.sqlInsertMetaSend(); + for (const i of t) { + await stmt.run(i.metaCID, i.reqId, i.resId, i.sendAt.toISOString()); + } + } + + readonly #sqlDeleteByConnection = new ResolveOnce(); + sqlDeleteByMetaCID(): SQLStatement { + return this.#sqlDeleteByConnection.once(() => { + return this.db.prepare(` + DELETE FROM MetaSend + WHERE metaCID in (SELECT metaCID FROM MetaByTenantLedger + WHERE + tenant = ? + AND + ledger = ? + AND + reqId = ? + AND + resId = ? + AND + metaCID NOT IN (SELECT value FROM json_each(?))) + `); + }); + } + + async deleteByConnection(dmi: ByConnection & { metaCIDs: string[] }) { + const stmt = this.sqlDeleteByMetaCID(); + return stmt.run(dmi.tenant, dmi.ledger, dmi.reqId, dmi.resId, JSON.stringify(dmi.metaCIDs)); + } +} diff --git a/src/v2-cloud/meta-merger/tenant-ledger.ts b/src/v2-cloud/meta-merger/tenant-ledger.ts new file mode 100644 index 00000000..b5dc5fa7 --- /dev/null +++ b/src/v2-cloud/meta-merger/tenant-ledger.ts @@ -0,0 +1,62 @@ +import { ResolveOnce } from "@adviser/cement"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; +import { TenantSql } from "./tenant.js"; + +export interface TenantLedgerRow { + readonly tenant: string; + readonly ledger: string; + readonly createdAt: Date; +} + +export class TenantLedgerSql { + static schema(drop = false) { + return [ + ...TenantSql.schema(drop), + ...conditionalDrop( + drop, + "TenantLedger", + ` + CREATE TABLE IF NOT EXISTS TenantLedger( + tenant TEXT NOT NULL, + ledger TEXT NOT NULL, + createdAt TEXT NOT NULL, + PRIMARY KEY(tenant, ledger), + FOREIGN KEY(tenant) REFERENCES Tenant(tenant) + ) + ` + ), + ]; + } + + readonly db: SQLDatabase; + readonly tenantSql: TenantSql; + constructor(db: SQLDatabase, tenantSql: TenantSql) { + this.db = db; + this.tenantSql = tenantSql; + } + + readonly #sqlCreateTenantLedger = new ResolveOnce(); + sqlCreateTenantLedger(): SQLStatement[] { + return this.#sqlCreateTenantLedger.once(() => { + return TenantLedgerSql.schema().map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertTenantLedger = new ResolveOnce(); + sqlEnsureTenantLedger(): SQLStatement { + return this.#sqlInsertTenantLedger.once(() => { + return this.db.prepare(` + INSERT INTO TenantLedger(tenant, ledger, createdAt) + SELECT ?, ?, ? WHERE + NOT EXISTS(SELECT 1 FROM TenantLedger WHERE tenant = ? and ledger = ?) + `); + }); + } + + async ensure(t: TenantLedgerRow) { + await this.tenantSql.ensure({ tenant: t.tenant, createdAt: t.createdAt }); + const stmt = this.sqlEnsureTenantLedger(); + const ret = stmt.run(t.tenant, t.ledger, t.createdAt, t.tenant, t.ledger); + return ret; + } +} diff --git a/src/v2-cloud/meta-merger/tenant.ts b/src/v2-cloud/meta-merger/tenant.ts new file mode 100644 index 00000000..3ee67001 --- /dev/null +++ b/src/v2-cloud/meta-merger/tenant.ts @@ -0,0 +1,51 @@ +import { ResolveOnce } from "@adviser/cement"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; + +export interface TenantRow { + readonly tenant: string; + readonly createdAt: Date; +} + +export class TenantSql { + static schema(drop = false): string[] { + return [ + ...conditionalDrop( + drop, + "Tenant", + ` + CREATE TABLE IF NOT EXISTS Tenant( + tenant TEXT NOT NULL PRIMARY KEY, + createdAt TEXT NOT NULL + ) + ` + ), + ]; + } + + readonly db: SQLDatabase; + constructor(db: SQLDatabase) { + this.db = db; + } + + readonly #sqlCreateTenant = new ResolveOnce(); + sqlCreateTenant(): SQLStatement[] { + return this.#sqlCreateTenant.once(() => { + return TenantSql.schema().map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertTenant = new ResolveOnce(); + sqlEnsureTenant(): SQLStatement { + return this.#sqlInsertTenant.once(() => { + return this.db.prepare(` + INSERT INTO Tenant(tenant, createdAt) + SELECT ?, ? WHERE NOT EXISTS(SELECT 1 FROM Tenant WHERE tenant = ?) + `); + }); + } + + async ensure(t: TenantRow) { + const stmt = this.sqlEnsureTenant(); + return stmt.run(t.tenant, t.createdAt, t.tenant); + } +} diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts new file mode 100644 index 00000000..dc39b54a --- /dev/null +++ b/src/v2-cloud/msg-dispatch.ts @@ -0,0 +1,139 @@ +import { Logger } from "@adviser/cement"; +import { SuperThis, ensureLogger } from "@fireproof/core"; +import { Gestalt, MsgBase, buildErrorMsg, MsgWithError, MsgIsWithConn, MsgWithConn, QSId } from "./msg-types.js"; + +import { PreSignedMsg } from "./pre-signed-url.js"; +import { HonoServerImpl } from "./hono-server.js"; +import { UnReg } from "./msger.js"; + +export interface MsgContext { + calculatePreSignedUrl(p: PreSignedMsg): Promise; +} + +export interface WSPair { + readonly client: WebSocket; + readonly server: WebSocket; +} + +export class WSConnection { + wspair?: WSPair; + + attachWSPair(wsp: WSPair) { + if (!this.wspair) { + this.wspair = wsp; + } else { + throw new Error("wspair already set"); + } + } +} + +type Promisable = T | Promise; + +// function WithValidConn(msg: T, rri?: ResOpen): msg is MsgWithConn { +// return MsgIsWithConn(msg) && !!rri && rri.conn.resId === msg.conn.resId && rri.conn.reqId === msg.conn.reqId; +// } + +interface ConnItem { + conn: QSId; + touched: Date; +} + +class ConnectionManager { + readonly conns = new Map(); + readonly maxItems: number; + + constructor(maxItems?: number) { + this.maxItems = maxItems || 100; + } + + addConn(conn: QSId): QSId { + if (this.conns.size >= this.maxItems) { + const oldest = Array.from(this.conns.values()); + const oneHourAgo = new Date(new Date().getTime() - 60 * 60 * 1000).getTime(); + oldest + .filter((item) => item.touched.getTime() < oneHourAgo) + .forEach((item) => this.conns.delete(item.conn.resId)); + } + this.conns.set(`${conn.reqId}:${conn.resId}`, { conn, touched: new Date() }); + return conn; + } + + isConnected(msg: MsgBase): msg is MsgWithConn { + if (!MsgIsWithConn(msg)) { + return false; + } + return this.conns.has(`${msg.conn.reqId}:${msg.conn.resId}`); + } +} +const connManager = new ConnectionManager(); + +export interface MsgDispatcherCtx { + readonly impl: HonoServerImpl; +} +export interface MsgDispatchItem { + readonly match: (msg: MsgBase) => boolean; + readonly isNotConn?: boolean; + fn(sthis: SuperThis, logger: Logger, ctx: MsgDispatcherCtx, msg: Q): Promisable>; +} + +export class MsgDispatcher { + readonly sthis: SuperThis; + readonly logger: Logger; + // wsConn?: WSConnection; + readonly gestalt: Gestalt; + readonly id: string; + + readonly connManager = connManager; + + static new(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { + return new MsgDispatcher(sthis, gestalt); + } + + private constructor(sthis: SuperThis, gestalt: Gestalt) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "Dispatcher"); + this.gestalt = gestalt; + this.id = sthis.nextId().str; + } + + // addConn(msg: MsgBase): Result { + // if (!MsgIsReqOpenWithConn(msg)) { + // return this.logger.Error().Msg("msg missing reqId").ResultError(); + // } + // return Result.Ok(connManager.addConn(msg.conn)); + // } + + readonly items = new Map>(); + registerMsg(...iItems: MsgDispatchItem[]): UnReg { + const items = iItems.flat(); + const ids: string[] = items.map((item) => { + const id = this.sthis.nextId(12).str; + this.items.set(id, item); + return id; + }); + return () => ids.forEach((id) => this.items.delete(id)); + } + + async dispatch(ctx: HonoServerImpl, msg: MsgBase, send: (msg: MsgBase) => Promisable): Promise { + const validateConn = async ( + msg: T, + fn: (msg: MsgWithConn) => Promisable> + ): Promise => { + if (!connManager.isConnected(msg)) { + return send(buildErrorMsg(this.sthis, this.logger, { ...msg }, new Error("dispatch missing connection"))); + // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); + } + // if (WithValidConn(msg, this.myOpen)) { + const r = await fn(msg); + return Promise.resolve(send(r)); + }; + const found = Array.from(this.items.values()).find((item) => item.match(msg)); + if (!found) { + return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); + } + if (!found.isNotConn) { + return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, { impl: ctx }, msg)); + } + return send(await found.fn(this.sthis, this.logger, { impl: ctx }, msg)); + } +} diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts new file mode 100644 index 00000000..1645fc23 --- /dev/null +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -0,0 +1,127 @@ +import { SuperThis } from "@fireproof/core"; +import { MsgDispatcher } from "./msg-dispatch.js"; +import { + MsgIsReqGetData, + buildResGetData, + MsgIsReqPutData, + MsgIsReqDelData, + buildResDelData, + buildResPutData, + ReqGetData, + ReqPutData, + ReqDelData, +} from "./msg-types-data.js"; +import { + MsgIsReqDelWAL, + MsgIsReqGetWAL, + MsgIsReqPutWAL, + ReqDelWAL, + ReqGetWAL, + ReqPutWAL, + buildResDelWAL, + buildResGetWAL, + buildResPutWAL, +} from "./msg-types-wal.js"; +import { + MsgIsReqGestalt, + buildResGestalt, + MsgIsReqOpen, + buildErrorMsg, + buildResOpen, + MsgIsReqOpenWithConn, + MsgWithConn, + ReqGestalt, + Gestalt, +} from "./msg-types.js"; +import { + BindGetMeta, + MsgIsBindGetMeta, + MsgIsReqDelMeta, + MsgIsReqPutMeta, + ReqDelMeta, + ReqPutMeta, +} from "./msg-type-meta.js"; + +export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { + const dp = MsgDispatcher.new(sthis, gestalt); + dp.registerMsg( + { + match: MsgIsReqGestalt, + isNotConn: true, + fn: (_sthis, _logger, _ctx, msg: ReqGestalt) => { + return buildResGestalt(msg, dp.gestalt); + }, + }, + { + match: MsgIsReqOpen, + isNotConn: true, + fn: (sthis, logger, _ctx, msg) => { + if (!MsgIsReqOpenWithConn(msg)) { + return buildErrorMsg(sthis, logger, msg, new Error("missing connection")); + } + if (dp.connManager.isConnected(msg)) { + return buildResOpen(sthis, msg, msg.conn.resId); + } + const resId = sthis.nextId(12).str; + const resOpen = buildResOpen(sthis, msg, resId); + dp.connManager.addConn(resOpen.conn); + return resOpen; + }, + }, + { + match: MsgIsReqGetData, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResGetData(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqPutData, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResPutData(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqDelData, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResDelData(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqGetWAL, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResGetWAL(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqPutWAL, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResPutWAL(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqDelWAL, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResDelWAL(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsBindGetMeta, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return ctx.impl.handleBindGetMeta(sthis, logger, msg); + }, + }, + { + match: MsgIsReqPutMeta, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return ctx.impl.handleReqPutMeta(sthis, logger, msg); + }, + }, + { + match: MsgIsReqDelMeta, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return ctx.impl.handleReqDelMeta(sthis, logger, msg); + }, + } + ); + return dp; +} diff --git a/src/v2-cloud/msg-processor.ts-off b/src/v2-cloud/msg-processor.ts-off new file mode 100644 index 00000000..788e3884 --- /dev/null +++ b/src/v2-cloud/msg-processor.ts-off @@ -0,0 +1,261 @@ +import { exception2Result, Logger, } from "@adviser/cement"; +import { + buildErrorMsg, + buildResDelMeta, + buildResGestalt, + buildResGetMeta, + buildResPutMeta, + defaultGestalt, + ErrorMsg, + getStoreFromType, + MsgBase, + MsgIsReqDelData, + MsgIsReqDelMeta, + MsgIsReqDelWAL, + MsgIsReqGestalt, + MsgIsReqGetData, + MsgIsReqGetMeta, + MsgIsReqGetWAL, + MsgIsReqPutData, + MsgIsReqPutMeta, + MsgIsReqPutWAL, + MsgIsReqSubscribeMeta, + ReqDelMeta, + ReqGetMeta, + ReqOptRes, + ReqPutMeta, + ReqRes, + ResDelMeta, + ResGetMeta, + ResPutMeta, +} from "./msg-types.js"; +import { calculatePreSignedUrl } from "./pre-signed-url.js"; +import { SuperThis } from "@fireproof/core"; + +export type WithErrorMsg = T | ErrorMsg; + +export interface CtxBase { + readonly logger: Logger; +} + +export interface ReqOptResCtx extends ReqOptRes { + readonly ctx?: C; +} + +export interface ReqResCtx extends ReqRes { + readonly ctx: C; +} + +export interface MsgProcessor { + dispatch( + decodeFn: () => Promise + ): Promise, O>>; + + // signedUrl(req: ReqSignedUrl, ctx: CtxBase): Promise>; + // subscribeMeta(req: ReqSubscribeMeta, ctx: CtxBase): Promise>; + + // delMeta(req: ReqDelMeta, ctx: CtxBase): Promise>; + // putMeta(req: ReqPutMeta, ctx: CtxBase): Promise>; + // getMeta(req: ReqGetMeta, ctx: CtxBase): Promise>; +} + +export interface RequestOpts { + readonly waitFor: (msg: MsgBase) => boolean; + readonly timeout?: number; // ms +} +// export interface Connection { +// readonly ws: WebSocket; +// readonly key: ConnectionKey; +// request(msg: MsgBase, opts: RequestOpts): Promise>; +// onMessage(msgFn: (msg: MsgBase) => void): () => void; +// close(): Promise; +// } + +export abstract class MsgProcessorBase implements MsgProcessor { + readonly logger: Logger; + readonly serverId: string; + readonly ctx: O; + readonly sthis: SuperThis; + constructor(sthis: SuperThis, logger: Logger, ctx: O, serverId: string) { + this.serverId = serverId; + this.logger = logger; + this.ctx = ctx; + this.sthis = sthis; + } + + async dispatch( + decodeFn: () => Promise, + reqFn: (msg: Q, ctx: O) => Promise> = async (req) => ({ req }) + ): Promise> { + const rReqMsg = await exception2Result(async () => (await decodeFn()) as Q); + if (rReqMsg.isErr()) { + const errMsg = buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, rReqMsg.Err()); + return { + req: errMsg as unknown as Q, + res: errMsg, + ctx: this.ctx, + }; + } + const { req, ctx: optCtx } = await reqFn(rReqMsg.Ok() as Q, this.ctx); + const ctx = { ...(optCtx || this.ctx) }; + switch (true) { + case MsgIsReqGestalt(req): + return { + req, + res: buildResGestalt(req, defaultGestalt(this.serverId, true)) as S | ErrorMsg, + ctx, + }; + + case MsgIsReqGetData(req): + case MsgIsReqGetWAL(req): + return { + req, + res: (await this.signedUrl( + { + ...req, + params: { + ...req.params, + method: "GET", + store: getStoreFromType(req).store, + }, + }, + ctx + )) as S | ErrorMsg, + ctx, + }; + + case MsgIsReqPutData(req): + case MsgIsReqPutWAL(req): + if (req.payload) { + return { + req, + res: buildErrorMsg(this.logger, req, new Error("inband payload not implemented")) as S | ErrorMsg, + ctx, + }; + } + return { + req, + res: (await this.signedUrl( + { + ...req, + params: { + ...req.params, + method: "PUT", + store: getStoreFromType(req).store, + }, + }, + ctx + )) as S | ErrorMsg, + ctx, + }; + + case MsgIsReqDelData(req): + case MsgIsReqDelWAL(req): + return { + req, + res: (await this.signedUrl( + { + ...req, + params: { + ...req.params, + method: "DELETE", + store: getStoreFromType(req).store, + }, + }, + ctx + )) as S | ErrorMsg, + ctx, + }; + + // case MsgIsReqSignedUrl(req): + // return { + // req, + // res: (await this.signedUrl(req, ctx)) as S | ErrorMsg, + // ctx, + // }; + case MsgIsReqSubscribeMeta(req): + return { + req, + res: (await this.subscribeMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + case MsgIsReqPutMeta(req): + return { + req, + res: (await this.putMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + case MsgIsReqGetMeta(req): + return { + req, + res: (await this.getMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + case MsgIsReqDelMeta(req): + return { + req, + res: (await this.delMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + } + return { + req: req, + res: buildErrorMsg(this.logger, req, new Error(`unknown msg.type=${req.type}`)) as S | ErrorMsg, + ctx, + }; + } + + async delMeta(req: ReqDelMeta, ctx: CFCtxWithGroup): Promise { + // delete meta does nothing in this implementation + // if you delete meta basically you are deleting the whole ledger + return buildResDelMeta(req, { + params: req.params, + status: "unsupported", + connId: ctx.group.connId, + }); + } + + async getMeta(req: ReqGetMeta, ctx: CF): Promise { + const rSignedUrl = await calculatePreSignedUrl( + { + tid: req.tid, + type: "reqSignedUrl", + version: req.version, + params: { ...req.params, method: "GET" }, + }, + ctx.env + ); + if (rSignedUrl.isErr()) { + return buildErrorMsg(this.logger, req, rSignedUrl.Err()); + } + return buildResGetMeta(req, { + signedGetUrl: rSignedUrl.Ok().toString(), + status: "found", + metas: [], + connId: "", + }); + } + + async putMeta(req: ReqPutMeta, ctx: CtxHasGroup): Promise { + const rSignedUrl = await calculatePreSignedUrl( + { + tid: req.tid, + type: "reqSignedUrl", + version: req.version, + params: { ...req.params, method: "PUT" }, + }, + ctx.env + ); + if (rSignedUrl.isErr()) { + return buildErrorMsg(this.logger, req, rSignedUrl.Err()); + } + // roughly time ordered + return buildResPutMeta(req, { + // metaId should be a hash of metas. + metaId: new Date().getTime().toString(), + metas: req.metas, + signedPutUrl: rSignedUrl.Ok().toString(), + connId: ctx.group.connId, + }); + } +} diff --git a/src/v2-cloud/msg-raw-connection-base.ts b/src/v2-cloud/msg-raw-connection-base.ts new file mode 100644 index 00000000..66bf75e1 --- /dev/null +++ b/src/v2-cloud/msg-raw-connection-base.ts @@ -0,0 +1,31 @@ +import { Logger } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { MsgBase, ErrorMsg, buildErrorMsg } from "./msg-types.js"; +import { ExchangedGestalt, OnErrorFn, UnReg } from "./msger.js"; + +export class MsgRawConnectionBase { + readonly sthis: SuperThis; + readonly exchangedGestalt: ExchangedGestalt; + + constructor(sthis: SuperThis, exGestalt: ExchangedGestalt) { + this.sthis = sthis; + this.exchangedGestalt = exGestalt; + } + + readonly onErrorFns = new Map(); + onError(fn: OnErrorFn): UnReg { + const key = this.sthis.nextId().str; + this.onErrorFns.set(key, fn); + return () => this.onErrorFns.delete(key); + } + + buildErrorMsg(logger: Logger, msg: Partial, err: Error): ErrorMsg { + // const logLine = this.sthis.logger.Error().Err(err).Any("msg", msg); + const rmsg = Array.from(this.onErrorFns.values()).reduce((msg, fn) => { + return fn(msg, err); + }, msg); + const emsg = buildErrorMsg(this.sthis, logger, rmsg, err); + logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); + return emsg; + } +} diff --git a/src/v2-cloud/msg-request.ts b/src/v2-cloud/msg-request.ts new file mode 100644 index 00000000..51facafe --- /dev/null +++ b/src/v2-cloud/msg-request.ts @@ -0,0 +1,220 @@ +// import { Future, Logger, exception2Result, Result, CoerceURI, KeyedResolvOnce, URI } from "@adviser/cement"; +// import { SuperThis, } from "@fireproof/core"; +// import { RequestOpts, } from "./msg-processor.js"; +// import { MsgBase, AuthType, Gestalt, defaultGestalt, ResGestalt, ReqGestalt, MsgIsResGestalt, MsgIsError, } from "./msg-types.js"; + +// import * as json from 'multiformats/codecs/json'; +// import * as cborg from '@fireproof/vendor/cborg'; +// import { MsgConnection } from "./msger.js"; + +// export interface EnDeCoder { +// encode(node: T): Uint8Array; +// decode(data: Uint8Array): T; +// } + +// export interface WaitForTid { +// readonly tid: string; +// readonly future: Future; +// // undefined match all +// readonly waitFor: (msg: MsgBase) => boolean; +// } + +// export interface FetchGestaltParams { +// readonly auth?: AuthType; +// readonly sthis: SuperThis; +// readonly gestaltURL: URI; +// readonly uniqServerId?: string; +// readonly getConn: () => Promise; +// } + +// export interface HttpConnectionParams { +// readonly gestaltURL: CoerceURI; +// readonly fetchConnection?: MsgConnection; +// readonly ende?: EnDeCoder; +// readonly uniqServerId?: string; +// } + +// export type RequestFN = (req: Q, opts: RequestOpts) => Promise> + +// const serverId = "FP-Universal-Client" + +// export function encoded(logger: Logger, g: "JSON" | "CBOR") { +// let ende: EnDeCoder +// let mime: string +// switch (g) { +// case "JSON": +// ende = json +// mime = "application/json" +// break; +// case "CBOR": +// ende = cborg +// mime = "application/cbor" +// break; +// default: +// throw logger.Error().Str("typ", g).Msg(`Unknown encoding: ${g}`).AsError() +// } +// return { ende, mime } +// } + +// const getGestalts = new KeyedResolvOnce(); + +// async function fetchGestalt(fgp: FetchGestaltParams): Promise { +// return getGestalts.get(fgp.gestaltURL.toString()).once(async () => { +// const conn = await fgp.getConn(); +// const rGestalt = await conn.request({ +// type: "reqGestalt", +// tid: fgp.sthis.nextId().str, +// version: serverId, +// gestalt: defaultGestalt({ id: fgp.uniqServerId || serverId }), +// }, { waitFor: MsgIsResGestalt }); +// if (MsgIsError(rGestalt)) { +// throw Error(rGestalt.message) +// } +// const gestalt = rGestalt.gestalt +// const ende = encoded(fgp.sthis.logger, gestalt.encodings[0]) +// return { +// ende: ende.ende, +// mime: ende.mime, +// auth: gestalt.auth, +// gestalt: gestalt +// } +// }) +// } + +// export function selectRandom(arr: T[]): T { +// return arr[Math.floor(Math.random() * arr.length)]; +// } + +// export interface MsgErrorClose { +// readonly msgFn: (msg: MessageEvent) => void; +// readonly errFn: (err: Event) => void; +// readonly closeFn: () => void; +// readonly openFn: () => void; +// } + +// export interface GestaltParams { +// readonly auth?: AuthType; +// readonly sthis: SuperThis; +// readonly gestaltURL: URI; +// readonly uniqServerId?: string; +// } + +// const keyedHttpConnection = new KeyedResolvOnce(); +// function httpFactory(sthis: SuperThis, uniqServerId: string, auth?: AuthType): (() => Promise) { +// return () => keyedHttpConnection.get(uniqServerId || serverId).once(async () => { +// return new HttpConnection(sthis, { +// ende: json, +// mime: "application/json", +// auth: auth, +// params: defaultGestalt(uniqServerId || serverId, false), +// }) +// }) +// } + +// const keyedWSConnection = new KeyedResolvOnce(); + +// export interface Attachable { +// attach(t: T): Promise +// } + +// export class WSAttachable implements Attachable { +// readonly gestalt: MsgerParams +// readonly sthis: SuperThis +// readonly waitForTid = new Map(); +// constructor(sthis: SuperThis, gestalt: MsgerParams) { +// this.gestalt = gestalt +// this.sthis = sthis +// } +// attach(t: WebSocket): Promise { +// return keyedWSConnection.get(this.gestalt.params.id).once(async () => { +// const c = new WSAttachConnection(this.sthis, t, this.waitForTid, { +// openFn: () => this.open(t), +// errFn: (err) => this.error(t, err), +// msgFn: (msg) => this.msg(t, msg), +// closeFn: () => this.close(t) +// }) +// return c +// }) +// } + +// open(ws: WebSocket) { +// this.sthis.logger.Info().Msg("open") +// } + +// error(ws: WebSocket, err: Event) { +// this.sthis.logger.Error().Msg("error") + +// } +// msg(ws: WebSocket, msg: MessageEvent) { +// this.sthis.logger.Info().Any("msg", msg).Msg("msg") +// ws.onmessage = async (event) => { +// const rMsg = await exception2Result(() => JSON.parse(event.data) as MsgBase); +// if (rMsg.isErr()) { +// this.logger.Error().Err(rMsg).Any(event.data).Msg("Invalid message"); +// return; +// } +// const msg = rMsg.Ok(); +// const waitFor = this.waitForTid.get(msg.tid); +// if (waitFor) { +// if (MsgIsError(msg)) { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// this.waitForTid.delete(msg.tid); +// waitFor.future.resolve(msg); +// } else if (waitFor.type) { +// // what for a specific type +// if (waitFor.type === msg.type) { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// this.waitForTid.delete(msg.tid); +// waitFor.future.resolve(msg); +// } else { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// } +// } else { +// // wild-card +// this.msgCallbacks.forEach((cb) => cb(msg)); +// this.waitForTid.delete(msg.tid); +// waitFor.future.resolve(msg); +// } +// } else { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// } +// }; +// } + +// close(ws: WebSocket) { +// this.sthis.logger.Info().Msg("close") +// } + +// // this.params = params; + +// } + +// export async function getAttachable(p: FetchGestaltParams): Promise> { +// const g = await fetchGestalt({ +// gestaltURL: p.gestaltURL, +// sthis: p.sthis, +// getConn: httpFactory(p.sthis, p.uniqServerId || serverId, p.auth), +// }) +// if (g.params.wsEndpoints.length > 0) { +// return new WSAttachable(p.sthis, g) as Attachable +// } +// return { +// attach: async () => new HttpConnection(p.sthis, g) +// } +// } + +// export class ConnectionImpl implements Connection { +// readonly sthis: SuperThis; +// constructor(sthis: SuperThis) { +// this.sthis = sthis; +// } + +// async request(req: Q, opts: RequestOpts): Promise> { + +// } + +// } + +// export class ConnectionImpl implements Connection { + +// } diff --git a/src/v2-cloud/msg-type-meta.ts b/src/v2-cloud/msg-type-meta.ts new file mode 100644 index 00000000..80c2201d --- /dev/null +++ b/src/v2-cloud/msg-type-meta.ts @@ -0,0 +1,160 @@ +import { Logger, VERSION } from "@adviser/cement"; +import { CRDTEntry } from "@fireproof/core"; +import { + GwCtx, + MsgBase, + MsgWithConn, + MsgWithOptionalConn, + MsgWithTenantLedger, + NextId, + ReqSignedUrlParam, + ResOptionalSignedUrl, +} from "./msg-types.js"; + +/* Put Meta */ +export interface ReqPutMeta extends MsgWithTenantLedger { + readonly type: "reqPutMeta"; + readonly params: ReqSignedUrlParam; + readonly metas: CRDTEntry[]; +} + +export interface ResPutMeta extends MsgWithTenantLedger, QSMeta { + readonly type: "resPutMeta"; +} + +export function buildReqPutMeta( + sthis: NextId, + signedUrlParams: ReqSignedUrlParam, + metas: CRDTEntry[], + gwCtx: GwCtx +): ReqPutMeta { + return { + tid: sthis.nextId().str, + type: "reqPutMeta", + ...gwCtx, + version: VERSION, + params: signedUrlParams, + metas, + }; +} + +export function MsgIsReqPutMeta(msg: MsgBase): msg is ReqPutMeta { + return msg.type === "reqPutMeta"; +} + +export function buildResPutMeta( + _sthis: NextId, + _logger: Logger, + req: MsgWithTenantLedger>, + meta: QSMeta +): ResPutMeta { + return { + ...meta, + tid: req.tid, + conn: req.conn, + tenant: req.tenant, + type: "resPutMeta", + // key: req.key, + version: VERSION, + }; +} + +export function MsgIsResPutMeta(qs: MsgBase): qs is ResPutMeta { + return qs.type === "resPutMeta"; +} + +/* Bind Meta */ +export interface BindGetMeta extends MsgWithTenantLedger { + readonly type: "bindGetMeta"; + readonly params: ReqSignedUrlParam; +} + +export function MsgIsBindGetMeta(msg: MsgBase): msg is BindGetMeta { + return msg.type === "bindGetMeta"; +} + +export interface QSMeta extends ResOptionalSignedUrl { + readonly metas: CRDTEntry[]; + readonly keys?: string[]; +} + +export interface EventGetMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { + readonly type: "eventGetMeta"; +} + +export function buildBindGetMeta(sthis: NextId, params: ReqSignedUrlParam, gwCtx: GwCtx): BindGetMeta { + return { + tid: sthis.nextId().str, + ...gwCtx, + type: "bindGetMeta", + version: VERSION, + params, + }; +} + +export function buildEventGetMeta( + _sthis: NextId, + _logger: Logger, + req: MsgWithTenantLedger>, + metaParam: QSMeta, + gwCtx: GwCtx +): EventGetMeta { + return { + ...metaParam, + ...gwCtx, + tid: req.tid, + type: "eventGetMeta", + params: { ...req.params, method: "GET", store: "meta" }, + version: VERSION, + }; +} + +export function MsgIsEventGetMeta(qs: MsgBase): qs is EventGetMeta { + return qs.type === "eventGetMeta"; +} + +/* Del Meta */ +export interface ReqDelMeta extends MsgWithTenantLedger { + readonly type: "reqDelMeta"; + readonly params: ReqSignedUrlParam; +} + +export function buildReqDelMeta(sthis: NextId, signedUrlParams: ReqSignedUrlParam, gwCtx: GwCtx): ReqDelMeta { + return { + tid: sthis.nextId().str, + ...gwCtx, + type: "reqDelMeta", + version: VERSION, + params: signedUrlParams, + }; +} + +export function MsgIsReqDelMeta(msg: MsgBase): msg is ReqDelMeta { + return msg.type === "reqDelMeta"; +} + +export interface ResDelMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { + readonly type: "resDelMeta"; +} + +export function buildResDelMeta( + _sthis: NextId, + _logger: Logger, + req: MsgWithTenantLedger>, + signedUrl?: string +): ResDelMeta { + return { + params: { ...req.params, method: "DELETE", store: "meta" }, + signedUrl, + tid: req.tid, + conn: req.conn, + tenant: req.tenant, + type: "resDelMeta", + // key: req.key, + version: VERSION, + }; +} + +export function MsgIsResDelMeta(qs: MsgBase): qs is ResDelMeta { + return qs.type === "resDelMeta"; +} diff --git a/src/v2-cloud/msg-types-data.ts b/src/v2-cloud/msg-types-data.ts new file mode 100644 index 00000000..12ceeb77 --- /dev/null +++ b/src/v2-cloud/msg-types-data.ts @@ -0,0 +1,109 @@ +import { Logger, Result, URI } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { + ReqSignedUrl, + NextId, + MsgBase, + ResSignedUrl, + MsgWithError, + buildRes, + ReqSignedUrlParam, + buildReqSignedUrl, + GwCtx, + MsgIsTenantLedger, + MsgWithConn, +} from "./msg-types.js"; +import { PreSignedMsg } from "./pre-signed-url.js"; + +export interface ReqGetData extends ReqSignedUrl { + readonly type: "reqGetData"; +} + +export function buildReqGetData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqGetData { + return buildReqSignedUrl(sthis, "reqGetData", sup, ctx); +} + +export function MsgIsReqGetData(msg: MsgBase): msg is ReqGetData { + return msg.type === "reqGetData"; +} + +export interface ResGetData extends ResSignedUrl { + readonly type: "resGetData"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsResGetData(msg: MsgBase): msg is ResGetData { + return msg.type === "resGetData" && MsgIsTenantLedger(msg); +} + +export interface CalculatePreSignedUrl { + calculatePreSignedUrl(p: PreSignedMsg): Promise>; +} + +export function buildResGetData( + sthis: SuperThis, + logger: Logger, + req: MsgWithConn, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes, ResGetData>("GET", "data", "resGetData", sthis, logger, req, ctx); +} + +export interface ReqPutData extends ReqSignedUrl { + readonly type: "reqPutData"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsReqPutData(msg: MsgBase): msg is ReqPutData { + return msg.type === "reqPutData"; +} + +export function buildReqPutData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqPutData { + return buildReqSignedUrl(sthis, "reqPutData", sup, ctx); +} + +export interface ResPutData extends ResSignedUrl { + readonly type: "resPutData"; +} + +export function MsgIsResPutData(msg: MsgBase): msg is ResPutData { + return msg.type === "resPutData"; +} + +export function buildResPutData( + sthis: SuperThis, + logger: Logger, + req: MsgWithConn, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes, ResPutData>("PUT", "data", "resPutData", sthis, logger, req, ctx); +} + +export interface ReqDelData extends ReqSignedUrl { + readonly type: "reqDelData"; +} + +export function MsgIsReqDelData(msg: MsgBase): msg is ReqDelData { + return msg.type === "reqDelData"; +} + +export function buildReqDelData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqDelData { + return buildReqSignedUrl(sthis, "reqDelData", sup, ctx); +} + +export interface ResDelData extends ResSignedUrl { + readonly type: "resDelData"; +} + +export function MsgIsResDelData(msg: MsgBase): msg is ResDelData { + return msg.type === "resDelData"; +} + +export function buildResDelData( + sthis: SuperThis, + logger: Logger, + req: MsgWithConn, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes, ResDelData>("DELETE", "data", "resDelData", sthis, logger, req, ctx); +} diff --git a/src/v2-cloud/msg-types-wal.ts b/src/v2-cloud/msg-types-wal.ts new file mode 100644 index 00000000..3363cd43 --- /dev/null +++ b/src/v2-cloud/msg-types-wal.ts @@ -0,0 +1,130 @@ +import { Logger } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { + MsgBase, + MsgWithError, + buildRes, + NextId, + ReqSignedUrl, + ResSignedUrl, + ReqSignedUrlParam, + buildReqSignedUrl, + GwCtx, + MsgIsTenantLedger, + MsgWithTenantLedger, + MsgWithConn, +} from "./msg-types.js"; +import { CalculatePreSignedUrl } from "./msg-types-data.js"; + +export interface ReqGetWAL extends ReqSignedUrl { + readonly type: "reqGetWAL"; +} + +export function MsgIsReqGetWAL(msg: MsgBase): msg is ReqGetWAL { + return msg.type === "reqGetWAL"; +} + +export function buildReqGetWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqGetWAL { + return buildReqSignedUrl(sthis, "reqGetWAL", sup, ctx); +} + +export interface ResGetWAL extends ResSignedUrl { + readonly type: "resGetWAL"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsResGetWAL(msg: MsgBase): msg is ResGetWAL { + return msg.type === "resGetWAL"; +} + +export function buildResGetWAL( + sthis: SuperThis, + logger: Logger, + req: MsgWithTenantLedger>, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes>, ResGetWAL>( + "GET", + "wal", + "resGetWAL", + sthis, + logger, + req, + ctx + ); +} + +export interface ReqPutWAL extends Omit { + readonly type: "reqPutWAL"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsReqPutWAL(msg: MsgBase): msg is ReqPutWAL { + return msg.type === "reqPutWAL"; +} + +export function buildReqPutWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqPutWAL { + return buildReqSignedUrl(sthis, "reqPutWAL", sup, ctx); +} + +export interface ResPutWAL extends Omit { + readonly type: "resPutWAL"; +} + +export function MsgIsResPutWAL(msg: MsgBase): msg is ResPutWAL { + return msg.type === "resPutWAL"; +} + +export function buildResPutWAL( + sthis: SuperThis, + logger: Logger, + req: MsgWithTenantLedger>, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes>, ResPutWAL>( + "PUT", + "wal", + "resPutWAL", + sthis, + logger, + req, + ctx + ); +} + +export interface ReqDelWAL extends Omit { + readonly type: "reqDelWAL"; +} + +export function MsgIsReqDelWAL(msg: MsgBase): msg is ReqDelWAL { + return msg.type === "reqDelWAL"; +} + +export function buildReqDelWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqDelWAL { + return buildReqSignedUrl(sthis, "reqDelWAL", sup, ctx); +} + +export interface ResDelWAL extends Omit { + readonly type: "resDelWAL"; +} + +export function MsgIsResDelWAL(msg: MsgBase): msg is ResDelWAL { + return msg.type === "resDelWAL" && MsgIsTenantLedger(msg); +} + +export function buildResDelWAL( + sthis: SuperThis, + logger: Logger, + req: MsgWithTenantLedger>, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes>, ResDelWAL>( + "DELETE", + "wal", + "resDelWAL", + sthis, + logger, + req, + ctx + ); +} diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts new file mode 100644 index 00000000..43664e83 --- /dev/null +++ b/src/v2-cloud/msg-types.ts @@ -0,0 +1,567 @@ +import { Future } from "@adviser/cement"; +import { Logger, SuperThis } from "@fireproof/core"; +import { CalculatePreSignedUrl } from "./msg-types-data.js"; +import { PreSignedMsg } from "./pre-signed-url.js"; + +export const VERSION = "FP-MSG-1.0"; + +export type MsgWithError = T | ErrorMsg; + +export interface RequestOpts { + readonly waitFor: (msg: MsgBase) => boolean; + readonly pollInterval?: number; // 1000ms + readonly timeout?: number; // ms +} + +export interface EnDeCoder { + encode(node: T): Uint8Array; + decode(data: Uint8Array): T; +} + +export interface WaitForTid { + readonly tid: string; + readonly future: Future; + readonly timeout?: number; + // undefined match all + readonly waitFor: (msg: MsgBase) => boolean; +} + +// export interface ConnId { +// readonly connId: string; +// } +// type AddConnId = Omit & ConnId & { readonly type: N }; +export interface NextId { + readonly nextId: SuperThis["nextId"]; +} + +export interface AuthType { + readonly type: "ucan"; +} + +export interface UCanAuth { + readonly type: "ucan"; + readonly params: { + readonly tbd: string; + }; +} + +export interface TenantLedger { + readonly tenant: string; + readonly ledger: string; +} + +export function keyTenantLedger(t: TenantLedger): string { + return `${t.tenant}:${t.ledger}`; +} + +export interface QSId { + readonly reqId: string; + readonly resId: string; +} + +// export interface Connection extends ReqResId{ +// readonly key: TenantLedger; +// } + +// export interface Connected { +// readonly conn: Connection; +// } + +export interface MsgBase { + readonly tid: string; + readonly type: string; + readonly version: string; + readonly auth?: AuthType; +} + +export function MsgIsTid(msg: MsgBase, tid: string): boolean { + return msg.tid === tid; +} + +export type MsgWithConn = T & { readonly conn: QSId }; + +export type MsgWithOptionalConn = T & { readonly conn?: QSId }; + +export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; + +export interface ErrorMsg extends MsgBase { + readonly type: "error"; + readonly src: unknown; + readonly message: string; + readonly body?: string; + readonly stack?: string[]; +} + +export function MsgIsError(rq: MsgBase): rq is ErrorMsg { + return rq.type === "error"; +} + +export function MsgIsQSError(rq: ReqRes): rq is ReqRes { + return rq.res.type === "error" || rq.req.type === "error"; +} + +export type HttpMethods = "GET" | "PUT" | "DELETE"; +export type FPStoreTypes = "meta" | "data" | "wal"; + +// reqRes is http +// stream is WebSocket +export type ProtocolCapabilities = "reqRes" | "stream"; + +export interface Gestalt { + /** + * Describes StoreTypes which are handled + */ + readonly storeTypes: FPStoreTypes[]; + /** + * A unique identifier + */ + readonly id: string; + /** + * protocol capabilities + * defaults "stream" + */ + readonly protocolCapabilities: ProtocolCapabilities[]; + /** + * HttpEndpoints (URL) required atleast one + * could be absolute or relative + */ + readonly httpEndpoints: string[]; + /** + * WebsocketEndpoints (URL) required atleast one + * could be absolute or relative + */ + readonly wsEndpoints: string[]; + /** + * Encodings supported + * JSON, CBOR + */ + readonly encodings: ("JSON" | "CBOR")[]; + /** + * Authentication methods supported + */ + readonly auth: AuthType[]; + /** + * Requires Authentication + */ + readonly requiresAuth: boolean; + /** + * In|Outband Data | Meta | WAL Support + * Inband Means that the Payload is part of the message + * Outband Means that the Payload is PUT/GET to a different URL + * A Clien implementation usally not support reading or writing + * support + */ + readonly data?: { + readonly inband: boolean; + readonly outband: boolean; + }; + readonly meta?: { + readonly inband: true; // meta inband is mandatory + readonly outband: boolean; + }; + readonly wal?: { + readonly inband: boolean; + readonly outband: boolean; + }; + /** + * Request Types supported + * reqGestalt, reqSubscribeMeta, reqPutMeta, reqGetMeta, reqDelMeta, reqUpdateMeta + */ + readonly reqTypes: string[]; + /** + * Response Types supported + * resGestalt, resSubscribeMeta, resPutMeta, resGetMeta, resDelMeta, updateMeta + */ + readonly resTypes: string[]; + /** + * Event Types supported + * updateMeta + */ + readonly eventTypes: string[]; +} + +export interface MsgerParams { + readonly mime: string; + readonly auth?: AuthType; + readonly hasPersistent?: boolean; + readonly protocolCapabilities?: ProtocolCapabilities[]; + // readonly protocol: "http" | "ws"; + readonly timeout: number; // msec +} + +// force the server id +export type GestaltParam = Partial & { readonly id: string }; + +export function defaultGestalt(msgP: MsgerParams, gestalt: GestaltParam): Gestalt { + return { + storeTypes: ["meta", "data", "wal"], + httpEndpoints: ["/fp"], + wsEndpoints: ["/ws"], + encodings: ["JSON"], + protocolCapabilities: msgP.protocolCapabilities || ["reqRes", "stream"], + auth: [], + requiresAuth: false, + data: msgP.hasPersistent + ? { + inband: true, + outband: true, + } + : undefined, + meta: msgP.hasPersistent + ? { + inband: true, + outband: true, + } + : undefined, + wal: msgP.hasPersistent + ? { + inband: true, + outband: true, + } + : undefined, + reqTypes: [ + "reqOpen", + "reqGestalt", + // "reqSignedUrl", + "reqSubscribeMeta", + "reqPutMeta", + "reqGetMeta", + "reqDelMeta", + "reqPutData", + "reqGetData", + "reqDelData", + "reqPutWAL", + "reqGetWAL", + "reqDelWAL", + "reqUpdateMeta", + ], + resTypes: [ + "resOpen", + "resGestalt", + // "resSignedUrl", + "resSubscribeMeta", + "resPutMeta", + "resGetMeta", + "resDelMeta", + "resPutData", + "resGetData", + "resDelData", + "resPutWAL", + "resGetWAL", + "resDelWAL", + "updateMeta", + ], + eventTypes: ["updateMeta"], + ...gestalt, + }; +} + +/** + * The ReqGestalt message is used to request the + * features of the Responder. + */ +export interface ReqGestalt extends MsgBase { + readonly type: "reqGestalt"; + readonly gestalt: Gestalt; +} + +export function MsgIsReqGestalt(msg: MsgBase): msg is ReqGestalt { + return msg.type === "reqGestalt"; +} + +export function buildReqGestalt(sthis: NextId, gestalt: Gestalt): ReqGestalt { + return { + tid: sthis.nextId().str, + type: "reqGestalt", + version: VERSION, + gestalt, + }; +} + +/** + * The ResGestalt message is used to respond with + * the features of the Responder. + */ +export interface ResGestalt extends MsgBase { + readonly type: "resGestalt"; + readonly gestalt: Gestalt; +} + +export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt): ResGestalt | ErrorMsg { + return { + tid: req.tid, + type: "resGestalt", + version: VERSION, + gestalt, + }; +} + +export function MsgIsResGestalt(msg: MsgBase): msg is ResGestalt { + return msg.type === "resGestalt"; +} + +export interface ReqOpenConnection { + // readonly key: TenantLedger; + readonly reqId?: string; + readonly resId?: string; // for double open +} + +export interface ReqOpenConn { + readonly reqId: string; + readonly resId?: string; +} + +export interface ReqOpen extends MsgBase { + readonly type: "reqOpen"; + readonly conn: ReqOpenConn; +} + +export function buildReqOpen(sthis: NextId, conn: ReqOpenConnection): ReqOpen { + return { + tid: sthis.nextId().str, + type: "reqOpen", + version: VERSION, + conn: { + ...conn, + reqId: conn.reqId || sthis.nextId().str, + }, + }; +} + +export function MsgIsReqOpenWithConn(imsg: MsgBase): imsg is MsgWithConn { + const msg = imsg as MsgWithConn; + return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; +} + +export function MsgIsReqOpen(imsg: MsgBase): imsg is MsgWithConn { + const msg = imsg as MsgWithConn; + return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; +} + +export interface ResOpen extends MsgBase { + readonly type: "resOpen"; + readonly conn: QSId; +} + +export function MsgIsWithConn(msg: T): msg is MsgWithConn { + const mwc = (msg as MsgWithConn).conn; + return mwc && !!(mwc as QSId).reqId && !!(mwc as QSId).resId; +} + +export function MsgIsConnected(msg: T, qsid: QSId): msg is MsgWithConn { + return MsgIsWithConn(msg) && msg.conn.reqId === qsid.reqId && msg.conn.resId === qsid.resId; +} + +export function buildResOpen(sthis: NextId, req: ReqOpen, resStreamId?: string): ResOpen { + if (!(req.conn && req.conn.reqId)) { + throw new Error("req.conn.reqId is required"); + } + return { + ...req, + type: "resOpen", + conn: { + ...req.conn, + resId: req.conn.resId || resStreamId || sthis.nextId().str, + }, + }; +} + +export function MsgIsResOpen(msg: MsgBase): msg is ResOpen { + return msg.type === "resOpen"; +} + +export interface ReqClose extends Omit { + readonly type: "reqClose"; +} + +export function MsgIsReqClose(msg: MsgBase): msg is ReqClose { + return msg.type === "reqClose" && MsgIsWithConn(msg); +} + +export interface ResClose extends Omit { + readonly type: "resClose"; +} + +export function MsgIsResClose(msg: MsgBase): msg is ResClose { + return msg.type === "resClose" && MsgIsWithConn(msg); +} + +export interface SignedUrlParam { + readonly method: HttpMethods; + readonly store: FPStoreTypes; + // base path + readonly path?: string; + // name of the file + readonly key: string; + readonly expires?: number; // seconds + readonly index?: string; +} + +export type ReqSignedUrlParam = Omit; + +export interface UpdateReqRes { + req: Q; + res: S; +} + +export type ReqRes = Readonly>; + +// export interface ReqOptRes { +// readonly req: Q; +// readonly res?: S; +// } + +// /* Signed URL */ +// export function buildReqSignedUrl(req: ReqSignedUrlParam): ReqSignedUrlParam { +// return { +// tid: req.tid, +// params: { +// // protocol: "wss", +// ...req.params, +// }, +// }; +// } + +// export function MsgIsReqSignedUrl(msg: MsgBase): msg is ReqSignedUrl { +// return msg.type === "reqSignedUrl"; +// } + +// interface StoreAndType { +// readonly store: FPStoreTypes; +// readonly resType: string; +// } +// const reqToRes: Record = { +// reqGetData: { store: "data", resType: "resGetData" }, +// reqPutData: { store: "data", resType: "resPutData" }, +// reqDelData: { store: "data", resType: "resDelData" }, +// reqGetWAL: { store: "wal", resType: "resGetWAL" }, +// reqPutWAL: { store: "wal", resType: "resPutWAL" }, +// reqDelWAL: { store: "wal", resType: "resDelWAL" }, +// }; + +// export function getStoreFromType(req: MsgBase): StoreAndType { +// return ( +// reqToRes[req.type] || +// (() => { +// throw new Error(`unknown req.type=${req.type}`); +// })() +// ); +// } + +// export function buildResSignedUrl(req: ReqSignedUrl, signedUrl: string): ResSignedUrl { +// return { +// tid: req.tid, +// type: getStoreFromType(req).resType, +// version: VERSION, +// params: req.params, +// signedUrl, +// }; +// } + +export function buildErrorMsg( + sthis: SuperThis, + logger: Logger, + base: Partial, + error: Error, + body?: string, + stack?: string[] +): ErrorMsg { + if (!stack && sthis.env.get("FP_STACK")) { + stack = error.stack?.split("\n"); + } + const msg = { + src: base, + type: "error", + tid: base.tid || "internal", + message: error.message, + version: VERSION, + body, + stack, + } satisfies ErrorMsg; + logger.Any("ErrorMsg", msg); + return msg; +} + +// export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; + +export function MsgIsTenantLedger(msg: T): msg is MsgWithTenantLedger { + const t = (msg as MsgWithTenantLedger).tenant; + return !!t && !!t.tenant && !!t.ledger; +} + +export interface ReqSignedUrl extends MsgWithTenantLedger { + // readonly type: "reqSignedUrl"; + readonly params: ReqSignedUrlParam; +} + +export interface GwCtx { + readonly tid?: string; + readonly conn?: QSId; + readonly tenant: TenantLedger; +} + +export interface GwCtxConn { + readonly tid?: string; + readonly conn: QSId; + readonly tenant: TenantLedger; +} + +export function buildReqSignedUrl( + sthis: NextId, + type: string, + params: ReqSignedUrlParam, + gwCtx: GwCtx +): T { + return { + tid: sthis.nextId().str, + type, + version: VERSION, + ...gwCtx, + params, + } as T; +} + +export interface ResSignedUrl extends MsgWithTenantLedger { + // readonly type: "resSignedUrl"; + readonly params: SignedUrlParam; + readonly signedUrl: string; +} + +export interface ResOptionalSignedUrl extends MsgWithTenantLedger { + // readonly type: "resSignedUrl"; + readonly params: SignedUrlParam; + readonly signedUrl?: string; +} + +export async function buildRes>, S extends ResSignedUrl>( + method: SignedUrlParam["method"], + store: FPStoreTypes, + type: string, + sthis: SuperThis, + logger: Logger, + req: Q, + ctx: CalculatePreSignedUrl +): Promise> { + const psm = { + type: "reqSignedUrl", + version: req.version, + params: { + ...req.params, + method, + store, + }, + conn: req.conn, + tenant: req.tenant, + tid: req.tid, + } satisfies PreSignedMsg; + const rSignedUrl = await ctx.calculatePreSignedUrl(psm); + if (rSignedUrl.isErr()) { + return buildErrorMsg(sthis, logger, req, rSignedUrl.Err()); + } + return { + ...req, + params: psm.params, + type, + signedUrl: rSignedUrl.Ok().toString(), + } as unknown as MsgWithError; +} diff --git a/src/v2-cloud/msger.ts b/src/v2-cloud/msger.ts new file mode 100644 index 00000000..d6ea4da2 --- /dev/null +++ b/src/v2-cloud/msger.ts @@ -0,0 +1,274 @@ +import { BuildURI, CoerceURI, Result, runtimeFn, URI } from "@adviser/cement"; +import { + buildReqGestalt, + defaultGestalt, + EnDeCoder, + Gestalt, + MsgBase, + MsgerParams, + MsgIsResGestalt, + RequestOpts, + ResGestalt, + MsgWithError, + MsgWithConn, + buildReqOpen, + MsgIsConnected, + MsgIsError, + MsgIsResOpen, + MsgWithOptionalConn, + QSId, + MsgIsTid, + ReqGestalt, +} from "./msg-types.js"; +import { SuperThis } from "@fireproof/core"; +import { HttpConnection } from "./http-connection.js"; +import { WSConnection } from "./ws-connection.js"; + +// const headers = { +// "Content-Type": "application/json", +// "Accept": "application/json", +// }; + +export function selectRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +export function timeout(ms: number, promise: Promise): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`TIMEOUT after ${ms}ms`)); + }, ms); + promise + .then(resolve) + .catch(reject) + .finally(() => clearTimeout(timer)); + }); +} + +export type OnMsgFn = (msg: MsgWithError) => void; +export type UnReg = () => void; + +export interface ExchangedGestalt { + readonly my: Gestalt; + readonly remote: Gestalt; +} + +export type OnErrorFn = (msg: Partial, err: Error) => Partial; + +export interface ActiveStream { + readonly id: string; + readonly bind: { + readonly msg: Q; + readonly opts: RequestOpts; + }; + timeout?: unknown; + controller?: ReadableStreamDefaultController>; +} + +export interface MsgRawConnection { + // readonly ws: WebSocket; + // readonly params: ConnectionKey; + // qsOpen: ReqRes; + readonly sthis: SuperThis; + readonly exchangedGestalt: ExchangedGestalt; + readonly activeBinds: Map>; + bind(req: Q, opts: RequestOpts): ReadableStream>; + request(req: Q, opts: RequestOpts): Promise>; + start(): Promise>; + close(): Promise>; + onMsg(msg: OnMsgFn): UnReg; +} + +export function jsonEnDe(sthis: SuperThis): EnDeCoder { + return { + encode: (node: unknown) => sthis.txt.encode(JSON.stringify(node)), + decode: (data: Uint8Array) => JSON.parse(sthis.txt.decode(data)), + }; +} + +export type MsgerParamsWithEnDe = MsgerParams & { readonly ende: EnDeCoder }; + +export function defaultMsgParams(sthis: SuperThis, igs: Partial): MsgerParamsWithEnDe { + return { + mime: "application/json", + ende: jsonEnDe(sthis), + timeout: 3000, + protocolCapabilities: ["reqRes", "stream"], + ...igs, + } satisfies MsgerParamsWithEnDe; +} + +export interface OpenParams { + readonly timeout: number; +} + +export async function applyStart(prC: Promise>): Promise> { + const rC = await prC; + if (rC.isErr()) { + return rC; + } + const c = rC.Ok(); + const r = await c.start(); + if (r.isErr()) { + return Result.Err(r.Err()); + } + return rC; +} + +export class MsgConnected implements MsgRawConnection { + static async connect( + mrc: Result | MsgRawConnection, + conn: Partial = {} + ): Promise> { + if (Result.Is(mrc)) { + if (mrc.isErr()) { + return Result.Err(mrc.Err()); + } + mrc = mrc.Ok(); + } + const res = await mrc.request(buildReqOpen(mrc.sthis, conn), { waitFor: MsgIsResOpen }); + if (MsgIsError(res) || !MsgIsResOpen(res)) { + return mrc.sthis.logger.Error().Err(res).Msg("unexpected response").ResultError(); + } + return Result.Ok(new MsgConnected(mrc, res.conn)); + } + + readonly sthis: SuperThis; + readonly conn: QSId; + readonly raw: MsgRawConnection; + readonly exchangedGestalt: ExchangedGestalt; + readonly activeBinds: Map>; + private constructor(raw: MsgRawConnection, conn: QSId) { + this.sthis = raw.sthis; + this.raw = raw; + this.exchangedGestalt = raw.exchangedGestalt; + this.conn = conn; + this.activeBinds = raw.activeBinds; + } + + bind( + req: Q, + opts: RequestOpts + ): ReadableStream> { + const stream = this.raw.bind({ ...req, conn: req.conn || this.conn }, opts); + const ts = new TransformStream, MsgWithError>({ + transform: (chunk, controller) => { + if (!MsgIsTid(chunk, req.tid)) { + return; + } + if (MsgIsConnected(chunk, this.conn)) { + if ((opts.waitFor && opts.waitFor(chunk)) || MsgIsError(chunk)) { + controller.enqueue(chunk); + } + } + }, + }); + // eslint-disable-next-line no-console + // why the hell pipeTo sends an error that is undefined? + stream.pipeThrough(ts); + // stream.pipeTo(ts.writable).catch((err) => err && err.message && console.error("bind error", err)); + return ts.readable; + } + + request(req: Q, opts: RequestOpts): Promise> { + return this.raw.request({ ...req, conn: req.conn || this.conn }, opts); + } + start(): Promise> { + return this.raw.start(); + } + close(): Promise> { + return this.raw.close(); + } + onMsg(msgFn: OnMsgFn): UnReg { + return this.raw.onMsg((msg) => { + if (MsgIsConnected(msg, this.conn)) { + msgFn(msg); + } + }); + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class Msger { + static async openHttp( + sthis: SuperThis, + // reqOpen: ReqOpen | undefined, + urls: URI[], + msgP: MsgerParamsWithEnDe, + exGestalt: ExchangedGestalt + ): Promise> { + return Result.Ok(new HttpConnection(sthis, urls, msgP, exGestalt)); + } + static async openWS( + sthis: SuperThis, + // qOpen: ReqOpen, + url: URI, + msgP: MsgerParamsWithEnDe, + exGestalt: ExchangedGestalt + ): Promise> { + let ws: WebSocket; + // const { encode } = jsonEnDe(sthis); + url = url.build().URI(); + // .setParam("reqOpen", sthis.txt.decode(encode(qOpen))) + if (runtimeFn().isNodeIsh) { + const { WebSocket } = await import("ws"); + ws = new WebSocket(url.toString()) as unknown as WebSocket; + } else { + ws = new WebSocket(url.toString()); + } + return Result.Ok(new WSConnection(sthis, ws, msgP, exGestalt)); + } + static async open( + sthis: SuperThis, + curl: CoerceURI, + imsgP: Partial = {} + ): Promise> { + // initial exchange with JSON encoding + const jsMsgP = defaultMsgParams(sthis, { ...imsgP, mime: "application/json", ende: jsonEnDe(sthis) }); + const url = URI.from(curl); + const gs = defaultGestalt(defaultMsgParams(sthis, imsgP), { id: "FP-Universal-Client" }); + /* + * request Gestalt with Http + */ + const rHC = await Msger.openHttp(sthis, [url], jsMsgP, { my: gs, remote: gs }); + if (rHC.isErr()) { + return rHC; + } + const hc = rHC.Ok(); + const resGestalt = await hc.request(buildReqGestalt(sthis, gs), { + waitFor: MsgIsResGestalt, + }); + if (!MsgIsResGestalt(resGestalt)) { + return Result.Err(new Error("Invalid Gestalt")); + } + await hc.close(); + const exGt = { my: gs, remote: resGestalt.gestalt } satisfies ExchangedGestalt; + const msgP = defaultMsgParams(sthis, imsgP); + if (exGt.remote.protocolCapabilities.includes("reqRes") && !exGt.remote.protocolCapabilities.includes("stream")) { + return applyStart( + Msger.openHttp( + sthis, + exGt.remote.httpEndpoints.map((i) => BuildURI.from(url).resolve(i).URI()), + msgP, + exGt + ) + ); + } + return applyStart( + Msger.openWS(sthis, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) + ); + } + + static connect( + sthis: SuperThis, + curl: CoerceURI, + imsgP: Partial = {}, + conn: Partial = {} + ): Promise> { + return Msger.open(sthis, curl, imsgP).then((srv) => MsgConnected.connect(srv, conn)); + } + + private constructor() { + /* */ + } +} diff --git a/src/v2-cloud/new-websocket.ts b/src/v2-cloud/new-websocket.ts new file mode 100644 index 00000000..1c8ef6fe --- /dev/null +++ b/src/v2-cloud/new-websocket.ts @@ -0,0 +1,11 @@ +import { CoerceURI, runtimeFn, URI } from "@adviser/cement"; + +export async function newWebSocket(url: CoerceURI): Promise { + const wsUrl = URI.from(url).toString(); + if (runtimeFn().isNodeIsh) { + const { WebSocket: MyWS } = await import("ws"); + return new MyWS(wsUrl) as unknown as WebSocket; + } else { + return new WebSocket(wsUrl); + } +} diff --git a/src/v2-cloud/node-hono-server.ts b/src/v2-cloud/node-hono-server.ts new file mode 100644 index 00000000..53f75a1f --- /dev/null +++ b/src/v2-cloud/node-hono-server.ts @@ -0,0 +1,142 @@ +import { UpgradeWebSocket, WSContext, WSContextInit, WSEvents } from "hono/ws"; +import { ConnMiddleware, HonoServerBase, HonoServerFactory, HonoServerImpl, RunTimeParams } from "./hono-server.js"; +import { HttpHeader, URI } from "@adviser/cement"; +import { Context, Hono } from "hono"; +import { ensureLogger, SuperThis } from "@fireproof/core"; +import { defaultMsgParams, jsonEnDe } from "./msger.js"; +import { defaultGestalt, Gestalt, MsgerParams } from "./msg-types.js"; +import { SQLDatabase } from "./meta-merger/abstract-sql.js"; +import { WSRoom } from "./ws-room.js"; + +interface ServerType { + close(fn: () => void): void; +} + +type serveFn = (options: unknown, listeningListener?: ((info: unknown) => void) | undefined) => ServerType; + +export interface NodeHonoFactoryParams { + readonly msgP?: MsgerParams; + readonly gs?: Gestalt; + readonly sql: SQLDatabase; +} + +const wsConnections = new Map(); +class NodeWSRoom implements WSRoom { + readonly sthis: SuperThis; + constructor(sthis: SuperThis) { + this.sthis = sthis; + } + acceptConnection(ws: WebSocket, wse: WSEvents): Promise { + const id = this.sthis.nextId(12).str; + wsConnections.set(id, ws); + + const wsCtx = new WSContext(ws as WSContextInit); + + ws.onerror = (err) => { + // console.log("onerror", err); + wse.onError?.(err, wsCtx); + }; + ws.onclose = (ev) => { + // console.log("onclose", ev); + wse.onClose?.(ev, wsCtx); + }; + ws.onmessage = (evt) => { + // console.log("onmessage", evt); + // wsCtx.send("Hellox from server"); + wse.onMessage?.(evt, wsCtx); + }; + + ws.accept(); + return Promise.resolve(); + } +} + +export class NodeHonoFactory implements HonoServerFactory { + _upgradeWebSocket!: UpgradeWebSocket; + _injectWebSocket!: (t: unknown) => void; + _serve!: serveFn; + _server!: ServerType; + // _env!: Env; + + readonly sthis: SuperThis; + readonly params: NodeHonoFactoryParams; + constructor(sthis: SuperThis, params: NodeHonoFactoryParams) { + this.sthis = sthis; + this.params = params; + } + + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { + // this._env = c.env; + // const sthis = ensureSuperThis(); + const sthis = this.sthis; + const logger = ensureLogger(sthis, `NodeHono[${URI.from(c.req.url).pathname}]`); + const ende = jsonEnDe(sthis); + + const fpProtocol = sthis.env.get("FP_PROTOCOL"); + const msgP = + this.params.msgP ?? + defaultMsgParams(sthis, { + hasPersistent: true, + protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], + }); + const gs = + this.params.gs ?? + defaultGestalt(msgP, { + id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", + }); + const wsRoom = new NodeWSRoom(sthis); + const nhs = new NodeHonoServer(sthis, this, gs, this.params.sql, wsRoom); + return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs })); + } + + async start(app: Hono): Promise { + try { + const { createNodeWebSocket } = await import("@hono/node-ws"); + const { serve } = await import("@hono/node-server"); + this._serve = serve as serveFn; + const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app }); + this._upgradeWebSocket = upgradeWebSocket; + this._injectWebSocket = injectWebSocket as (t: unknown) => void; + } catch (e) { + throw this.sthis.logger.Error().Err(e).Msg("Failed to start NodeHonoFactory").AsError(); + } + } + + async serve(app: Hono, port: number): Promise { + await new Promise((resolve) => { + this._server = this._serve({ fetch: app.fetch, port }, () => { + this._injectWebSocket(this._server); + resolve(); + }); + }); + } + async close(): Promise { + this._server.close(() => { + /* */ + }); + // return new Promise((res) => this._server.close(() => res())); + } +} + +export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { + readonly _upgradeWebSocket: UpgradeWebSocket; + constructor( + sthis: SuperThis, + factory: NodeHonoFactory, + gs: Gestalt, + sqldb: SQLDatabase, + wsRoom: WSRoom, + headers?: HttpHeader + ) { + super(sthis, sthis.logger, gs, sqldb, wsRoom, headers); + this._upgradeWebSocket = factory._upgradeWebSocket; + } + + override upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { + return async (_conn, c, next) => { + // conn.attachWSPair({ client: c.req, server: c.res }); + return this._upgradeWebSocket(createEvents)(c, next); + }; + } +} diff --git a/src/v2-cloud/pre-signed-url.ts b/src/v2-cloud/pre-signed-url.ts new file mode 100644 index 00000000..a5f25de7 --- /dev/null +++ b/src/v2-cloud/pre-signed-url.ts @@ -0,0 +1,80 @@ +import { Result, URI } from "@adviser/cement"; +import { AwsClient } from "aws4fetch"; +import { MsgWithConn, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; + +export interface PreSignedMsg extends MsgWithTenantLedger { + readonly params: SignedUrlParam; +} + +// export interface PreSignedConnMsg { +// readonly params: SignedUrlParam; +// readonly tid: string; +// readonly conn: QSId; +// } + +export interface PreSignedEnv { + readonly storageUrl: URI; + readonly aws: { + readonly accessKeyId: string; + readonly secretAccessKey: string; + readonly region?: string; + }; + readonly test?: { + readonly amzDate?: string; + }; +} + +export async function calculatePreSignedUrl(psm: PreSignedMsg, env: PreSignedEnv): Promise> { + // if (!ipsm.conn) { + // return Result.Err(new Error("Connection is not supported")); + // } + // const psm = ipsm as PreSignedConnMsg; + + // verify if you are not overriding + let store: string = psm.params.store; + if (psm.params.index?.length) { + store = `${store}-${psm.params.index}`; + } + const expiresInSeconds = psm.params.expires || 60 * 60; + + const suffix = ""; + // switch (psm.params.store) { + // case "wal": + // case "meta": + // suffix = ".json"; + // break; + // default: + // break; + // } + + const opUrl = env.storageUrl + .build() + // .protocol(vals.protocuol === "ws" ? "http:" : "https:") + .setParam("X-Amz-Expires", expiresInSeconds.toString()) + .setParam("tid", psm.tid) + .appendRelative(psm.tenant.tenant) + .appendRelative(psm.tenant.ledger) + .appendRelative(store) + .appendRelative(`${psm.params.key}${suffix}`) + .URI(); + const a4f = new AwsClient({ + ...env.aws, + region: env.aws.region || "us-east-1", + service: "s3", + }); + const signedUrl = await a4f + .sign( + new Request(opUrl.toString(), { + method: psm.params.method, + }), + { + aws: { + signQuery: true, + datetime: env.test?.amzDate, + // datetime: env.TEST_DATE, + }, + } + ) + .then((res) => res.url); + return Result.Ok(URI.from(signedUrl)); +} diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts new file mode 100644 index 00000000..144ff493 --- /dev/null +++ b/src/v2-cloud/test-helper.ts @@ -0,0 +1,217 @@ +import { Future, Result, URI } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { $, fs } from "zx"; +import { HttpConnection } from "./http-connection.js"; +import { + MsgerParams, + Gestalt, + defaultGestalt, + buildReqGestalt, + MsgIsResGestalt, + MsgIsError, + MsgBase, +} from "./msg-types.js"; +import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection } from "./msger.js"; +import { WSConnection } from "./ws-connection.js"; +import * as toml from "smol-toml"; +import { Env } from "./backend/env.js"; +import { HonoServer } from "./hono-server.js"; +import { NodeHonoFactory } from "./node-hono-server.js"; +import { CFHonoFactory } from "./backend/cf-hono-server.js"; +import { BetterSQLDatabase } from "./meta-merger/bettersql-abstract-sql.js"; + +export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { + const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["reqRes"] }), { + id: "HTTP-server", + }); + const exGt = { my, remote }; + return { + name: "HTTP", + remoteGestalt: remote, + cInstance: HttpConnection, + ok: { + url: () => URI.from(`http://127.0.0.1:${port}/fp`), + open: () => + applyStart( + Msger.openHttp( + sthis, + [URI.from(`http://localhost:${port}/fp`)], + { + ...msgP, + // protocol: "http", + timeout: 1000, + }, + exGt + ) + ), + }, + connRefused: { + url: () => URI.from(`http://127.0.0.1:${port - 1}/fp`), + open: async (): Promise>> => { + const ret = await Msger.openHttp( + sthis, + [URI.from(`http://localhost:${port - 1}/fp`)], + { + ...msgP, + // protocol: "http", + timeout: 1000, + }, + exGt + ); + if (ret.isErr()) { + return ret; + } + // should fail + const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); + if (MsgIsError(res)) { + return Result.Err(res.message); + } + return ret; + }, + }, + timeout: { + url: () => URI.from(`http://4.7.1.1:${port}/fp`), + open: async (): Promise>> => { + const ret = await Msger.openHttp( + sthis, + [URI.from(`http://4.7.1.1:${port}/fp`)], + { + ...msgP, + // protocol: "http", + timeout: 500, + }, + exGt + ); + // should fail + const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); + if (MsgIsError(res)) { + return Result.Err(res.message); + } + return ret; + }, + }, + }; +} + +export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { + const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["stream"] }), { + id: "WS-server", + }); + const exGt = { my, remote }; + return { + name: "WS", + remoteGestalt: remote, + cInstance: WSConnection, + ok: { + url: () => URI.from(`http://127.0.0.1:${port}/ws`), + open: () => + applyStart( + Msger.openWS( + sthis, + URI.from(`http://localhost:${port}/ws`), + { + ...msgP, + // protocol: "ws", + timeout: 1000, + }, + exGt + ) + ), + }, + connRefused: { + url: () => URI.from(`http://127.0.0.1:${port - 1}/ws`), + open: () => + Msger.openWS( + sthis, + URI.from(`http://localhost:${port - 1}/ws`), + { + ...msgP, + // protocol: "ws", + timeout: 1000, + }, + exGt + ), + }, + timeout: { + url: () => URI.from(`http://4.7.1.1:${port - 1}/ws`), + open: () => + Msger.openWS( + sthis, + URI.from(`http://4.7.1.1:${port - 1}/ws`), + { + ...msgP, + // protocol: "ws", + timeout: 500, + }, + exGt + ), + }, + }; +} + +export async function resolveToml(backend: "D1" | "DO") { + const tomlFile = "src/cloud/backend/wrangler.toml"; + const tomeStr = await fs.readFile(tomlFile, "utf-8"); + const wranglerFile = toml.parse(tomeStr) as unknown as { + env: Record; + }; + return { + tomlFile, + env: wranglerFile.env[`test-reqRes-${backend}`].vars, + }; +} + +export function NodeHonoServerFactory() { + return { + name: "NodeHonoServer", + factory: async (sthis: SuperThis, msgP: MsgerParams, remoteGestalt: Gestalt, _port: number) => { + const { env } = await resolveToml("D1"); + sthis.env.sets(env as unknown as Record); + const nhf = new NodeHonoFactory(sthis, { + msgP, + gs: remoteGestalt, + sql: new BetterSQLDatabase("./dist/node-meta.sqlite"), + }); + return new HonoServer(nhf); + }, + }; +} +export function CFHonoServerFactory(backend: "D1" | "DO") { + return { + name: `CFHonoServer(${backend})`, + factory: async (_sthis: SuperThis, _msgP: MsgerParams, remoteGestalt: Gestalt, port: number) => { + if (process.env.FP_WRANGLER_PORT) { + return new HonoServer(new CFHonoFactory()); + } + const { tomlFile } = await resolveToml(backend); + $.verbose = !!process.env.FP_DEBUG; + const runningWrangler = $` + wrangler dev -c ${tomlFile} --port ${port} --env test-${remoteGestalt.protocolCapabilities[0]}-${backend} --no-show-interactive-dev-session & + waitPid=$! + echo "PID:$waitPid" + wait $waitPid`; + const waitReady = new Future(); + let pid: number | undefined; + runningWrangler.stdout.on("data", (chunk) => { + // console.log(">>", chunk.toString()) + const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; + if (mightPid) { + pid = +mightPid; + } + if (chunk.includes("Ready on http")) { + waitReady.resolve(true); + } + }); + runningWrangler.stderr.on("data", (chunk) => { + // eslint-disable-next-line no-console + console.error("!!", chunk.toString()); + }); + await waitReady.asPromise(); + return new HonoServer( + new CFHonoFactory(() => { + if (pid) process.kill(pid); + }) + ); + }, + }; +} diff --git a/src/v2-cloud/ws-connection.ts b/src/v2-cloud/ws-connection.ts new file mode 100644 index 00000000..5c29cfd7 --- /dev/null +++ b/src/v2-cloud/ws-connection.ts @@ -0,0 +1,211 @@ +import { exception2Result, Future, Logger, Result } from "@adviser/cement"; +import { SuperThis, ensureLogger } from "@fireproof/core"; +import { + MsgBase, + MsgIsError, + buildErrorMsg, + ReqOpen, + WaitForTid, + MsgWithError, + RequestOpts, + MsgIsTid, +} from "./msg-types.js"; +import { ActiveStream, ExchangedGestalt, MsgerParamsWithEnDe, MsgRawConnection, OnMsgFn, UnReg } from "./msger.js"; +import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; + +export interface WSReqOpen { + readonly reqOpen: ReqOpen; + readonly ws: WebSocket; // this WS is opened with a specific URL-Param +} + +export class WSConnection extends MsgRawConnectionBase implements MsgRawConnection { + readonly logger: Logger; + readonly msgP: MsgerParamsWithEnDe; + readonly ws: WebSocket; + // readonly baseURI: URI; + + readonly #onMsg = new Map(); + readonly #onClose = new Map(); + + readonly waitForTid = new Map(); + + opened = false; + + readonly id: string; + + constructor(sthis: SuperThis, ws: WebSocket, msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { + super(sthis, exGestalt); + this.id = sthis.nextId().str; + this.logger = ensureLogger(sthis, "WSConnection"); + this.msgP = msgP; + this.ws = ws; + // this.wqs = { ...wsq }; + } + + async start(): Promise> { + const onOpenFuture: Future> = new Future>(); + const timer = setTimeout(() => { + const err = this.logger.Error().Dur("timeout", this.msgP.timeout).Msg("Timeout").AsError(); + this.toMsg(buildErrorMsg(this.sthis, this.logger, {} as MsgBase, err)); + onOpenFuture.resolve(Result.Err(err)); + }, this.msgP.timeout); + this.ws.onopen = () => { + onOpenFuture.resolve(Result.Ok(undefined)); + this.opened = true; + }; + this.ws.onerror = (ierr) => { + const err = this.logger.Error().Err(ierr).Msg("WS Error").AsError(); + onOpenFuture.resolve(Result.Err(err)); + const res = this.buildErrorMsg(this.logger.Error(), {}, err); + this.toMsg(res); + }; + this.ws.onmessage = (evt) => { + if (!this.opened) { + this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + {} as MsgBase, + this.logger.Error().Msg("Received message before onOpen").AsError() + ) + ); + } + this.#wsOnMessage(evt); + }; + this.ws.onclose = () => { + this.opened = false; + this.close().catch((ierr) => { + const err = this.logger.Error().Err(ierr).Msg("close error").AsError(); + onOpenFuture.resolve(Result.Err(err)); + this.toMsg(buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, err)); + }); + }; + /* wait for onOpen */ + const rOpen = await onOpenFuture.asPromise().finally(() => { + clearTimeout(timer); + }); + if (rOpen.isErr()) { + return rOpen; + } + // const resOpen = await this.request(this.wqs.reqOpen, { waitFor: MsgIsResOpen }); + // if (!MsgIsResOpen(resOpen)) { + // return Result.Err(this.logger.Error().Any("ErrMsg", resOpen).Msg("Invalid response").AsError()); + // } + // this.wqs.resOpen = resOpen; + return Result.Ok(undefined); + } + + readonly #wsOnMessage = async (event: MessageEvent) => { + const rMsg = await exception2Result(() => this.msgP.ende.decode(event.data) as MsgBase); + if (rMsg.isErr()) { + this.logger.Error().Err(rMsg).Any(event.data).Msg("Invalid message"); + return; + } + const msg = rMsg.Ok(); + const waitFor = this.waitForTid.get(msg.tid); + this.#onMsg.forEach((cb) => cb(msg)); + if (waitFor) { + if (MsgIsError(msg)) { + this.waitForTid.delete(msg.tid); + waitFor.future.resolve(msg); + } else if (waitFor.waitFor(msg)) { + // what for a specific type + this.waitForTid.delete(msg.tid); + waitFor.future.resolve(msg); + } else { + // wild-card + this.waitForTid.delete(msg.tid); + waitFor.future.resolve(msg); + } + } + }; + + async close(): Promise> { + this.#onClose.forEach((fn) => fn()); + this.#onClose.clear(); + this.#onMsg.clear(); + this.ws.close(); + return Result.Ok(undefined); + } + + toMsg(msg: MsgWithError): MsgWithError { + this.#onMsg.forEach((fn) => fn(msg)); + return msg; + } + + sendMsg(msg: MsgBase): Promise { + this.ws.send(this.msgP.ende.encode(msg)); + return Promise.resolve(); + } + + onMsg(fn: OnMsgFn): UnReg { + const key = this.sthis.nextId().str; + this.#onMsg.set(key, fn as OnMsgFn); + return () => this.#onMsg.delete(key); + } + + onClose(fn: UnReg): UnReg { + const key = this.sthis.nextId().str; + this.#onClose.set(key, fn); + return () => this.#onClose.delete(key); + } + + readonly activeBinds = new Map>(); + bind(req: Q, opts: RequestOpts): ReadableStream> { + const state: ActiveStream = { + id: this.sthis.nextId().str, + bind: { + msg: req, + opts, + }, + // timeout: undefined, + // controller: undefined, + } satisfies ActiveStream; + this.activeBinds.set(state.id, state); + return new ReadableStream>({ + cancel: () => { + // clearTimeout(state.timeout as number); + this.activeBinds.delete(state.id); + }, + start: (controller) => { + this.onMsg((msg) => { + if (MsgIsError(msg)) { + controller.enqueue(msg); + return; + } + if (!MsgIsTid(msg, req.tid)) { + return; + } + if (opts.waitFor && opts.waitFor(msg)) { + controller.enqueue(msg); + } + }); + this.sendMsg(req); + const future = new Future(); + this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); + future.asPromise().then((msg) => { + if (MsgIsError(msg)) { + // double err emitting + controller.enqueue(msg); + controller.close(); + } + }); + }, + }); + } + + async request(req: Q, opts: RequestOpts): Promise> { + if (!this.opened) { + return buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Msg("Connection not open").AsError()); + } + const future = new Future(); + this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); + await this.sendMsg(req); + return future.asPromise(); + } + + // toOnMessage(msg: WithErrorMsg): Result> { + // this.mec.msgFn?.(msg as unknown as MessageEvent); + // return Result.Ok(msg); + // } +} diff --git a/src/v2-cloud/ws-room.ts b/src/v2-cloud/ws-room.ts new file mode 100644 index 00000000..ebe8e33b --- /dev/null +++ b/src/v2-cloud/ws-room.ts @@ -0,0 +1,5 @@ +import { WSEvents } from "hono/ws"; + +export interface WSRoom { + acceptConnection(ws: WebSocket, wse: WSEvents): Promise; +} diff --git a/tests/Dockerfile.connect-cloud b/tests/Dockerfile.connect-cloud new file mode 100644 index 00000000..9e3f8884 --- /dev/null +++ b/tests/Dockerfile.connect-cloud @@ -0,0 +1,19 @@ +FROM node:20-slim AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +RUN apt update && apt install -y git +COPY src/cloud/backend /usr/src/app/cloud/backend +COPY .gitignore LICENSE.md /usr/src/app/cloud/backend/ +COPY package-cloud-backend.json /usr/src/app/cloud/backend +COPY version-copy-package.ts /usr/src/app/cloud/backend +WORKDIR /usr/src/app/cloud/backend +RUN pnpm install zx cmd-ts + +COPY package.json /usr/src/app/cloud/backend +RUN npx tsx version-copy-package.ts --skip-pack ./dist/package-cloud-backend.json && \ + cp ./dist/package.json . && \ + cat package.json && \ + pnpm i + +CMD pnpm run start diff --git a/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts b/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts index 0acc5d2e..0dee93de 100644 --- a/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts +++ b/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts @@ -23,7 +23,7 @@ export default async (req: Request) => { return new Response(JSON.stringify({ ok: true }), { status: 201 }); } else if (metaDb) { const meta = getStore("meta"); - const x = await req.json() as CRDTEntry[]; + const x = await req.json(); // fixme, marty changed to [0] as it is a slice of the structure we expected const { data, cid, parents } = x[0]; await meta.setJSON(`${metaDb}/${cid}`, { data, parents }); diff --git a/tests/start-cloud.sh b/tests/start-cloud.sh new file mode 100644 index 00000000..3f71e73e --- /dev/null +++ b/tests/start-cloud.sh @@ -0,0 +1,13 @@ +docker rm -f $(docker ps --format '{{.ID}}.{{.Names}}' -a | grep 'cloud'| sed 's/\..*$//') +docker buildx build -t fireproof-cloud:latest --progress plain --no-cache -f ./tests/Dockerfile.connect-cloud . +# --no-cache-filter +docker run --name cloud \ + -e ACCESS_KEY_ID="minioadmin" \ + -e SECRET_ACCESS_KEY="minioadmin" \ + -e BUCKET_NAME="testbucket" \ + -e STORAGE_URL="http://localhost:9000/testbucket" \ + -e FP_STACK=fp \ + -e FP_DEBUG=Fireproof \ + -d -p 1968:1968 fireproof-cloud + + diff --git a/tests/waiton.ts b/tests/waiton.ts index 4c0d0885..97218668 100644 --- a/tests/waiton.ts +++ b/tests/waiton.ts @@ -1,6 +1,12 @@ import waitOn from "wait-on"; const opts = { - resources: ["http://localhost:8888/", "http://localhost:1999/", "http://localhost:8787/", "http://localhost:9000/"], + resources: [ + "http://localhost:8888/", + "http://localhost:1999/", + "http://localhost:8787/", + "http://localhost:9000/", + "http://localhost:1968", + ], delay: 0, // initial delay in ms, default 0 interval: 500, // poll interval in ms, default 250ms simultaneous: 4, // limit to 1 connection per resource at a time diff --git a/version-copy-package.ts b/version-copy-package.ts index b7cb9cb0..8f8986bf 100644 --- a/version-copy-package.ts +++ b/version-copy-package.ts @@ -1,70 +1,85 @@ -/* eslint-disable no-console */ -import fs from "fs/promises"; -import process from "process"; -import path from "path"; -import { $ } from "zx"; + const cmd = command({ + name: "version-copy-package", + description: "prepare a package.json for a release", + version: "1.0.0", + args: { + // method: option({ + // long: "method", + // type: oneOf(["GET", "PUT", "POST", "DELETE"]), + // defaultValue: () => "PUT", + // defaultValueIsSerializable: true, + // }), + verbose: flag({ + long: "verbose", + type: boolean, + // defaultValue: () => "false", + // defaultValueIsSerializable: true, + }), + skipPack: flag({ + long: "skip-pack", + type: boolean, + // defaultValue: () => "false", + // defaultValueIsSerializable: true, + }), + buildDest: positional({ + type: string, + description: "build destination", + }), + }, + handler: async (args) => { + $.verbose = args.verbose; -async function copyFilesToDist(destDir: string) { - for (const file of ["./.gitignore", "./LICENSE.md"]) { - await fs.copyFile(file, path.join(destDir, file)); - } -} - -async function patchVersion(packageJson: Record) { - let version = "refs/tags/v0.0.0-smoke"; - if (process.env.GITHUB_REF && process.env.GITHUB_REF.startsWith("refs/tags/v")) { - version = process.env.GITHUB_REF; - } - version = version.split("/").slice(-1)[0].replace(/^v/, ""); - console.log(`Patch version ${version} in package.json`); - packageJson.version = version; -} - -async function main() { - $.verbose = true; - const buildDest = process.argv[process.argv.length - 1]; - if (!(buildDest.startsWith("dist/") || buildDest.startsWith("./dist/"))) { - console.error("Usage: tsx version-copy-package.ts dist//template-package.json"); - process.exit(1); - } - const destDir = path.dirname(buildDest); - if (!(await fs.stat(destDir)).isDirectory) { - console.error(`Directory ${destDir} does not exist`); - process.exit(1); - } - await copyFilesToDist(destDir); - const mainPackageJson = JSON.parse(await fs.readFile("package.json", "utf8")); - const templateFile = path.basename(buildDest); - const destPackageJson = JSON.parse(await fs.readFile(templateFile, "utf-8")); - // copy version from package.json - for (const destDeps of Object.keys(destPackageJson.dependencies)) { - if (!mainPackageJson.dependencies[destDeps]) { - console.error(`Dependency ${destDeps} not found in main package.json`); - } else { - destPackageJson.dependencies[destDeps] = mainPackageJson.dependencies[destDeps]; - } - } - patchVersion(destPackageJson); - // add a dependency to fireproof core with the same tag we're building - destPackageJson.dependencies["@fireproof/core"] = mainPackageJson.dependencies["@fireproof/core"]; - if (!mainPackageJson.dependencies["@fireproof/core"]) { - throw new Error("there must be a version of @fireproof/core in main"); - } - for (const i of ["keywords", "contributors", "license"]) { - if (typeof mainPackageJson[i] === "string") { - destPackageJson[i] = mainPackageJson[i]; - } else if (Array.isArray(mainPackageJson[i])) { - destPackageJson[i] = Array.from(new Set([...mainPackageJson[i], ...(destPackageJson[i] || [])])); - } else { - destPackageJson[i] = { ...mainPackageJson[i], ...destPackageJson[i] }; - } - } - const destPackageJsonFile = path.join(destDir, "package.json"); - await fs.writeFile(destPackageJsonFile, JSON.stringify(destPackageJson, null, 2)); - console.log( - `Copied ${templateFile} to ${destDir} with version ${destPackageJson.version} using fireproof/core=${destPackageJson.dependencies["@fireproof/core"]}` - ); - await $`cd ${destDir} && pnpm pack`.pipe(process.stdout); + // const buildDest = process.argv[process.argv.length - 1]; + const buildDest = args.buildDest; + if (!(buildDest.startsWith("dist/") || buildDest.startsWith("./dist/"))) { + console.error("Usage: tsx version-copy-package.ts dist//template-package.json"); + process.exit(1); + } + const destDir = path.dirname(buildDest); + await fs.mkdir(destDir, { recursive: true }); + if (!(await fs.stat(destDir)).isDirectory) { + console.error(`Directory ${destDir} does not exist`); + process.exit(1); + } + await copyFilesToDist(destDir); + const mainPackageJson = JSON.parse(await fs.readFile("package.json", "utf8")); + const templateFile = path.basename(buildDest); + const destPackageJson = JSON.parse(await fs.readFile(templateFile, "utf-8")); + // copy version from package.json + for (const destDeps of Object.keys(destPackageJson.dependencies)) { + if (!mainPackageJson.dependencies[destDeps]) { + console.error(`Dependency ${destDeps} not found in main package.json`); + } else { + destPackageJson.dependencies[destDeps] = mainPackageJson.dependencies[destDeps]; + } + } + patchVersion(destPackageJson); + // add a dependency to fireproof core with the same tag we're building + destPackageJson.dependencies["@fireproof/core"] = mainPackageJson.dependencies["@fireproof/core"]; + if (!mainPackageJson.dependencies["@fireproof/core"]) { + throw new Error("there must be a version of @fireproof/core in main"); + } + for (const i of ["keywords", "contributors", "license"]) { + if (typeof mainPackageJson[i] === "string") { + destPackageJson[i] = mainPackageJson[i]; + } else if (Array.isArray(mainPackageJson[i])) { + destPackageJson[i] = Array.from(new Set([...mainPackageJson[i], ...(destPackageJson[i] || [])])); + } else { + destPackageJson[i] = { ...mainPackageJson[i], ...destPackageJson[i] }; + } + } + const destPackageJsonFile = path.join(destDir, "package.json"); + await fs.writeFile(destPackageJsonFile, JSON.stringify(destPackageJson, null, 2)); + console.log( + `Copied ${templateFile} to ${destDir} with version ${destPackageJson.version} using fireproof/core=${destPackageJson.dependencies["@fireproof/core"]}` + ); + if (args.skipPack) { + return; + } + await $`cd ${destDir} && pnpm pack`.pipe(process.stdout); + }, + }); + await run(cmd, process.argv.slice(2)); } main().catch(console.error); diff --git a/vitest.v1-cloud.config.ts b/vitest.v1-cloud.config.ts index 0f918785..5da6fc0b 100644 --- a/vitest.v1-cloud.config.ts +++ b/vitest.v1-cloud.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from "vitest/config"; - import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; +// import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; export default defineConfig({ plugins: [tsconfigPaths()], @@ -9,6 +9,8 @@ export default defineConfig({ exclude: [ "node_modules/@fireproof/core/tests/react/**", "node_modules/@fireproof/core/tests/fireproof/config.test.ts", + "node_modules/@fireproof/core/tests/blockstore/keyed-crypto*", + "node_modules/@fireproof/core/tests/**/utils.test.ts", ], include: [ // "node_modules/@fireproof/core/tests/**/*test.?(c|m)[jt]s?(x)", @@ -19,5 +21,8 @@ export default defineConfig({ globals: true, setupFiles: "./setup.v1-cloud.ts", testTimeout: 25000, + // poolOptions: { + // workers: { wrangler: { configPath: './src/cloud/backend/wrangler.toml' } }, + // }, }, }); diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index 3c60278e..00000000 --- a/wrangler.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Top-level configuration -name = "connector" -#main = "src/cf-index.ts" -compatibility_date = "2024-06-03" -compatibility_flags = [ "nodejs_compat" ] From a40adf3634bfad783f0d2da444506529bb86133b Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Thu, 28 Nov 2024 23:13:40 +0100 Subject: [PATCH 02/14] chore: intro of local cloud tests chore: cloud now generated pre-signed-urls chore: fix signature chore: remove env version chore: remove console.log chore: now we have cloud-backend tests chore: the ng redirecting backend chore: everything except subscript chore: MsgConnection chore: Msger with connection tests chore: hono is our server platform chore: Get/Put/Delete.Data/WAL chore: refactor cloud test helper chore: added data/wal store tests chore: from single scripts to docker-compose chore: improve exit code handling hore: zx do not throw chore: make zx happy with strings vs. arrays chore: on error exitCode chore: fix the checksum errors chore: sql implementation of meta-merge chore: run tests on node and cf-worker chore: stablize the tests chore: with Meta-Merge chore: implement sql driver for durable object chore: added WSRoom for ws broadcast to all connected sockets chore: rename cloud to fp-cloud --- .coderabbit.yaml | 2 +- README.md | 10 + package.json | 17 +- pnpm-lock.yaml | 195 +++++- setup.cloud.ts | 9 +- setup.libsql.ts | 5 + setup.node-sqlite3-wasm.ts | 2 +- src/aws/gateway.ts | 6 +- src/coerce-binary.ts | 38 ++ src/fp-cloud/backend/cf-dobj-abstract-sql.ts | 31 + src/fp-cloud/backend/cf-hono-server.ts | 194 ++++++ src/fp-cloud/backend/env.d.ts | 59 ++ src/fp-cloud/backend/fp-meta-groups.ts-off | 47 ++ src/fp-cloud/backend/server.ts | 72 +++ src/fp-cloud/backend/wrangler.toml | 143 +++++ src/fp-cloud/client/README.md | 58 ++ src/fp-cloud/client/cli-pre-signed-url.ts | 119 ++++ src/fp-cloud/client/cloud-gateway.test.ts | 123 ++++ src/fp-cloud/client/gateway.ts | 605 ++++++++++++++++++ src/fp-cloud/client/index.ts | 125 ++++ src/fp-cloud/cloud.test.ts-off | 533 +++++++++++++++ src/fp-cloud/connection.test.ts | 362 +++++++++++ src/fp-cloud/hono-server.ts | 265 ++++++++ src/fp-cloud/http-connection.ts | 178 ++++++ src/fp-cloud/meta-merger/abstract-sql.ts | 53 ++ .../meta-merger/bettersql-abstract-sql.ts | 36 ++ .../meta-merger/cf-worker-abstract-sql.ts | 31 + src/fp-cloud/meta-merger/create-schema-cli.ts | 9 + .../meta-merger/meta-by-tenant-ledger.ts | 173 +++++ src/fp-cloud/meta-merger/meta-merger.test.ts | 245 +++++++ src/fp-cloud/meta-merger/meta-merger.ts | 116 ++++ src/fp-cloud/meta-merger/meta-send.ts | 128 ++++ src/fp-cloud/meta-merger/tenant-ledger.ts | 62 ++ src/fp-cloud/meta-merger/tenant.ts | 51 ++ src/fp-cloud/msg-dispatch.ts | 139 ++++ src/fp-cloud/msg-dispatcher-impl.ts | 127 ++++ src/fp-cloud/msg-processor.ts-off | 261 ++++++++ src/fp-cloud/msg-raw-connection-base.ts | 31 + src/fp-cloud/msg-request.ts | 220 +++++++ src/fp-cloud/msg-type-meta.ts | 160 +++++ src/fp-cloud/msg-types-data.ts | 109 ++++ src/fp-cloud/msg-types-wal.ts | 130 ++++ src/fp-cloud/msg-types.ts | 567 ++++++++++++++++ src/fp-cloud/msger.ts | 274 ++++++++ src/fp-cloud/new-websocket.ts | 11 + src/fp-cloud/node-hono-server.ts | 142 ++++ src/fp-cloud/pre-signed-url.ts | 80 +++ src/fp-cloud/test-helper.ts | 217 +++++++ src/fp-cloud/ws-connection.ts | 211 ++++++ src/fp-cloud/ws-room.ts | 5 + src/netlify/server.ts | 5 +- src/partykit/partykit-gateway.test.ts | 1 - .../v0.19/sqlite/libsql/sqlite-connection.ts | 79 +++ src/sql/v0.19/sqlite_factory.ts | 11 +- src/ucan/client.ts | 12 +- src/ucan/common.ts | 29 +- tests/Dockerfile.connect-netlify | 1 + .../app/netlify/edge-functions/fireproof.ts | 5 +- tsconfig.json | 11 +- version-copy-package.ts | 23 + ...kv.config.ts => vitest.cf-worker.config.ts | 8 +- vitest.libsql.config.ts | 18 + vitest.v1-cloud.config.ts | 9 +- vitest.v2-cloud.config.ts | 28 + vitest.workspace.ts | 7 +- 65 files changed, 6991 insertions(+), 42 deletions(-) create mode 100644 setup.libsql.ts create mode 100644 src/coerce-binary.ts create mode 100644 src/fp-cloud/backend/cf-dobj-abstract-sql.ts create mode 100644 src/fp-cloud/backend/cf-hono-server.ts create mode 100644 src/fp-cloud/backend/env.d.ts create mode 100644 src/fp-cloud/backend/fp-meta-groups.ts-off create mode 100644 src/fp-cloud/backend/server.ts create mode 100644 src/fp-cloud/backend/wrangler.toml create mode 100644 src/fp-cloud/client/README.md create mode 100644 src/fp-cloud/client/cli-pre-signed-url.ts create mode 100644 src/fp-cloud/client/cloud-gateway.test.ts create mode 100644 src/fp-cloud/client/gateway.ts create mode 100644 src/fp-cloud/client/index.ts create mode 100644 src/fp-cloud/cloud.test.ts-off create mode 100644 src/fp-cloud/connection.test.ts create mode 100644 src/fp-cloud/hono-server.ts create mode 100644 src/fp-cloud/http-connection.ts create mode 100644 src/fp-cloud/meta-merger/abstract-sql.ts create mode 100644 src/fp-cloud/meta-merger/bettersql-abstract-sql.ts create mode 100644 src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts create mode 100644 src/fp-cloud/meta-merger/create-schema-cli.ts create mode 100644 src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts create mode 100644 src/fp-cloud/meta-merger/meta-merger.test.ts create mode 100644 src/fp-cloud/meta-merger/meta-merger.ts create mode 100644 src/fp-cloud/meta-merger/meta-send.ts create mode 100644 src/fp-cloud/meta-merger/tenant-ledger.ts create mode 100644 src/fp-cloud/meta-merger/tenant.ts create mode 100644 src/fp-cloud/msg-dispatch.ts create mode 100644 src/fp-cloud/msg-dispatcher-impl.ts create mode 100644 src/fp-cloud/msg-processor.ts-off create mode 100644 src/fp-cloud/msg-raw-connection-base.ts create mode 100644 src/fp-cloud/msg-request.ts create mode 100644 src/fp-cloud/msg-type-meta.ts create mode 100644 src/fp-cloud/msg-types-data.ts create mode 100644 src/fp-cloud/msg-types-wal.ts create mode 100644 src/fp-cloud/msg-types.ts create mode 100644 src/fp-cloud/msger.ts create mode 100644 src/fp-cloud/new-websocket.ts create mode 100644 src/fp-cloud/node-hono-server.ts create mode 100644 src/fp-cloud/pre-signed-url.ts create mode 100644 src/fp-cloud/test-helper.ts create mode 100644 src/fp-cloud/ws-connection.ts create mode 100644 src/fp-cloud/ws-room.ts create mode 100644 src/sql/v0.19/sqlite/libsql/sqlite-connection.ts rename vitest.cf-kv.config.ts => vitest.cf-worker.config.ts (63%) create mode 100644 vitest.libsql.config.ts create mode 100644 vitest.v2-cloud.config.ts diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4a839e76..91d1c97e 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -10,6 +10,6 @@ reviews: collapse_walkthrough: false auto_review: enabled: true - drafts: true + drafts: false chat: auto_reply: true diff --git a/README.md b/README.md index 89cebffa..b0545673 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,13 @@ To run a single test by its full name, you can use the `-t` flag followed by the ```console $ pnpm test-gateways --project partykit -t "should sync to an empty db" ``` + +Cloud Meta Merge Datastructure: + +1. PK(reqId,resId,tenant,ledger) accessed(date) (delete after x of time) +2. PK(tenant,ledger,reqId,resId) meta deliveryCount (delete if deiveryCount > y) + if meta is updated deliveryCount = 0 + +getMeta updates deliveryCount +getMeta on stream starts updates stream of resGetMeta +avoid subscribe method diff --git a/package.json b/package.json index d8e8c842..7e3f7090 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,13 @@ "description": "Live database connector for the web", "type": "module", "scripts": { - "publish": "pnpm run '/publish:/'", - "publish:aws": "tsx ./publish-package.ts ./dist/aws/package.json", - "publish:v1-cloud": "tsx ./publish-package.ts ./dist/v1-cloud/package.json", - "publish:netlify": "tsx ./publish-package.ts ./dist/netlify/package.json", - "publish:s3": "tsx ./publish-package.ts ./dist/s3/package.json", - "publish:partykit": "tsx ./publish-package.ts ./dist/partykit/package.json", - "publish:ucan": "tsx ./publish-package.ts ./dist/ucan/package.json", + "xpublish": "pnpm run '/publish:/'", + "xpublish:aws": "tsx ./publish-package.ts ./dist/aws/package.json", + "xpublish:cloud": "tsx ./publish-package.ts ./dist/cloud/package.json", + "xpublish:netlify": "tsx ./publish-package.ts ./dist/netlify/package.json", + "xpublish:s3": "tsx ./publish-package.ts ./dist/s3/package.json", + "xpublish:partykit": "tsx ./publish-package.ts ./dist/partykit/package.json", + "xpublish:ucan": "tsx ./publish-package.ts ./dist/ucan/package.json", "prebuild": "rm -rf dist", "xprepare": "pnpm run build", "build": "pnpm run '/^build:/' && pnpm run '/^pub:/'", @@ -108,6 +108,7 @@ "@hono/node-ws": "^1.0.4", "@jspm/core": "^2.1.0", "@netlify/blobs": "^8.1.1", + "@libsql/client": "^0.14.0", "@ucanto/client": "^9.0.1", "@ucanto/core": "^10.3.1", "@ucanto/interface": "^10.2.0", @@ -127,7 +128,7 @@ "cmd-ts": "^0.13.0", "dotenv": "^16.4.5", "events": "^3.3.0", - "hono": "^4.6.11", + "hono": "^4.6.13", "idb-keyval": "^6.2.1", "is-deep-strict-equal-x": "^1.1.2", "jose": "^6.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 909375d8..4e8d7bf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@jspm/core': specifier: ^2.1.0 version: 2.1.0 + '@libsql/client': + specifier: ^0.14.0 + version: 0.14.0 '@netlify/blobs': specifier: ^8.1.1 version: 8.1.1 @@ -108,7 +111,7 @@ importers: specifier: ^3.3.0 version: 3.3.0 hono: - specifier: ^4.6.11 + specifier: ^4.6.13 version: 4.7.4 idb-keyval: specifier: ^6.2.1 @@ -1200,10 +1203,64 @@ packages: '@jspm/core@2.1.0': resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==} + '@libsql/client@0.14.0': + resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} + + '@libsql/core@0.14.0': + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} + + '@libsql/darwin-arm64@0.4.7': + resolution: {integrity: sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg==} + cpu: [arm64] + os: [darwin] + + '@libsql/darwin-x64@0.4.7': + resolution: {integrity: sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA==} + cpu: [x64] + os: [darwin] + + '@libsql/hrana-client@0.7.0': + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + + '@libsql/isomorphic-fetch@0.3.1': + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm64-gnu@0.4.7': + resolution: {integrity: sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.4.7': + resolution: {integrity: sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.4.7': + resolution: {integrity: sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.4.7': + resolution: {integrity: sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.4.7': + resolution: {integrity: sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw==} + cpu: [x64] + os: [win32] + '@multiformats/murmur3@2.1.8': resolution: {integrity: sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@netlify/blobs@8.1.1': resolution: {integrity: sha512-7Dg3PzArvQ0Owq4wpkLECC9iaDBOxuWUN2uwbQtUF6tZssyez2QN+eO0CjuIhhZUivbw+X9bwsyiEjWkdJnv/A==} engines: {node: ^14.16.0 || >=16.0.0} @@ -2121,6 +2178,10 @@ packages: data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + debounce-fn@5.1.2: resolution: {integrity: sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==} engines: {node: '>=12'} @@ -2173,6 +2234,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -2398,6 +2463,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2444,6 +2513,10 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2899,6 +2972,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2932,6 +3008,10 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libsql@0.4.7: + resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} + os: [darwin, linux, win32] + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3113,6 +3193,10 @@ packages: resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} engines: {node: '>=10'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3122,6 +3206,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-sql-parser@3.9.4: resolution: {integrity: sha512-U8xa/QBpNz/dc4BERBkMg//XTrBDcj0uIg5YDYPV4ChYgHPEw4JhoT5YWTxQuKBg/3C1kfkTO4MuEYw7fCYHJw==} engines: {node: '>=8'} @@ -3360,6 +3448,9 @@ packages: prolly-trees@1.0.4: resolution: {integrity: sha512-vtnxfw5wnUHbGa0IIIk9B9DRztJWZw+t9d0s0iGxY/VzEGCg2EMl8GgGU3EhSquFLWapwbGjFTL1ipbezaXR3g==} + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + property-is-enumerable-x@2.1.2: resolution: {integrity: sha512-lRqJh4yGnwiZUx7x8abWhfucNsImqwxbFoCQzRjfwJflOBenZoFq/YMlWJShdzBmqd9i2/RrrvFW8cBs1+H+3w==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -4012,6 +4103,10 @@ packages: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -5298,11 +5393,69 @@ snapshots: '@jspm/core@2.1.0': {} + '@libsql/client@0.14.0': + dependencies: + '@libsql/core': 0.14.0 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.7.7 + libsql: 0.4.7 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.14.0': + dependencies: + js-base64: 3.7.7 + + '@libsql/darwin-arm64@0.4.7': + optional: true + + '@libsql/darwin-x64@0.4.7': + optional: true + + '@libsql/hrana-client@0.7.0': + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.7.7 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-fetch@0.3.1': {} + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.18.0 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm64-gnu@0.4.7': + optional: true + + '@libsql/linux-arm64-musl@0.4.7': + optional: true + + '@libsql/linux-x64-gnu@0.4.7': + optional: true + + '@libsql/linux-x64-musl@0.4.7': + optional: true + + '@libsql/win32-x64-msvc@0.4.7': + optional: true + '@multiformats/murmur3@2.1.8': dependencies: multiformats: 13.3.2 murmurhash3js-revisited: 3.0.0 + '@neon-rs/load@0.0.4': {} + '@netlify/blobs@8.1.1': {} '@noble/curves@1.8.1': @@ -6568,6 +6721,8 @@ snapshots: data-uri-to-buffer@2.0.2: {} + data-uri-to-buffer@4.0.1: {} + debounce-fn@5.1.2: dependencies: mimic-fn: 4.0.0 @@ -6606,6 +6761,8 @@ snapshots: delayed-stream@1.0.0: {} + detect-libc@2.0.2: {} + detect-libc@2.0.3: {} devalue@4.3.3: {} @@ -6909,6 +7066,11 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -6956,6 +7118,10 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fs-constants@1.0.0: {} fsevents@2.3.3: @@ -7518,6 +7684,8 @@ snapshots: joycon@3.1.1: {} + js-base64@3.7.7: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -7548,6 +7716,19 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libsql@0.4.7: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.4.7 + '@libsql/darwin-x64': 0.4.7 + '@libsql/linux-arm64-gnu': 0.4.7 + '@libsql/linux-arm64-musl': 0.4.7 + '@libsql/linux-x64-gnu': 0.4.7 + '@libsql/linux-x64-musl': 0.4.7 + '@libsql/win32-x64-msvc': 0.4.7 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7723,12 +7904,20 @@ snapshots: dependencies: semver: 7.7.1 + node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-sql-parser@3.9.4: dependencies: big-integer: 1.6.52 @@ -7994,6 +8183,8 @@ snapshots: bl: 4.1.0 node-sql-parser: 3.9.4 + promise-limit@2.7.0: {} + property-is-enumerable-x@2.1.2: dependencies: simple-methodize-x: 1.0.4 @@ -8730,6 +8921,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} diff --git a/setup.cloud.ts b/setup.cloud.ts index 07650fdb..fbe41c0d 100644 --- a/setup.cloud.ts +++ b/setup.cloud.ts @@ -1,10 +1,11 @@ import { BuildURI } from "@adviser/cement"; -import { registerFireproofCloudStoreProtocol } from "./src/cloud/client/gateway.ts"; -import dotenv from "dotenv"; -registerFireproofCloudStoreProtocol(); +// import { registerFireproofCloudStoreProtocol } from "./src/cloud/client/gateway.ts"; +// import dotenv from "dotenv"; -dotenv.config(); +// registerFireproofCloudStoreProtocol(); + +// dotenv.config(); process.env.FP_STORAGE_URL = BuildURI.from("fireproof://localhost:1968") // .setParam("testMode", "true") diff --git a/setup.libsql.ts b/setup.libsql.ts new file mode 100644 index 00000000..52662f0f --- /dev/null +++ b/setup.libsql.ts @@ -0,0 +1,5 @@ +import { registerSqliteStoreProtocol } from "./src/sql/gateway-sql.js"; + +registerSqliteStoreProtocol(); +process.env.FP_STORAGE_URL = "sqlite://dist/fp-dir-libsql?taste=libsql"; +process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-libsql"; diff --git a/setup.node-sqlite3-wasm.ts b/setup.node-sqlite3-wasm.ts index a39af714..c06ba69f 100644 --- a/setup.node-sqlite3-wasm.ts +++ b/setup.node-sqlite3-wasm.ts @@ -2,4 +2,4 @@ import { registerSqliteStoreProtocol } from "./src/sql/gateway-sql.js"; registerSqliteStoreProtocol(); process.env.FP_STORAGE_URL = "sqlite://dist/fp-dir-node-sqlite3-wasm?taste=node-sqlite3-wasm"; -process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-node-sqlite3-warm"; +process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-node-sqlite3-wasm"; diff --git a/src/aws/gateway.ts b/src/aws/gateway.ts index cdec3677..94e170be 100644 --- a/src/aws/gateway.ts +++ b/src/aws/gateway.ts @@ -155,7 +155,7 @@ export class AWSGateway implements bs.Gateway { return this.logger.Error().Any({ resp: done }).Msg("failed to upload meta").ResultError(); } - const doneJson = await done.json<{ uploadURL?: string }>(); + const doneJson = (await done.json()) as { uploadURL?: string }; if (!doneJson.uploadURL) { return this.logger.Error().Url(fetchUrl).Msg("Upload URL not found in the response").ResultError(); } @@ -217,7 +217,7 @@ export class AWSGateway implements bs.Gateway { return Result.Err(new NotFoundError(`data not found: ${url}`)); } - const data = new Uint8Array(await response.arrayBuffer()); + const data = to_uint8(await response.arrayBuffer()); return Result.Ok(data); } @@ -284,7 +284,7 @@ export class AWSGateway implements bs.Gateway { // console.log("Download Wal response error:", response.status); return Result.Err(new NotFoundError(`wal not found: ${url}`)); } - const data = new Uint8Array(await response.arrayBuffer()); + const data = to_uint8(await response.arrayBuffer()); return Result.Ok(data); } diff --git a/src/coerce-binary.ts b/src/coerce-binary.ts new file mode 100644 index 00000000..b76afc73 --- /dev/null +++ b/src/coerce-binary.ts @@ -0,0 +1,38 @@ +export async function top_uint8( + input: string | ArrayBuffer | ArrayBufferView | Uint8Array | SharedArrayBuffer | Blob +): Promise { + if (input instanceof Blob) { + return new Uint8Array(await input.arrayBuffer()); + } + return to_uint8(input); +} + +export function to_uint8(input: string | ArrayBuffer | ArrayBufferView | Uint8Array | SharedArrayBuffer): Uint8Array { + if (typeof input === "string") { + // eslint-disable-next-line no-restricted-globals + return new TextEncoder().encode(input); + } + if (input instanceof ArrayBuffer || input instanceof SharedArrayBuffer) { + return new Uint8Array(input); + } + + if (input instanceof Uint8Array) { + return input; + } + // not nice but we make the cloudflare types happy + return new Uint8Array(input as unknown as ArrayBuffer); +} + +export function to_blob(input: ArrayBuffer | ArrayBufferView | Uint8Array | Blob): Blob { + if (input instanceof Blob) { + return input; + } + return new Blob([to_uint8(input)]); +} + +export function to_arraybuf(input: ArrayBuffer | ArrayBufferView | Uint8Array): ArrayBuffer { + if (input instanceof ArrayBuffer) { + return input; + } + return to_uint8(input).buffer as ArrayBuffer; +} diff --git a/src/fp-cloud/backend/cf-dobj-abstract-sql.ts b/src/fp-cloud/backend/cf-dobj-abstract-sql.ts new file mode 100644 index 00000000..37e44ab6 --- /dev/null +++ b/src/fp-cloud/backend/cf-dobj-abstract-sql.ts @@ -0,0 +1,31 @@ +// import { DurableObject } from "cloudflare:workers"; +import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "../meta-merger/abstract-sql.js"; +// import { Env } from "./env.js"; +import { ExecSQLResult, FPBackendDurableObject } from "./server.js"; + +export class CFDObjSQLStatement implements SQLStatement { + readonly sql: string; + readonly db: CFDObjSQLDatabase; + constructor(db: CFDObjSQLDatabase, sql: string) { + this.db = db; + this.sql = sql; + } + async run(...params: SQLParams): Promise { + const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; + return res.rawResults[0] as T; + } + async all(...params: SQLParams): Promise { + const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; + return res.rawResults as T[]; + } +} + +export class CFDObjSQLDatabase implements SQLDatabase { + readonly dobj: DurableObjectStub; + constructor(dobj: DurableObjectStub) { + this.dobj = dobj; + } + prepare(sql: string): SQLStatement { + return new CFDObjSQLStatement(this, sql); + } +} diff --git a/src/fp-cloud/backend/cf-hono-server.ts b/src/fp-cloud/backend/cf-hono-server.ts new file mode 100644 index 00000000..c190a7bb --- /dev/null +++ b/src/fp-cloud/backend/cf-hono-server.ts @@ -0,0 +1,194 @@ +import { HttpHeader, KeyedResolvOnce, Logger, LoggerImpl, URI } from "@adviser/cement"; +import { Context, Hono } from "hono"; +import { ConnMiddleware, HonoServerFactory, RunTimeParams, HonoServerBase } from "../hono-server.js"; +import { WSContext, WSContextInit, WSEvents } from "hono/ws"; +import { buildErrorMsg, defaultGestalt, EnDeCoder, Gestalt } from "../msg-types.js"; +// import { RequestInfo as CFRequestInfo } from "@cloudflare/workers-types"; +import { defaultMsgParams, jsonEnDe } from "../msger.js"; +import { ensureLogger, ensureSuperThis, SuperThis } from "@fireproof/core"; +import { SQLDatabase } from "../meta-merger/abstract-sql.js"; +import { CFWorkerSQLDatabase } from "../meta-merger/cf-worker-abstract-sql.js"; +import { CFDObjSQLDatabase } from "./cf-dobj-abstract-sql.js"; +import { Env } from "./env.js"; +import { WSRoom } from "../ws-room.js"; +import { FPBackendDurableObject, FPRoomDurableObject } from "./server.js"; + +const startedChs = new KeyedResolvOnce(); + +export function getBackendDurableObject(env: Env) { + // console.log("getDurableObject", env); + const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; + const rany = env as unknown as Record>; + const dObjNs = rany[cfBackendKey]; + const id = dObjNs.idFromName(env.FP_BACKEND_DO_ID ?? cfBackendKey); + return dObjNs.get(id); +} + +export function getRoomDurableObject(env: Env) { + // console.log("getDurableObject", env); + const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_WS_ROOM"; + const rany = env as unknown as Record>; + const dObjNs = rany[cfBackendKey]; + const id = dObjNs.idFromName(cfBackendKey); + return dObjNs.get(id); +} + +class CFWSRoom implements WSRoom { + readonly dobj: DurableObjectStub; + constructor(dobj: DurableObjectStub) { + this.dobj = dobj; + } + async acceptConnection(ws: WebSocket, wse: WSEvents): Promise { + const ret = await this.dobj.acceptWebSocket(ws, wse); + const wsCtx = new WSContext(ws as WSContextInit); + wse.onOpen?.({} as Event, wsCtx); + // return Promise.resolve(); + // ws.accept(); + return ret; + } +} + +export class CFHonoFactory implements HonoServerFactory { + readonly _onClose: () => void; + constructor( + onClose: () => void = () => { + /* */ + } + ) { + this._onClose = onClose; + } + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { + // this._env = c.env + const sthis = ensureSuperThis({ + logger: new LoggerImpl(), + }); + sthis.env.sets(c.env); + const logger = ensureLogger(sthis, `CFHono[${URI.from(c.req.url).pathname}]`); + const ende = jsonEnDe(sthis); + // this.sthis.env. + const fpProtocol = sthis.env.get("FP_PROTOCOL"); + const msgP = defaultMsgParams(sthis, { + hasPersistent: true, + protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], + }); + const gs = defaultGestalt(msgP, { + id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", + }); + + const wsRoom = new CFWSRoom(c.env); + const cfBackendMode = c.env.CF_BACKEND_MODE && c.env.CF_BACKEND_MODE === "DURABLE_OBJECT" ? "DURABLE_OBJECT" : "D1"; + let db: SQLDatabase; + switch (cfBackendMode) { + case "DURABLE_OBJECT": { + db = new CFDObjSQLDatabase(getBackendDurableObject(c.env)); + const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + // TODO WE NEED TO START THE DURABLE OBJECT + // but then on every request we import the schema + return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs })); + } + // break; + case "D1": + default: { + const cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"; + return startedChs + .get(cfBackendKey) + .once(async () => { + db = new CFWorkerSQLDatabase(c.env[cfBackendKey] as D1Database); + const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + await chs.start(); + return chs; + }) + .then((chs) => fn({ sthis, logger, ende, impl: chs })); + } + // break; + } + // return ret; // .then((v) => sthis.logger.Flush().then(() => v)) + } + + async start(_app: Hono): Promise { + // const { upgradeWebSocket } = await import("hono/cloudflare-workers"); + // this._upgradeWebSocket = upgradeWebSocket; + } + + async serve(_app: Hono, _port?: number): Promise { + return {} as T; + } + async close(): Promise { + this._onClose(); + return; + } +} + +export class CFHonoServer extends HonoServerBase { + // _upgradeWebSocket?: UpgradeWebSocket + + readonly ende: EnDeCoder; + // readonly env: Env; + // readonly wsConnections = new Map() + constructor( + sthis: SuperThis, + logger: Logger, + ende: EnDeCoder, + gs: Gestalt, + sqlDb: SQLDatabase, + wsRoom: WSRoom, + headers?: HttpHeader + ) { + super(sthis, logger, gs, sqlDb, wsRoom, headers); + this.ende = ende; + // this.env = env; + } + + // getDurableObject(conn: Connection) { + // const id = env.FP_META_GROUPS.idFromName("fireproof"); + // const stub = env.FP_META_GROUPS.get(id); + // } + + upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { + // if (!this._upgradeWebSocket) { + // throw new Error("upgradeWebSocket not implemented"); + // } + return async (conn, c, _next) => { + const upgradeHeader = c.req.header("Upgrade"); + if (!upgradeHeader || upgradeHeader !== "websocket") { + return new Response( + this.ende.encode(buildErrorMsg(this.sthis, this.logger, {}, new Error("expected Upgrade: websocket"))), + { status: 426 } + ); + } + // const env = c.env as Env; + // const id = env.FP_META_GROUPS.idFromName([conn.key.tenant, conn.key.ledger].join(":")); + // const dObj = env.FP_META_GROUPS.get(id); + // c.env.WS_EVENTS = createEvents(c); + // return dObj.fetch(c.req.raw as unknown as CFRequestInfo) as unknown as Promise; + // this._upgradeWebSocket!(createEvents)(c, next); + + const { 0: client, 1: server } = new WebSocketPair(); + conn.attachWSPair({ client, server }); + + const wsEvents = await createEvents(c); + // console.log("upgradeWebSocket", c.req.url); + + // const wsCtx = new WSContext(server as WSContextInit); + + // server.onopen = (ev) => { + // console.log("onopen", ev); + // wsEvents.onOpen?.(ev, wsCtx); + // } + + await this.wsRoom.acceptConnection(server, wsEvents); + + // server.send("Hello from server"); + + // this.wsConnections.set(this.sthis.nextId().str, { client, server }); + // const client = webSocketPair[0], + // server = webSocketPair[1]; + + return new Response(null, { + status: 101, + webSocket: client, + }); + }; + } +} diff --git a/src/fp-cloud/backend/env.d.ts b/src/fp-cloud/backend/env.d.ts new file mode 100644 index 00000000..a3f757e1 --- /dev/null +++ b/src/fp-cloud/backend/env.d.ts @@ -0,0 +1,59 @@ +// Generated by Wrangler on Fri Aug 16 2024 13:55:06 GMT+0200 (Central European Summer Time) +// by running `wrangler types` + +import type { DurableObjectNamespace } from "@cloudflare/workers-types"; +// import { WSEvents } from "hono/ws"; +import { FPRoomDurableObject, FPBackendDurableObject } from "./server.ts"; + +export interface Env { + // bucket: R2Bucket; + // kv_store: KVNamespace; + + /** AWS/S3 access key ID for storage backend */ + ACCESS_KEY_ID: string; + ACCOUNT_ID: string; + BUCKET_NAME: string; + CLOUDFLARE_API_TOKEN: string; + EMAIL: string; + FIREPROOF_SERVICE_PRIVATE_KEY: string; + POSTMARK_TOKEN: string; + SECRET_ACCESS_KEY: string; + SERVICE_ID: string; + STORAGE_URL: string; + REGION: string; + VERSION: string; + FP_DEBUG: string; + FP_STACK: string; + FP_FORMAT: string; + FP_PROTOCOL: string; + /** Test date in ISO8601 format (YYYYMMDD'T'HHmmss'Z'). Optional. */ + TEST_DATE?: string; + /** Maximum idle time in seconds before connection timeout. Optional. */ + MAX_IDLE_TIME?: string; + + // default D1 + CF_BACKEND_MODE: "D1" | "DURABLE_OBJECT"; + // default D1 "FP_BACKEND_D1" + // default DURABLE_OBJECT "FP_BACKEND_DO" + CF_BACKEND_KEY?: string; + + FP_BACKEND_D1: D1Database; + + FP_BACKEND_DO: DurableObjectNamespace; + // default CF_BACKEND_KEY + FP_BACKEND_DO_ID: string; + + // default "FP_WS_ROOM" + CF_WS_ROOM_KEY: string; + + FP_WS_ROOM: DurableObjectNamespace; + + // WS_EVENTS: WSEvents; +} + +// declare module "cloudflare:test" { +// // ...or if you have an existing `Env` type... +// interface ProvidedEnv extends Env { +// readonly test: boolean; +// } +// } diff --git a/src/fp-cloud/backend/fp-meta-groups.ts-off b/src/fp-cloud/backend/fp-meta-groups.ts-off new file mode 100644 index 00000000..d18d3aae --- /dev/null +++ b/src/fp-cloud/backend/fp-meta-groups.ts-off @@ -0,0 +1,47 @@ +import { DurableObject } from "cloudflare:workers"; +import { Env } from "./env.js"; + +export class FPMetaGroups extends DurableObject { + currentlyConnectedWebSockets: number; + + constructor(ctx: DurableObjectState, env: Env) { + // This is reset whenever the constructor runs because + // regular WebSockets do not survive Durable Object resets. + // + // WebSockets accepted via the Hibernation API can survive + // a certain type of eviction, but we will not cover that here. + super(ctx, env); + this.currentlyConnectedWebSockets = 0; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async fetch(request: Request): Promise { + // Creates two ends of a WebSocket connection. + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + // Calling `accept()` tells the runtime that this WebSocket is to begin terminating + // request within the Durable Object. It has the effect of "accepting" the connection, + // and allowing the WebSocket to send and receive messages. + server.accept(); + this.currentlyConnectedWebSockets += 1; + + // Upon receiving a message from the client, the server replies with the same message, + // and the total number of connections with the "[Durable Object]: " prefix + // eslint-disable-next-line @typescript-eslint/no-unused-vars + server.addEventListener("message", (event: MessageEvent) => { + server.send(`[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`); + }); + + // If the client closes the connection, the runtime will close the connection too. + server.addEventListener("close", (cls: CloseEvent) => { + this.currentlyConnectedWebSockets -= 1; + server.close(cls.code, "Durable Object is closing WebSocket"); + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); + } +} diff --git a/src/fp-cloud/backend/server.ts b/src/fp-cloud/backend/server.ts new file mode 100644 index 00000000..c0f100b6 --- /dev/null +++ b/src/fp-cloud/backend/server.ts @@ -0,0 +1,72 @@ +// / +// import { Logger } from "@adviser/cement"; +// import { Hono } from "hono"; +import { DurableObject } from "cloudflare:workers"; +import { HonoServer } from "../hono-server.js"; +import { Hono } from "hono"; +import { Env } from "./env.js"; +import { CFHonoFactory } from "./cf-hono-server.js"; +import { WSContext, WSContextInit, WSEvents } from "hono/ws"; + +const app = new Hono(); +const honoServer = new HonoServer(new CFHonoFactory()); + +export default { + fetch: async (req, env, ctx): Promise => { + await honoServer.register(app); + return app.fetch(req, env, ctx); + }, +} satisfies ExportedHandler; +/* + async fetch(req, env, _ctx): Promise { + const id = env.FP_META_GROUPS.idFromName("fireproof"); + const stub = env.FP_META_GROUPS.get(id); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return stub.fetch(req as any) as unknown as Promise; + }, +} satisfies ExportedHandler; +*/ + +export interface ExecSQLResult { + readonly rowsRead: number; + readonly rowsWritten: number; + readonly rawResults: unknown[]; +} + +export class FPBackendDurableObject extends DurableObject { + async execSql(sql: string, params: unknown[]): Promise { + const cursor = await this.ctx.storage.sql.exec(sql, ...params); + const rawResults = cursor.toArray(); + const res = { + rowsRead: cursor.rowsRead, + rowsWritten: cursor.rowsWritten, + rawResults, + }; + // console.log("execSql", sql, params, res); + return res; + } +} + +export class FPRoomDurableObject extends DurableObject { + private wsEvents?: WSEvents; + + async acceptWebSocket(ws: WebSocket, wsEvents: WSEvents): Promise { + this.ctx.acceptWebSocket(ws); + this.wsEvents = wsEvents; + } + + webSocketError(ws: WebSocket, error: unknown): void | Promise { + const wsCtx = new WSContext(ws as WSContextInit); + this.wsEvents?.onError?.(error as Event, wsCtx); + } + + async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer): Promise { + const wsCtx = new WSContext(ws as WSContextInit); + this.wsEvents?.onMessage?.({ data: msg } as MessageEvent, wsCtx); + } + + webSocketClose(ws: WebSocket, code: number, reason: string): void | Promise { + const wsCtx = new WSContext(ws as WSContextInit); + this.wsEvents?.onClose?.({ code, reason } as CloseEvent, wsCtx); + } +} diff --git a/src/fp-cloud/backend/wrangler.toml b/src/fp-cloud/backend/wrangler.toml new file mode 100644 index 00000000..dfdee3a2 --- /dev/null +++ b/src/fp-cloud/backend/wrangler.toml @@ -0,0 +1,143 @@ +name = "fireproof-cloud" +main = "server.ts" +compatibility_date = "2024-04-19" +compatibility_flags = ["nodejs_compat"] +# upload_source_maps = true + +# [durable_objects] +# bindings = [ +# { name = "FP_DO", class_name = "FPDurableObject"}, +# ] + +# [[d1_databases]] +# binding = "DB" +# database_name = "test-meta-merge" +# database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + +[durable_objects] +bindings = [ + # { name = "FP_DO", class_name = "FPDurableObject", script_name = "cf-dobj-abstract-sql" } + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[[migrations]] +tag = "v1" # Should be unique for each entry +new_sqlite_classes = ["FPBackendDurableObject"] + +[observability] +enabled = true +head_sampling_rate = 1 + + +[env.test.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "http" +CF_BACKEND_MODE = "DURABLE_OBJECT" + +# [env.test.services] +# bindings = [ +# { binding = "FP_DO", service = "FP_DO" } +# ] + +[[env.test.d1_databases]] +binding = "FP_BACKEND_D1" +database_name = "test-meta-merge" +database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + +[[env.test.migrations]] +tag = "v1" # Should be unique for each entry +new_sqlite_classes = ["FPBackendDurableObject"] + +[env.test.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + + +[env.test-reqRes-D1.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "http" + +[[env.test-reqRes-D1.d1_databases]] +binding = "FP_BACKEND_D1" +database_name = "test-meta-merge" +database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + +[env.test-reqRes-D1.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[env.test-reqRes-DO.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "http" +CF_BACKEND_MODE = "DURABLE_OBJECT" + +[env.test-reqRes-DO.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[env.test-stream-D1.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "ws" + + +[env.test-stream-D1.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + +[[env.test-stream-D1.d1_databases]] +binding = "FP_BACKEND_D1" +database_name = "test-meta-merge" +database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" + + +[env.test-stream-DO.vars] +VERSION = "FP-MSG-1.0" +STORAGE_URL = "http://localhost:9000/testbucket" +ACCESS_KEY_ID = "minioadmin" +SECRET_ACCESS_KEY = "minioadmin" +FP_DEBUG = "FPMetaGroups" +#FP_FORMAT = "yaml" +# TEST_DATE = "20241121T225359Z" +FP_PROTOCOL = "ws" +CF_BACKEND_MODE = "DURABLE_OBJECT" + +[env.test-stream-DO.durable_objects] +bindings = [ + { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, + { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } +] + + diff --git a/src/fp-cloud/client/README.md b/src/fp-cloud/client/README.md new file mode 100644 index 00000000..ec73ad43 --- /dev/null +++ b/src/fp-cloud/client/README.md @@ -0,0 +1,58 @@ +# Fireproof Cloud + +This gateway intended for use with Fireproof Cloud. + +## Usage + +You can call the `connect` function with a database and it will provision a remote UUID for the database, and sync the database to the remote. It will also log a URL to the console that you can open in a browser to connect to the database, as well as try to open the URL in a new tab. Tell us what you think about this workflow! + +```typescript +import { fireproof } from "@fireproof/core"; +import { connect } from "@fireproof/cloud"; + +const database = await fireproof("my-db-name"); +const connection = await connect(database); +``` + +### With React Hooks + +In a React component, you can use the `useFireproof` hook to get the database and then call `connect` (it is safe to call `connect` multiple times, but in this example we're using a state variable to store the dashboard URL). + +```typescript +import { useFireproof } from "use-fireproof"; +import { connect } from "@fireproof/cloud"; + +const { database } = useFireproof("my-db-name"); +const [dashboardUrl, setDashboardUrl] = useState(); + +// there is a useConnection hook coming soon +useEffect(() => { + connect(database).then((connection) => { + setDashboardUrl(connection.dashboardUrl?.toString()); + }); +}, [database]); +``` + +## The Second Argument + +The second argument to `connect` is the remote database name. This will be assigned for you if you don't provide one, and the created name will be persisted locally. + +The most common way to use this is if you want to sync to a remote database. The UUID will have been assigned when on first sync, and now you want to connect a new client to that remote. + +```typescript +const connection = await connect(database, "my-remote-uuid"); +``` + +If you provide a name, it will be used as the remote database name. If you want to control the name, you should use a prefix unique to your app, so no one else uses your endpoint. This is useful if you want the database name to come from your URL slug, like `/my-app/my-db-name`. + +```typescript +const connection = await connect(database, `com.my-app.v1.${database.name}`); +``` + +Note: if your database already has data in it, connecting to a new remote will do nothing. To prevent data lost, you need to rename the local database to an unused name and the connect. + +## No Warranty, For Evaluation Purposes + +This preview of Fireproof Cloud doesn't even have login, so don't expect your data to be persisted, etc. Please give us feedback on the workflow! We'll be adding login and access control soon. + +The source of truth on this stuff is the team. Join us on [Discord](https://discord.gg/cCryrNHePH) if you want to chat! diff --git a/src/fp-cloud/client/cli-pre-signed-url.ts b/src/fp-cloud/client/cli-pre-signed-url.ts new file mode 100644 index 00000000..8419d631 --- /dev/null +++ b/src/fp-cloud/client/cli-pre-signed-url.ts @@ -0,0 +1,119 @@ +// small tool to generate pre-signed url for cloud storage +// curl $(npx tsx src/cloud/client/cli-pre-signed-url.ts GET) +// curl -X PUT --data-binary @/etc/protocols $(npx tsx src/cloud/client/cli-pre-signed-url.ts) +import { BuildURI } from "@adviser/cement"; +import { AwsClient } from "aws4fetch"; +import dotenv from "dotenv"; +import { command, run, option, oneOf, string } from "cmd-ts"; +import { ensureSuperThis } from "@fireproof/core"; +// import * as t from 'io-ts'; + +(async () => { + dotenv.config(); + const sthis = ensureSuperThis(); + const cmd = command({ + name: "cli-pre-signed-url", + description: "sign a url for cloud storage", + version: "1.0.0", + args: { + method: option({ + long: "method", + type: oneOf(["GET", "PUT", "POST", "DELETE"]), + defaultValue: () => "PUT", + defaultValueIsSerializable: true, + }), + accessKeyId: option({ + long: "accessKeyId", + type: string, + defaultValue: () => sthis.env.get("CF_ACCESS_KEY_ID") || "accessKeyId", + defaultValueIsSerializable: true, + }), + secretAccessKey: option({ + long: "secretAccessKey", + type: string, + defaultValue: () => sthis.env.get("CF_SECRET_ACCESS_KEY") || "secretAccessKey", + defaultValueIsSerializable: true, + }), + region: option({ + long: "region", + type: string, + defaultValue: () => "us-east-1", + defaultValueIsSerializable: true, + }), + service: option({ + long: "service", + type: string, + defaultValue: () => "s3", + defaultValueIsSerializable: true, + }), + storageURL: option({ + long: "storageURL", + type: string, + defaultValue: () => sthis.env.get("CF_STORAGE_URL") || "https://bucket.example.com/db/main", + defaultValueIsSerializable: true, + }), + path: option({ + long: "path", + type: string, + defaultValue: () => "db/main", + defaultValueIsSerializable: true, + }), + expires: option({ + long: "expires", + type: string, + defaultValue: () => "3600", + defaultValueIsSerializable: true, + }), + now: option({ + long: "now", + type: { + async from(str): Promise { + const decoded = new Date(str); + if (isNaN(decoded.getTime())) { + throw new Error("invalid date"); + } + // 2021-09-01T12:34:56Z + return decoded + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d+Z$/, "Z"); + }, + displayName: "WithoutMillis", + description: "without milliseconds", + }, + // 2021-09-01T12:34:56Z + // 2024-11-17T07:21:10.958Z + defaultValue: () => + new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d+Z$/, "Z"), + defaultValueIsSerializable: true, + }), + }, + handler: async (args) => { + const a4f = new AwsClient({ + accessKeyId: args.accessKeyId, + secretAccessKey: args.secretAccessKey, + region: args.region, + service: args.service, + }); + const buildUrl = BuildURI.from(args.storageURL).appendRelative(args.path).setParam("X-Amz-Expires", args.expires); + + // eslint-disable-next-line no-console + console.log( + await a4f + .sign(new Request(buildUrl.toString(), { method: args.method }), { + aws: { + signQuery: true, + datetime: args.now, + }, + }) + .then((res) => res.url) + ); + }, + }); + + await run(cmd, process.argv.slice(2)); + // eslint-disable-next-line no-console +})().catch(console.error); diff --git a/src/fp-cloud/client/cloud-gateway.test.ts b/src/fp-cloud/client/cloud-gateway.test.ts new file mode 100644 index 00000000..b30f07fe --- /dev/null +++ b/src/fp-cloud/client/cloud-gateway.test.ts @@ -0,0 +1,123 @@ +import { Hono } from "hono"; +import { HonoServer } from "../hono-server.js"; +import { defaultGestalt } from "../msg-types.js"; +import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle } from "../test-helper.js"; +import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; +import { defaultMsgParams } from "../msger.js"; +import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; +import { BuildURI } from "@adviser/cement"; + +const sthis = ensureSuperThis(); +const msgP = defaultMsgParams(sthis, { hasPersistent: true }); +const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); + +describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gateway", ({ factory }) => { + const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); + const style = wsStyle(sthis, port, msgP, my); + + let server: HonoServer; + let gw: bs.Gateway; + let unregister: () => void; + let url: BuildURI; + beforeAll(async () => { + const app = new Hono(); + server = await factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.register(app, port)); + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + gw = new FireproofCloudGateway(sthis); + url = BuildURI.from(`fireproof://localhost:${port}/`) + .setParam("protocol", "http") + .setParam("name", "ledger-name") + .setParam("tenant", "tendant"); + }); + afterAll(async () => { + await server.close(); + unregister(); + }); + describe("data", () => { + it("get not found", async () => { + await Promise.all( + Array(20) + .fill(async () => { + url.setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + + it("put - get - del - get", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const resStart = await gw.start(url.URI()); + expect(resStart.isOk()).toBeTruthy(); + + url.setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + }); + + describe("WAL", () => { + it("get not found", async () => { + await Promise.all( + Array(20) + .fill(async () => { + url.setParam("store", "wal"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + + it("put - get - del - get", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const resStart = await gw.start(url.URI()); + expect(resStart.isOk()).toBeTruthy(); + + url.setParam("store", "wal"); + const key = `theWALKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + }); +}); diff --git a/src/fp-cloud/client/gateway.ts b/src/fp-cloud/client/gateway.ts new file mode 100644 index 00000000..a221fcef --- /dev/null +++ b/src/fp-cloud/client/gateway.ts @@ -0,0 +1,605 @@ +// import PartySocket, { PartySocketOptions } from "partysocket"; +import { Result, URI, KeyedResolvOnce, exception2Result, key } from "@adviser/cement"; +import { bs, ensureLogger, Logger, NotFoundError, rt, SuperThis } from "@fireproof/core"; +import { + buildErrorMsg, + buildReqOpen, + FPStoreTypes, + HttpMethods, + MsgBase, + MsgIsError, + ReqSignedUrl, + MsgWithError, + ResSignedUrl, +} from "../msg-types.js"; +import { to_uint8 } from "../../coerce-binary.js"; +import { MsgConnected, Msger } from "../msger.js"; +import { MsgIsResGetData, MsgIsResPutData, ResDelData, ResGetData, ResPutData } from "../msg-types-data.js"; + +const VERSION = "v0.1-fp-cloud"; + +export interface StoreTypeGateway { + get(uri: URI, conn: Promise>): Promise>; + put(uri: URI, body: Uint8Array, conn: Promise>): Promise>; + delete(uri: URI, conn: Promise>): Promise>; +} + +abstract class BaseGateway { + readonly logger: Logger; + readonly sthis: SuperThis; + constructor(sthis: SuperThis, module: string) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, module); + } + + abstract getConn(uri: URI, conn: MsgConnected): Promise>; + async get(uri: URI, prConn: Promise>): Promise> { + const rConn = await prConn; + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in getConn").ResultError(); + } + const conn = rConn.Ok(); + // this.logger.Debug().Any("conn", conn.key).Msg("get"); + return this.getConn(uri, conn); + } + + abstract putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise>; + async put(uri: URI, body: Uint8Array, prConn: Promise>): Promise> { + const rConn = await prConn; + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); + } + const conn = rConn.Ok(); + // this.logger.Debug().Any("conn", conn.key).Msg("put"); + return this.putConn(uri, body, conn); + } + + abstract delConn(uri: URI, conn: MsgConnected): Promise>; + async delete(uri: URI, prConn: Promise>): Promise> { + const rConn = await prConn; + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); + } + const conn = rConn.Ok(); + // this.logger.Debug().Any("conn", conn.key).Msg("del"); + return this.delConn(uri, conn); + } + + // prepareReqSignedUrl(type: string, method: HttpMethods, store: FPStoreTypes, uri: URI, conn: Connection): Result { + + // const sig = { + // conn, + // params: { + // method, + // store, + // key: uri.getParam(" + // } satisfies ReqSignedUrl; + // return Result.Ok(buildReqSignedUrl(this.sthis, type, sig, conn)) + // } + + async getResSignedUrl( + type: string, + method: HttpMethods, + store: FPStoreTypes, + waitForFn: (msg: MsgBase) => boolean, + uri: URI, + conn: MsgConnected + ): Promise> { + const rParams = uri.getParamsResult({ + key: key.REQUIRED, + store: key.REQUIRED, + path: key.OPTIONAL, + tenant: key.REQUIRED, + name: key.REQUIRED, + index: key.OPTIONAL, + }); + if (rParams.isErr()) { + return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, rParams.Err()); + } + const params = rParams.Ok(); + if (store !== params.store) { + return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, new Error("store mismatch")); + } + const rsu = { + tid: this.sthis.nextId().str, + type, + // conn: conn.conn, + tenant: { + tenant: params.tenant, + ledger: params.name, + }, + // tenant: conn.tenant, + params: { + method, + store, + ...params, + key: params.key, + }, + version: VERSION, + } as ReqSignedUrl; + return conn.request(rsu, { waitFor: waitForFn }); + } + + async putObject(uri: URI, uploadUrl: string, body: Uint8Array): Promise> { + this.logger.Debug().Any("url", { uploadUrl, uri }).Msg("put-fetch-url"); + const rUpload = await exception2Result(async () => fetch(uploadUrl, { method: "PUT", body })); + if (rUpload.isErr()) { + return this.logger.Error().Url(uploadUrl, "uploadUrl").Err(rUpload).Msg("Error in put fetch").ResultError(); + } + if (!rUpload.Ok().ok) { + return this.logger.Error().Url(uploadUrl, "uploadUrl").Http(rUpload.Ok()).Msg("Error in put fetch").ResultError(); + } + if (uri.getParam("testMode")) { + trackPuts.add(uri.toString()); + } + return Result.Ok(undefined); + } + + async getObject(uri: URI, downloadUrl: string): Promise> { + this.logger.Debug().Any("url", { downloadUrl, uri }).Msg("get-fetch-url"); + const rDownload = await exception2Result(async () => fetch(downloadUrl.toString(), { method: "GET" })); + if (rDownload.isErr()) { + return this.logger + .Error() + .Url(downloadUrl, "uploadUrl") + .Err(rDownload) + .Msg("Error in get downloadUrl") + .ResultError(); + } + const download = rDownload.Ok(); + if (!download.ok) { + if (download.status === 404) { + return Result.Err(new NotFoundError("Not found")); + } + return this.logger.Error().Url(downloadUrl, "uploadUrl").Err(rDownload).Msg("Error in get fetch").ResultError(); + } + return Result.Ok(to_uint8(await download.arrayBuffer())); + } + + async delObject(uri: URI, deleteUrl: string): Promise> { + this.logger.Debug().Any("url", { deleteUrl, uri }).Msg("get-fetch-url"); + const rDelete = await exception2Result(async () => fetch(deleteUrl.toString(), { method: "DELETE" })); + if (rDelete.isErr()) { + return this.logger.Error().Url(deleteUrl, "deleteUrl").Err(rDelete).Msg("Error in get deleteURL").ResultError(); + } + const download = rDelete.Ok(); + if (!download.ok) { + if (download.status === 404) { + return Result.Err(new NotFoundError("Not found")); + } + return this.logger.Error().Url(deleteUrl, "deleteUrl").Err(rDelete).Msg("Error in del fetch").ResultError(); + } + return Result.Ok(undefined); + } +} + +class DataGateway extends BaseGateway implements StoreTypeGateway { + constructor(sthis: SuperThis) { + super(sthis, "DataGateway"); + } + async getConn(uri: URI, conn: MsgConnected): Promise> { + // type: string, method: HttpMethods, store: FPStoreTypes, waitForFn: + const rResSignedUrl = await this.getResSignedUrl( + "reqGetData", + "GET", + "data", + MsgIsResGetData, + uri, + conn + ); + if (MsgIsError(rResSignedUrl)) { + return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); + } + const { signedUrl: downloadUrl } = rResSignedUrl; + return this.getObject(uri, downloadUrl); + } + async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { + const rResSignedUrl = await this.getResSignedUrl( + "reqPutData", + "PUT", + "data", + MsgIsResPutData, + uri, + conn + ); + if (MsgIsError(rResSignedUrl)) { + return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); + } + const { signedUrl: uploadUrl } = rResSignedUrl; + return this.putObject(uri, uploadUrl, body); + } + async delConn(uri: URI, conn: MsgConnected): Promise> { + const rResSignedUrl = await this.getResSignedUrl( + "reqDelData", + "DELETE", + "data", + MsgIsResPutData, + uri, + conn + ); + if (MsgIsError(rResSignedUrl)) { + return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); + } + const { signedUrl: deleteUrl } = rResSignedUrl; + return this.delObject(uri, deleteUrl); + } +} + +class MetaGateway extends BaseGateway implements StoreTypeGateway { + constructor(sthis: SuperThis) { + super(sthis, "MetaGateway"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getConn(uri: URI, conn: MsgConnected): Promise> { + // const rkey = uri.getParamResult("key"); + // if (rkey.isErr()) { + // return Result.Err(rkey.Err()); + // } + // const rsu = buildReqGetMeta(this.sthis, conn.key, { + // ...conn.key, + // method: "GET", + // store: "meta", + // key: rkey.Ok(), + // }); + // const rRes = await conn.request(rsu, { + // waitType: "resGetMeta", + // }); + // if (rRes.isErr()) { + // return Result.Err(rRes.Err()); + // } + // const res = rRes.Ok(); + // if (MsgIsError(res)) { + // return Result.Err(res); + // } + // if (res.signedGetUrl) { + // return this.getObject(uri, res.signedGetUrl); + // } + // return Result.Ok(this.sthis.txt.encode(JSON.stringify(res.metas))); + return Result.Ok(new Uint8Array()); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { + // const bodyRes = Result.Ok(body); // await bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body); + // if (bodyRes.isErr()) { + // return this.logger.Error().Err(bodyRes).Msg("Error in addCryptoKeyToGatewayMetaPayload").ResultError(); + // } + // const rsu = this.prepareReqSignedUrl(uri, "PUT", conn.key); + // if (rsu.isErr()) { + // return Result.Err(rsu.Err()); + // } + // const dbMetas = JSON.parse(this.sthis.txt.decode(bodyRes.Ok())) as CRDTEntry[]; + // this.logger.Debug().Any("dbMetas", dbMetas).Msg("putMeta"); + // const req = buildReqPutMeta(this.sthis, conn.key, rsu.Ok().params, dbMetas); + // const res = await conn.request(req, { waitType: "resPutMeta" }); + // if (res.isErr()) { + // return Result.Err(res.Err()); + // } + // // console.log("putMeta", JSON.stringify({dbMetas, res})); + // this.logger.Debug().Any("qs", { req, res: res.Ok() }).Msg("putMeta"); + // this.putObject(uri, res.Ok().signedPutUrl, bodyRes.Ok()); + // return res; + return Result.Ok(undefined); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async delConn(uri: URI, conn: MsgConnected): Promise> { + // const rsu = this.prepareReqSignedUrl(uri, "DELETE", conn.key); + // if (rsu.isErr()) { + // return Result.Err(rsu.Err()); + // } + // const res = await conn.request(buildReqDelMeta(this.sthis, conn.key, rsu.Ok().params), { + // waitType: "resDelMeta", + // }); + // if (res.isErr()) { + // return Result.Err(res.Err()); + // } + // const { signedDelUrl } = res.Ok(); + // if (signedDelUrl) { + // return this.delObject(uri, signedDelUrl); + // } + // return Result.Ok(undefined); + return Result.Ok(undefined); + } +} + +class WALGateway extends BaseGateway implements StoreTypeGateway { + // WAL will not pollute to the cloud + readonly wals = new Map(); + constructor(sthis: SuperThis) { + super(sthis, "WALGateway"); + } + getWalKeyFromUri(uri: URI): Result { + const rKey = uri.getParamsResult({ + key: 0, + name: 0, + }); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + const { name, key } = rKey.Ok(); + return Result.Ok(`${name}:${key}`); + } + async getConn(uri: URI): Promise> { + const rKey = this.getWalKeyFromUri(uri); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + const wal = this.wals.get(rKey.Ok()); + if (!wal) { + return Result.Err(new NotFoundError("Not found")); + } + return Result.Ok(wal); + } + async putConn(uri: URI, body: Uint8Array): Promise> { + const rKey = this.getWalKeyFromUri(uri); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + this.wals.set(rKey.Ok(), body); + return Result.Ok(undefined); + } + async delConn(uri: URI): Promise> { + const rKey = this.getWalKeyFromUri(uri); + if (rKey.isErr()) { + return Result.Err(rKey.Err()); + } + this.wals.delete(rKey.Ok()); + return Result.Ok(undefined); + } +} + +const storeTypedGateways = new KeyedResolvOnce(); +function getStoreTypeGateway(sthis: SuperThis, uri: URI): StoreTypeGateway { + const store = uri.getParam("store"); + switch (store) { + case "data": + return storeTypedGateways.get(store).once(() => new DataGateway(sthis)); + case "meta": + return storeTypedGateways.get(store).once(() => new MetaGateway(sthis)); + case "wal": + return storeTypedGateways.get(store).once(() => new WALGateway(sthis)); + default: + throw ensureLogger(sthis, "getStoreTypeGateway") + .Error() + .Str("store", store) + .Msg("Invalid store type") + .ResultError(); + } +} + +// const keyedConnections = new KeyedResolvOnce(); +interface Subscription { + readonly sid: string; + readonly uri: string; // optimization + readonly callback: (msg: Uint8Array) => void; + readonly unsub: () => void; +} +const subscriptions = new Map(); +// const doServerSubscribe = new KeyedResolvOnce(); +const trackPuts = new Set(); +export class FireproofCloudGateway implements bs.Gateway { + readonly logger: Logger; + readonly sthis: SuperThis; + + constructor(sthis: SuperThis) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "FireproofCloudGateway", { + this: true, + }); + } + + async buildUrl(baseUrl: URI, key: string): Promise> { + return Result.Ok(baseUrl.build().setParam("key", key).URI()); + } + + async start(uri: URI): Promise> { + await this.sthis.start(); + const ret = uri.build().defParam("version", VERSION); + const rName = uri.getParamResult("name"); + if (rName.isErr()) { + return this.logger.Error().Err(rName).Msg("name not found").ResultError(); + } + ret.defParam("protocol", "wss"); + return Result.Ok(ret.URI()); + } + + async get(uri: URI): Promise { + return getStoreTypeGateway(this.sthis, uri).get(uri, this.getCloudConnection(uri)); + } + + async put(uri: URI, body: Uint8Array): Promise> { + const ret = await getStoreTypeGateway(this.sthis, uri).put(uri, body, this.getCloudConnection(uri)); + if (ret.isOk()) { + if (uri.getParam("testMode")) { + trackPuts.add(uri.toString()); + } + } + return ret; + } + + async delete(uri: URI): Promise { + trackPuts.delete(uri.toString()); + return getStoreTypeGateway(this.sthis, uri).delete(uri, this.getCloudConnection(uri)); + } + + async close(uri: URI): Promise { + const uriStr = uri.toString(); + // CAUTION here is my happen a mutation of subscriptions caused by unsub + for (const sub of Array.from(subscriptions.values())) { + for (const s of sub) { + if (s.uri.toString() === uriStr) { + s.unsub(); + } + } + } + const rConn = await this.getCloudConnection(uri); + if (rConn.isErr()) { + return this.logger.Error().Err(rConn).Msg("Error in getCloudConnection").ResultError(); + } + const conn = rConn.Ok(); + await conn.close(); + return Result.Ok(undefined); + } + + // fireproof://localhost:1999/?name=test-public-api&protocol=ws&store=meta + async getCloudConnection(uri: URI): Promise> { + const rParams = uri.getParamsResult({ + name: key.REQUIRED, + protocol: "https", + store: key.REQUIRED, + storekey: key.OPTIONAL, + tenant: key.REQUIRED, + }); + if (rParams.isErr()) { + return this.logger.Error().Url(uri).Err(rParams).Msg("getCloudConnection:err").ResultError(); + } + const params = rParams.Ok(); + // let tenant: string; + // if (params.tenant) { + // tenant = params.tenant; + // } else { + // if (!params.storekey) { + // return this.logger.Error().Url(uri).Msg("no tendant or storekey given").ResultError(); + // } + // const dataKey = params.storekey.replace(/:(meta|wal)@$/, `:data@`); + // const kb = await rt.kb.getKeyBag(this.sthis); + // const rfingerprint = await kb.getNamedKey(dataKey); + // if (rfingerprint.isErr()) { + // return this.logger.Error().Err(rfingerprint).Msg("Error in getNamedKey").ResultError(); + // } + // tenant = rfingerprint.Ok().fingerPrint; + // } + const qOpen = buildReqOpen(this.sthis, {}); + + let cUrl = uri.build().protocol(params.protocol).cleanParams().URI(); + if (cUrl.pathname === "/") { + cUrl = cUrl.build().pathname("/fp").URI(); + } + return Msger.connect(this.sthis, cUrl, qOpen); + // keyedConnections.get(keyTenantLedger(qOpen.conn.key)).once(async () => Msger.open(this.sthis, cUrl, qOpen)); + } + + // private notifySubscribers(data: Uint8Array, callbacks: ((msg: Uint8Array) => void)[] = []): void { + // for (const cb of callbacks) { + // try { + // cb(data); + // } catch (error) { + // this.logger.Error().Err(error).Msg("Error in subscriber callback execution"); + // } + // } + // } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async subscribe(uri: URI, callback: (meta: Uint8Array) => void): Promise { + return Result.Err(new Error("Not implemented")); + // const rParams = uri.getParamsResult({ + // store: 0, + // storekey: 0, + // }); + // if (rParams.isErr()) { + // return this.logger.Error().Err(rParams).Msg("Error in subscribe").ResultError(); + // } + // const { store } = rParams.Ok(); + // if (store !== "meta") { + // return Result.Err(new Error("store must be meta")); + // } + // const rConn = await this.getCloudConnection(uri); + // if (rConn.isErr()) { + // return this.logger.Error().Err(rConn).Msg("Error in subscribe:getCloudConnection").ResultError(); + // } + // const conn = rConn.Ok(); + // const rResSubscribeMeta = await doServerSubscribe.get(pkKey(conn.key)).once(async () => { + // const subId = this.sthis.nextId().str; + // const fn = (subId: string) => (msg: MsgBase) => { + // if (MsgIsUpdateMetaEvent(msg) && subId === msg.subscriberId) { + // // console.log("onMessage", subId, conn.key, msg.metas); + // const s = subscriptions.get(subId); + // if (!s) { + // return; + // } + // console.log("msg", JSON.stringify(msg)); + // this.notifySubscribers( + // this.sthis.txt.encode(JSON.stringify(msg.metas)), + // s.map((s) => s.callback) + // ); + // } + // }; + // conn.onMessage(fn(subId)); + // return conn.request(buildReqSubscriptMeta(this.sthis, conn.key, subId), { + // waitType: "resSubscribeMeta", + // }); + // }); + // if (rResSubscribeMeta.isErr()) { + // return this.logger.Error().Err(rResSubscribeMeta).Msg("Error in subscribe:request").ResultError(); + // } + // const subId = rResSubscribeMeta.Ok().subscriberId; + // let callbacks = subscriptions.get(subId); + // if (!callbacks) { + // callbacks = []; + // subscriptions.set(subId, callbacks); + // } + // const sid = this.sthis.nextId().str; + // const unsub = () => { + // const idx = callbacks.findIndex((c) => c.sid === sid); + // if (idx !== -1) { + // callbacks.splice(idx, 1); + // } + // if (callbacks.length === 0) { + // subscriptions.delete(subId); + // } + // }; + // callbacks.push({ uri: uri.toString(), callback, sid, unsub }); + // return Result.Ok(unsub); + } + + async destroy(_uri: URI): Promise> { + await Promise.all(Array.from(trackPuts).map(async (k) => this.delete(URI.from(k)))); + return Result.Ok(undefined); + } +} + +// function pkKey(set?: ConnectionKey): string { +// const ret = JSON.stringify( +// Object.entries(set || {}) +// .sort(([a], [b]) => a.localeCompare(b)) +// .filter(([k]) => k !== "id") +// .map(([k, v]) => ({ [k]: v })) +// ); +// return ret; +// } + +export class FireproofCloudTestStore implements bs.TestGateway { + readonly logger: Logger; + readonly sthis: SuperThis; + readonly gateway: bs.Gateway; + constructor(gw: bs.Gateway, sthis: SuperThis) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "FireproofCloudTestStore"); + this.gateway = gw; + } + async get(uri: URI, key: string): Promise { + const url = uri.build().setParam("key", key).URI(); + const dbFile = this.sthis.pathOps.join(rt.getPath(url, this.sthis), rt.getFileName(url, this.sthis)); + this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get"); + const buffer = await this.gateway.get(url); + this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got"); + return buffer.Ok(); + } +} + +const onceRegisterFireproofCloudStoreProtocol = new KeyedResolvOnce<() => void>(); +export function registerFireproofCloudStoreProtocol(protocol = "fireproof:", overrideBaseURL?: string) { + return onceRegisterFireproofCloudStoreProtocol.get(protocol).once(() => { + URI.protocolHasHostpart(protocol); + return bs.registerStoreProtocol({ + protocol, + overrideBaseURL, + gateway: async (sthis) => { + return new FireproofCloudGateway(sthis); + }, + test: async (sthis: SuperThis) => { + const gateway = new FireproofCloudGateway(sthis); + return new FireproofCloudTestStore(gateway, sthis); + }, + }); + }); +} diff --git a/src/fp-cloud/client/index.ts b/src/fp-cloud/client/index.ts new file mode 100644 index 00000000..48804cd3 --- /dev/null +++ b/src/fp-cloud/client/index.ts @@ -0,0 +1,125 @@ +import { BuildURI, CoerceURI, KeyedResolvOnce, runtimeFn, URI } from "@adviser/cement"; +import { bs, Database, fireproof } from "@fireproof/core"; +import { ConnectFunction, connectionFactory, makeKeyBagUrlExtractable } from "../../connection-from-store.js"; +import { registerFireproofCloudStoreProtocol } from "./gateway.js"; + +interface ConnectData { + readonly remoteName: string; + firstConnect: boolean; + endpoint?: string; +} + +const SYNC_DB_NAME = "fp_sync"; + +// Usage: +// +// import { useFireproof } from 'use-fireproof' +// import { connect } from '@fireproof/cloud' +// +// const { db } = useFireproof('test') +// +// const cx = connect(db); + +// TODO need to set the keybag url automatically + +// if (!process.env.FP_KEYBAG_URL) { +// process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-fireproof?fs=mem"; +// } + +// if (!runtimeFn().isBrowser) { +// const url = BuildURI.from(process.env.FP_KEYBAG_URL || rt.kb.defaultKeyBagUrl()); +// url.setParam("extractKey", "_deprecated_internal_api"); +// process.env.FP_KEYBAG_URL = url.toString(); +// } + +registerFireproofCloudStoreProtocol(); + +const connectionCache = new KeyedResolvOnce(); +export const rawConnect: ConnectFunction = ( + db: Database, + remoteDbName = "", + url = "fireproof://cloud.fireproof.direct" +) => { + const { sthis, blockstore, name: dbName } = db; + if (!dbName) { + throw new Error("dbName is required"); + } + const urlObj = BuildURI.from(url); + urlObj.protocol("fireproof:"); + const existingName = urlObj.getParam("name"); + urlObj.defParam("name", remoteDbName || existingName || dbName); + urlObj.defParam("localName", dbName); + urlObj.defParam("storekey", `@${dbName}:data@`); + urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/"); + // const fpUrl = urlObj + // .toString() + // .replace(/^http:\/\//, "fireproof://") + // .replace(/^https:\/\//, "fireproof://"); + // eslint-disable-next-line no-console + console.log("Config URL: " + urlObj.toString()); + return connectionCache.get(urlObj.toString()).once(() => { + makeKeyBagUrlExtractable(sthis); + const connection = connectionFactory(sthis, urlObj); + connection.connect_X(blockstore); + return connection; + }); +}; + +async function getOrCreateRemoteName(dbName: string, remoteName?: string) { + const syncDb = fireproof(SYNC_DB_NAME); + + const result = await syncDb.query("localName", { key: dbName, includeDocs: true }); + if (result.rows.length === 0) { + const doc = { + remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str, + localName: dbName, + firstConnect: !remoteName, + } as ConnectData; + const { id } = await syncDb.put(doc); + return { ...doc, _id: id }; + } + const doc = result.rows[0].doc; + return doc; +} + +export function connect( + db: Database, + remoteName?: string, + dashboardURI: CoerceURI = "https://dashboard.fireproof.storage/", + remoteURI: CoerceURI = "fireproof://cloud.fireproof.direct" +): Promise { + const dbName = db.name as string; + if (!dbName) { + throw new Error("Database name is required for cloud connection"); + } + + return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => { + if (!doc) { + throw new Error("Failed to get or create remote name"); + } + doc.endpoint = URI.from(remoteURI).toString(); + const connection = rawConnect(db, doc.remoteName, URI.from(doc.endpoint).toString()); + const connectURI = URI.from(dashboardURI).build().pathname("/fp/databases/connect"); + connectURI.defParam("localName", dbName); + connectURI.defParam("remoteName", doc.remoteName); + if (doc.endpoint) { + connectURI.defParam("endpoint", doc.endpoint); + } + // eslint-disable-next-line no-console + console.log("Fireproof Cloud: " + connectURI.toString()); + if ( + doc.firstConnect && + runtimeFn().isBrowser && + window.location.href.indexOf(URI.from(dashboardURI).toString()) === -1 + ) { + // Set firstConnect to false after opening the window, so we don't constantly annoy with the dashboard + const syncDb = fireproof(SYNC_DB_NAME); + doc.firstConnect = false; + await syncDb.put(doc); + + // window.open(connectURI.toString(), "_blank"); + } + connection.dashboardUrl = URI.from(connectURI); + return connection; + }); +} diff --git a/src/fp-cloud/cloud.test.ts-off b/src/fp-cloud/cloud.test.ts-off new file mode 100644 index 00000000..a0811ec5 --- /dev/null +++ b/src/fp-cloud/cloud.test.ts-off @@ -0,0 +1,533 @@ +// import { env } from "cloudflare:test" +import { BuildURI, Future, URI } from "@adviser/cement"; +import { ReqSignedUrl, ResSignedUrl } from "./msg-types.js"; +import { Env } from "./backend/env.js"; +import { $ } from "zx"; +import fs from "fs/promises"; +import * as toml from "smol-toml"; +import { bs, CRDTEntry, Database, ensureSuperThis, fireproof, isNotFoundError, rt } from "@fireproof/core"; +import { AwsClient } from "aws4fetch"; +import { smokeDB } from "../../tests/helper.js"; +// import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./client/gateway.ts-off"; +import { calculatePreSignedUrl } from "./pre-signed-url.js"; +import { newWebSocket } from "./new-websocket.js"; +import { registerFireproofCloudStoreProtocol } from "./client/gateway.js"; + +// function testReqSignedUrl(tid = "test") { +// return { +// tid: tid, +// type: "reqSignedUrl", +// params: { +// // protocol: "ws", +// path: "/hallo", +// store: "wal", +// key: "main", +// }, +// version: "test", +// } satisfies ReqSignedUrl; +// } + +// async function testResSignedUrl(env: Env, tid?: string, amzDate?: string): Promise { +// const req = testReqSignedUrl(tid); +// const rSignedUrl = await calculatePreSignedUrl(req, env, amzDate); +// if (rSignedUrl.isErr()) { +// throw rSignedUrl.Err(); +// } +// return { +// params: req.params, +// signedUrl: rSignedUrl.Ok().toString(), +// // `http://localhost:8080/tenantId/test-name/wal/main.json?tid=${tid}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKeyId%2F20241121%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241121T225359Z&X-Amz-Expires=86400&X-Amz-Signature=f52d5ecfbb6be93210dd57cb49ba1e426a8aee24a0738aedb636ae5722fcdded&X-Amz-SignedHeaders=host`, +// tid: tid || "test", +// type: "resSignedUrl", +// version: env.VERSION, +// } satisfies ResSignedUrl; +// } + +// describe("CloudBackendTest", () => { +// const sthis = ensureSuperThis(); +// let env: Env; +// let pid: number; +// const port = +(process.env.FP_WRANGLER_PORT || 0) || ~~(1024 + Math.random() * (0x10000 - 1024)); +// const wrangler = BuildURI.from("http://localhost") +// .port("" + port) +// .URI(); +// async function cfFetch(relative: string, init: RequestInit) { +// return fetch(wrangler.build().appendRelative(relative).asURL(), init); +// } +// beforeAll(async () => { +// const tomlFile = "src/cloud/backend/wrangler.toml"; +// const tomeStr = await fs.readFile(tomlFile, "utf-8"); +// const wranglerFile = toml.parse(tomeStr) as unknown as { +// env: { test: { vars: Env } }; +// }; +// env = wranglerFile.env.test.vars; +// if (process.env.FP_WRANGLER_PORT) { +// return; +// } +// $.verbose = !!process.env.FP_DEBUG; +// const runningWrangler = $` +// wrangler dev -c ${tomlFile} --port ${port} --env test --no-show-interactive-dev-session & +// waitPid=$! +// echo "PID:$waitPid" +// wait $waitPid`; +// const waitReady = new Future(); +// runningWrangler.stdout.on("data", (chunk) => { +// // console.log(">>", chunk.toString()) +// const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; +// if (mightPid) { +// pid = +mightPid; +// } +// if (chunk.includes("Ready on http")) { +// waitReady.resolve(true); +// } +// }); +// runningWrangler.stderr.on("data", (chunk) => { +// // eslint-disable-next-line no-console +// console.error("!!", chunk.toString()); +// }); +// await waitReady.asPromise(); +// // await f.asPromise() +// // wrangler dev -c src/cloud/backend/wrangler.toml --port 4711 --env test +// }); + +// afterAll(async () => { +// // console.log("kill", runningWrangler.pid, runningWrangler) +// // process.kill(runningWrangler.pid) +// // process.stdin.write(Array(4).fill("x\n\r").join("")) +// if (pid) process.kill(pid); +// }); + +// describe("raw tests", () => { +// it("return 404", async () => { +// const res = await cfFetch("/posts", {}); +// expect(res.status).toBe(404); +// expect(await res.json()).toEqual({ +// message: "Notfound:/posts", +// tid: "internal", +// type: "error", +// version: env.VERSION, +// }); +// }); +// it("return 422 invalid json", async () => { +// const res = await cfFetch("/fp", { method: "PUT" }); +// expect(res.status).toBe(422); +// expect(await res.json()).toEqual({ +// message: "Unexpected end of JSON input", +// tid: "internal", +// type: "error", +// version: env.VERSION, +// }); +// }); + +// it("return 422 illegal msg", async () => { +// const res = await cfFetch("/fp", { +// method: "PUT", +// body: JSON.stringify({ +// bucket: "test", +// key: "test", +// }), +// }); +// expect(res.status).toBe(422); +// expect(await res.json()).toEqual({ +// message: "unknown msg.type=undefined", +// tid: "internal", +// type: "error", +// version: env.VERSION, +// }); +// }); + +// it("return 200 msg", async () => { +// const res = await cfFetch("/fp", { +// method: "PUT", +// body: JSON.stringify(testReqSignedUrl()), +// }); +// expect(res.status).toBe(200); +// expect(await res.json()).toEqual(await testResSignedUrl(env)); +// }); + +// // it("reqOpen without websocket", async () => { +// // const conn = await msgOpen(cfURL, { } +// // }); + +// // it("reqOpen with websocket", async () => { +// // }); + +// it("use websockets SignedUrl", async () => { +// await Promise.all( +// Array(100) +// .fill(null) +// .map(async () => { +// const url = wrangler.build().appendRelative("/ws").protocol("ws:"); +// const so = await newWebSocket(url); +// const done = new Future(); +// let total = 10; +// let tid = `${total}-test-${Math.random()}`; +// so.onopen = () => { +// so.send(JSON.stringify(testReqSignedUrl(tid))); +// }; +// so.onmessage = async (msg) => { +// try { +// const res = JSON.parse(msg.data.toString()) as ResSignedUrl; +// expect(res).toEqual(await testResSignedUrl(env, tid, URI.from(res.signedUrl).getParam("X-Amz-Date"))); +// if (--total === 0) { +// done.resolve(true); +// } else { +// tid = `${total}-test-${Math.random()}`; +// so.send(JSON.stringify(testReqSignedUrl(tid))); +// } +// } catch (err) { +// done.reject(err); +// } +// }; +// so.onerror = (ev) => { +// assert.fail(`WebSocket error: ${ev}`); +// }; +// return done.asPromise().then(() => so.close(1000, "done")); +// }) +// ); +// }); +// }); + +describe("FireproofCloudGateway", () => { + let db: Database; + let unregister: () => void; + interface ExtendedGateway extends bs.Gateway { + headerSize: number; + subscribe?: (url: URI, callback: (meta: Uint8Array) => void) => Promise; // Changed VoidResult to UnsubscribeResult + } + + // has to leave + interface ExtendedStore { + gateway: ExtendedGateway; + _url: URI; + name: string; + } + + beforeAll(() => { + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + }); + + beforeEach(() => { + const config = { + store: { + stores: { + base: wrangler.build().protocol("fireproof:").setParam("protocol", "ws").setParam("testMode", "true"), + // process.env.FP_STORAGE_URL, // || "fireproof://localhost:1968", + }, + }, + }; + const name = "fireproof-cloud-test-db-" + sthis.nextId().str; + db = fireproof(name, config); + }); + + afterEach(async () => { + // Clear the database before each test + if (db) { + await db.close(); + await db.destroy(); + } + }); + + afterAll(() => { + unregister(); + }); + + // it("env setup is ok", () => { + // // expect(process.env.FP_STORAGE_URL).toMatch(/fireproof:\/\/localhost:1999/); + // }); + + it("should have loader and options", () => { + const loader = db.blockstore.loader; + expect(loader).toBeDefined(); + if (!loader) { + throw new Error("Loader is not defined"); + } + expect(loader.ebOpts).toBeDefined(); + expect(loader.ebOpts.store).toBeDefined(); + expect(loader.ebOpts.store.stores).toBeDefined(); + if (!loader.ebOpts.store.stores) { + throw new Error("Loader stores is not defined"); + } + if (!loader.ebOpts.store.stores.base) { + throw new Error("Loader stores.base is not defined"); + } + + const baseUrl = URI.from(loader.ebOpts.store.stores.base); + expect(baseUrl.protocol).toBe("fireproof:"); + // expect(baseUrl.hostname).toBe("localhost"); + // expect(baseUrl.port || "").toBe("1999"); + }); + + it("should initialize and perform basic operations", async () => { + const docs = await smokeDB(db); + + // // get a new db instance + // db = new Database(name, config); + + // Test update operation + const updateDoc = await db.get<{ content: string }>(docs[0]._id); + updateDoc.content = "Updated content"; + const updateResult = await db.put(updateDoc); + expect(updateResult.id).toBe(updateDoc._id); + + const updatedDoc = await db.get<{ content: string }>(updateDoc._id); + expect(updatedDoc.content).toBe("Updated content"); + + // Test delete operation + await db.del(updateDoc._id); + try { + await db.get(updateDoc._id); + throw new Error("Document should have been deleted"); + } catch (e) { + const error = e as Error; + expect(error.message).toContain("Not found"); + } + }); + + it("should subscribe to changes", async () => { + // Extract stores from the loader + const metaStore = (await db.blockstore.loader?.metaStore()) as unknown as ExtendedStore; + + const metaGateway = metaStore?.gateway; + + const metaUrl = await metaGateway?.buildUrl(metaStore?._url, "main"); + await metaGateway?.start(metaStore?._url); + + let didCall = false; + + expect(metaGateway.subscribe).toBeTypeOf("function"); + if (metaGateway.subscribe) { + const future = new Future(); + + const metaSubscribeResult = await metaGateway.subscribe(metaUrl?.Ok(), (data: Uint8Array) => { + // console.log("data", data); + const decodedData = sthis.txt.decode(data); + expect(decodedData).toContain("parents"); + didCall = true; + future.resolve(); + }); + expect(metaSubscribeResult.isOk()).toBeTruthy(); + const ok = await db.put({ _id: "key1", hello: "world1" }); + expect(ok).toBeTruthy(); + expect(ok.id).toBe("key1"); + await future.asPromise(); + expect(didCall).toBeTruthy(); + metaSubscribeResult.Ok()(); + } + }); +}); +describe("AwsClient R2", () => { + it("make presigned url", async () => { + const sthis = ensureSuperThis(); + const a4f = new AwsClient({ + accessKeyId: sthis.env.get("CF_ACCESS_KEY_ID") || "accessKeyId", + secretAccessKey: sthis.env.get("CF_SECRET_ACCESS_KEY") || "secretAccessKey", + region: "us-east-1", + service: "s3", + }); + const buildUrl = BuildURI.from(sthis.env.get("CF_STORAGE_URL") || "https://bucket.example.com/db/main") + .appendRelative("db/main") + .setParam("X-Amz-Expires", "22"); + const signedUrl = await a4f + .sign(new Request(buildUrl.toString(), { method: "PUT" }), { + aws: { + signQuery: true, + datetime: "2021-09-01T12:34:56Z", + }, + }) + .then((res) => res.url); + expect(URI.from(signedUrl).asObj()).toEqual( + buildUrl + .setParam("X-Amz-Date", "2021-09-01T12:34:56Z") + .setParam("X-Amz-Algorithm", "AWS4-HMAC-SHA256") + .setParam("X-Amz-Credential", `${a4f.accessKeyId}/2021-09-/${a4f.region}/${a4f.service}/aws4_request`) + .setParam("X-Amz-SignedHeaders", "host") + .setParam( + "X-Amz-Signature", + sthis.env.get("CF_PRESIGNED_SIGNATURE") || "bbae4604fbe51a4ce9972183d8871a8a187ab0f4d2415afd6dc728f8ccc9900f" + ) + .asObj() + ); + }); +}); + +describe(`store=meta`, () => { + const store = "meta"; + let gw: bs.Gateway; + const sthis = ensureSuperThis(); + let uri: URI; + beforeAll(async () => { + gw = new FireproofCloudGateway(sthis); + const id = sthis.nextId().str; + uri = BuildURI.from("fireproof://localhost") + .port("" + port) + .setParam("store", store) + .setParam("name", id) + .setParam("protocol", "ws") + .setParam("storekey", id) + .setParam("testMode", "true") + .URI(); + + const last: Uint8Array[] = []; + const cnt = 4; + Array(cnt) + .fill(null) + .map(async () => { + const rOk = (await gw.subscribe?.(uri, (meta: Uint8Array) => { + last.push(meta); + if (last.length === cnt) { + expect(last[0]).toEqual(last[1]); + expect(last[1]).toEqual(last[2]); + expect(last[2]).toEqual(last[3]); + last.length = 0; + } + })) as bs.VoidResult; + expect(rOk.isOk()).toBeTruthy(); + }); + + const keyBag = await rt.kb.getKeyBag(sthis); + await keyBag.getNamedKey(`@${id}:data@`); + }); + + afterAll(async () => { + const rOk = await gw.close(uri); + expect(rOk.isOk()).toBeTruthy(); + }); + + const subscribeCallbacks: { + connId: string; + uri: URI; + cb: ReturnType void>>; + unsub: bs.UnsubscribeResult; + }[] = []; + beforeEach(async () => { + await Promise.all( + Array(1) + .fill(null) + .map(async () => { + const cb = vitest.fn(); + const connId = sthis.nextId().str; + const uriConnId = uri.build().setParam("connId", connId).URI(); + const unsub = (await gw.subscribe?.(uriConnId, (meta: Uint8Array) => + cb(sthis.txt.decode(meta), connId) + )) as bs.UnsubscribeResult; + subscribeCallbacks.push({ cb, unsub, connId, uri: uriConnId }); + }) + ); + }); + afterEach(() => { + subscribeCallbacks.forEach(({ unsub }) => unsub.Ok()()); + subscribeCallbacks.length = 0; + }); + + function crdtEntry(connId = "default"): Uint8Array { + return sthis.txt.encode( + JSON.stringify([ + { + cid: `${connId}:bafyreidjlylxmmb3yuz7levzzbso3g7ql54zovxl3mkhbbqxmmfnfbkoym`, + data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWdvdHM3dmFzeHhhdmdoY3FjeHo3ZXJibTdtY21ramQybTV0bXpzcGdhbG91d2lpcjYzZnkifV19Z3BhcmVudHOA", + parents: [], + }, + { + cid: `${connId}:bafyreie7izpgpmxd6heoiweoyblgyzoxt74xrp5wcpqo66bmjv2plgmceq`, + data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWQyZ2l1c2t2YWJoZTZ5ZHdsdXo0aGx4Z3lyNTZ5dmZmbjVpdndqdmhlYXl3cWJ4bHFmeGEifV19Z3BhcmVudHOA", + parents: [], + }, + ] satisfies CRDTEntry[]) + ); + } + + it(`buildUrl`, async () => { + const rOk = await gw.buildUrl(uri, "KEY"); + const url = rOk.Ok(); + expect(url.getParam("store")).toBe(store); + expect(url.getParam("key")).toBe("KEY"); + }); + it(`start`, async () => { + const rOk = await gw.start(uri); + const url = rOk.Ok(); + expect(url.getParam("store")).toBe(store); + expect(url.getParam("version")).toBeTruthy(); + }); + + it(`unsubscribe`, async () => { + subscribeCallbacks.forEach((sub) => sub.unsub.Ok()()); + const rOk = await gw.put(uri.build().setParam("key", "main").URI(), crdtEntry()); + // console.log(rOk); + expect(rOk.isOk()).toBeTruthy(); + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + }); + + it(`get-put-delete`, async () => { + async function getNotFound() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.get(u.build().setParam("key", key).URI()); + expect(rOk.isErr()).toBeTruthy(); + expect(isNotFoundError(rOk.Err())).toBeTruthy(); + } + } + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + } + console.log("getNotFound-pre"); + + subscribeCallbacks.forEach(({ cb }) => { + expect(cb).toHaveBeenCalledTimes(0); + }); + // get not found + await getNotFound(); + // put + async function put() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.put( + u.build().setParam("key", key).URI(), + crdtEntry(`${key}:${u.getParam("connId", "default")}`) + ); + expect(rOk.isOk()).toBeTruthy(); + } + } + // console.log('put', subscribeCallbacks.map(({ cb }) => cb.mock.calls)); + subscribeCallbacks.forEach(({ cb, connId }) => { + // expect(cb).toHaveBeenCalledTimes(subscribeCallbacks.length * 2); + for (const key of ["KEY1", "KEY2"]) { + expect(cb).toHaveBeenCalledWith(sthis.txt.decode(crdtEntry(`${key}:${connId}`)), connId); + } + }); + } + // console.log('put-pre') + await put(); + subscribeCallbacks.forEach(({ cb }) => cb.mockClear()); + + async function get() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.get(u.build().setParam("key", key).URI()); + const data = JSON.parse(sthis.txt.decode(rOk.Ok())) as CRDTEntry[]; + expect(data).toEqual(subscribeCallbacks.map(({ connId }) => crdtEntry(connId))); + } + } + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + } + console.log("get-pre"); + await get(); + async function del() { + for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { + for (const key of ["KEY1", "KEY2"]) { + const rOk = await gw.delete(u.build().setParam("key", key).URI()); + expect(rOk.isOk()).toBeTruthy(); + } + } + subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); + } + console.log("del-pre"); + await del(); + // get not found + console.log("getNotFound-pre"); + await getNotFound(); + }); + it(`close`, async () => { + const rOk = await gw.close(uri); + expect(rOk.isOk()).toBeTruthy(); + }); +}); diff --git a/src/fp-cloud/connection.test.ts b/src/fp-cloud/connection.test.ts new file mode 100644 index 00000000..43c4fa3a --- /dev/null +++ b/src/fp-cloud/connection.test.ts @@ -0,0 +1,362 @@ +import { ensureSuperThis } from "@fireproof/core"; +import { URI } from "@adviser/cement"; +import { + buildReqGestalt, + buildReqOpen, + MsgIsError, + MsgIsResGestalt, + MsgIsResOpen, + defaultGestalt, + ReqSignedUrlParam, + GwCtx, + MsgWithError, + ResOptionalSignedUrl, +} from "./msg-types.js"; +import { + MsgIsResGetData, + MsgIsResPutData, + MsgIsResDelData, + buildReqPutData, + buildReqDelData, + buildReqGetData, +} from "./msg-types-data.js"; +import { + buildReqGetWAL, + buildReqPutWAL, + buildReqDelWAL, + MsgIsResGetWAL, + MsgIsResPutWAL, + MsgIsResDelWAL, +} from "./msg-types-wal.js"; +import { applyStart, defaultMsgParams, MsgConnected, Msger } from "./msger.js"; +import { HonoServer } from "./hono-server.js"; +import { Hono } from "hono"; +import { calculatePreSignedUrl } from "./pre-signed-url.js"; +import { CFHonoServerFactory, httpStyle, NodeHonoServerFactory, resolveToml, wsStyle } from "./test-helper.js"; +import { + buildReqDelMeta, + buildBindGetMeta, + buildReqPutMeta, + MsgIsResDelMeta, + ResDelMeta, + ReqDelMeta, + BindGetMeta, + EventGetMeta, + MsgIsEventGetMeta, + MsgIsResPutMeta, +} from "./msg-type-meta.js"; + +async function refURL(sp: ResOptionalSignedUrl) { + const { env } = await resolveToml("D1"); + return ( + await calculatePreSignedUrl(sp, { + storageUrl: URI.from(env.STORAGE_URL), + aws: { + accessKeyId: env.ACCESS_KEY_ID, + secretAccessKey: env.SECRET_ACCESS_KEY, + region: env.REGION, + }, + test: { + amzDate: URI.from(sp.signedUrl).getParam("X-Amz-Date"), + }, + }) + ) + .Ok() + .asObj(); +} + +describe("Connection", () => { + const sthis = ensureSuperThis(); + const msgP = defaultMsgParams(sthis, { hasPersistent: true }); + + beforeAll(async () => { + sthis.env.sets((await resolveToml("D1")).env as unknown as Record); + }); + + describe.each([NodeHonoServerFactory(), CFHonoServerFactory("DO"), CFHonoServerFactory("D1")])( + "$name - Connection", + (honoServer) => { + const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); + const qOpen = buildReqOpen(sthis, { reqId: "req-open-test" }); + const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); + describe.each([httpStyle(sthis, port, msgP, my), wsStyle(sthis, port, msgP, my)])( + `${honoServer.name} - $name`, + (style) => { + let server: HonoServer; + beforeAll(async () => { + const app = new Hono(); + server = await honoServer + .factory(sthis, msgP, style.remoteGestalt, port) + .then((srv) => srv.register(app, port)); + }); + afterAll(async () => { + // console.log("closing server"); + await server.close(); + }); + it(`conn refused`, async () => { + const rC = await applyStart(style.connRefused.open()); + expect(rC.isErr()).toBeTruthy(); + expect(rC.Err().message).toMatch(/ECONNREFUSED/); + }); + + it(`timeout`, async () => { + const rC = await applyStart(style.timeout.open()); + expect(rC.isErr()).toBeTruthy(); + expect(rC.Err().message).toMatch(/Timeout/i); + }); + + describe(`connection`, () => { + let c: MsgConnected; + beforeEach(async () => { + const rC = await style.ok.open().then((r) => MsgConnected.connect(r, { reqId: "req-open-testx" })); + expect(rC.isOk()).toBeTruthy(); + c = rC.Ok(); + expect(c.conn).toEqual({ + reqId: "req-open-testx", + resId: c.conn.resId, + }); + }); + afterEach(async () => { + await c.close(); + }); + + it("kaputt url http", async () => { + const r = await c.raw.request( + { + tid: "test", + type: "kaputt", + version: "FP-MSG-1.0", + }, + { waitFor: () => true } + ); + if (!MsgIsError(r)) { + assert.fail("expected MsgError"); + return; + } + expect(r).toEqual({ + message: "unexpected message", + tid: "test", + type: "error", + version: "FP-MSG-1.0", + src: { + tid: "test", + type: "kaputt", + version: "FP-MSG-1.0", + }, + }); + }); + it("gestalt url http", async () => { + const msgP = defaultMsgParams(sthis, {}); + const req = buildReqGestalt(sthis, defaultGestalt(msgP, { id: "test" })); + const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); + if (!MsgIsResGestalt(r)) { + assert.fail("expected MsgError", JSON.stringify(r)); + } + expect(r.gestalt).toEqual(c.exchangedGestalt?.remote); + }); + + it("openConnection", async () => { + const req = buildReqOpen(sthis, { ...c.conn }); + const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); + if (!MsgIsResOpen(r)) { + assert.fail(JSON.stringify(r)); + } + expect(r).toEqual({ + conn: { ...c.conn, resId: r.conn?.resId }, + tid: req.tid, + type: "resOpen", + version: "FP-MSG-1.0", + }); + }); + }); + + it("open", async () => { + const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, { + reqId: "req-open-testy", + }); + expect(rC.isOk()).toBeTruthy(); + const c = rC.Ok(); + expect(c.conn).toEqual({ + reqId: "req-open-testy", + resId: c.conn.resId, + }); + expect(c.raw).toBeInstanceOf(style.cInstance); + expect(c.exchangedGestalt).toEqual({ + my, + remote: style.remoteGestalt, + }); + await c.close(); + }); + describe(`${honoServer.name} - Msgs`, () => { + let gwCtx: GwCtx; + let conn: MsgConnected; + beforeAll(async () => { + const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); + expect(rC.isOk()).toBeTruthy(); + conn = rC.Ok(); + gwCtx = { + conn: conn.conn, + tenant: { + tenant: "Tenant", + ledger: "Ledger", + }, + }; + }); + afterAll(async () => { + await conn.close(); + }); + it("Open", async () => { + const res = await conn.raw.request(buildReqOpen(sthis, conn.conn), { waitFor: MsgIsResOpen }); + if (!MsgIsResOpen(res)) { + assert.fail("expected MsgResOpen", JSON.stringify(res)); + } + expect(MsgIsResOpen(res)).toBeTruthy(); + expect(res.conn).toEqual({ ...qOpen.conn, resId: res.conn.resId }); + }); + + function sup() { + return { + path: "test/me", + key: "key-test", + } satisfies ReqSignedUrlParam; + } + describe("Data", async () => { + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildReqGetData(sthis, sp, gwCtx), { waitFor: MsgIsResGetData }); + if (MsgIsResGetData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResGetData", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const res = await conn.request(buildReqPutData(sthis, sp, gwCtx), { waitFor: MsgIsResPutData }); + if (MsgIsResPutData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResPutData", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelData(sthis, sp, gwCtx), { waitFor: MsgIsResDelData }); + if (MsgIsResDelData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelData", JSON.stringify(res)); + } + }); + }); + + describe("Meta", async () => { + it("bind stop", async () => { + const sp = sup(); + expect(conn.raw.activeBinds.size).toBe(0); + const streams: ReadableStream>[] = Array(5) + .fill(0) + .map(() => { + return conn.bind(buildBindGetMeta(sthis, sp, gwCtx), { + waitFor: MsgIsEventGetMeta, + }); + }); + for await (const stream of streams) { + const reader = stream.getReader(); + while (true) { + const { done, value: msg } = await reader.read(); + if (done) { + break; + } + if (MsgIsEventGetMeta(msg)) { + // expect(msg.params).toEqual(sp); + expect(URI.from(msg.signedUrl).asObj()).toEqual(await refURL(msg)); + } else { + assert.fail("expected MsgEventGetMeta", JSON.stringify(msg)); + } + await reader.cancel(); + } + } + expect(conn.raw.activeBinds.size).toBe(0); + // await Promise.all(streams.map((s) => s.cancel())); + }); + + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildBindGetMeta(sthis, sp, gwCtx), { waitFor: MsgIsEventGetMeta }); + if (MsgIsEventGetMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgIsEventGetMeta", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const metas = Array(5) + .fill({ cid: "x", parents: [], data: "MomRkYXRho" }) + .map((data) => { + return { ...data, cid: sthis.timeOrderedNextId().str }; + }); + const res = await conn.request(buildReqPutMeta(sthis, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta }); + if (MsgIsResPutMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgIsResPutMeta", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelMeta(sthis, sp, gwCtx), { + waitFor: MsgIsResDelMeta, + }); + if (MsgIsResDelMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + } + }); + }); + describe("WAL", async () => { + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildReqGetWAL(sthis, sp, gwCtx), { waitFor: MsgIsResGetWAL }); + if (MsgIsResGetWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResGetWAL", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const res = await conn.request(buildReqPutWAL(sthis, sp, gwCtx), { waitFor: MsgIsResPutWAL }); + if (MsgIsResPutWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResPutWAL", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelWAL(sthis, sp, gwCtx), { waitFor: MsgIsResDelWAL }); + if (MsgIsResDelWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + } + }); + }); + }); + } + ); + } + ); +}); diff --git a/src/fp-cloud/hono-server.ts b/src/fp-cloud/hono-server.ts new file mode 100644 index 00000000..b04fe3b1 --- /dev/null +++ b/src/fp-cloud/hono-server.ts @@ -0,0 +1,265 @@ +import { exception2Result, HttpHeader, param, ResolveOnce, Result, URI } from "@adviser/cement"; +import { Logger, SuperThis } from "@fireproof/core"; +import { Context, Hono, Next } from "hono"; +import { top_uint8 } from "../coerce-binary.js"; +import { + Gestalt, + buildErrorMsg, + MsgBase, + EnDeCoder, + ErrorMsg, + MsgWithError, + buildRes, + MsgWithConn, + GwCtx, + MsgIsError, +} from "./msg-types.js"; +import { MsgDispatcher, WSConnection } from "./msg-dispatch.js"; +import { WSEvents } from "hono/ws"; +import { calculatePreSignedUrl, PreSignedMsg } from "./pre-signed-url.js"; +import { buildMsgDispatcher } from "./msg-dispatcher-impl.js"; +import { + BindGetMeta, + buildEventGetMeta, + buildResDelMeta, + buildResPutMeta, + EventGetMeta, + ReqDelMeta, + ReqPutMeta, + ResDelMeta, + ResPutMeta, +} from "./msg-type-meta.js"; +import { MetaMerger } from "./meta-merger/meta-merger.js"; +import { SQLDatabase } from "./meta-merger/abstract-sql.js"; +import { WSRoom } from "./ws-room.js"; + +export interface RunTimeParams { + readonly sthis: SuperThis; + readonly logger: Logger; + readonly ende: EnDeCoder; + readonly impl: HonoServerImpl; +} +// eslint-disable-next-line @typescript-eslint/no-invalid-void-type +export type ConnMiddleware = (conn: WSConnection, c: Context, next: Next) => Promise; +export interface HonoServerImpl { + start(): Promise; + gestalt(): Gestalt; + calculatePreSignedUrl(p: PreSignedMsg): Promise>; + upgradeWebSocket: (createEvents: (c: Context) => WSEvents | Promise) => ConnMiddleware; + handleBindGetMeta(sthis: SuperThis, logger: Logger, msg: BindGetMeta): Promise>; + handleReqPutMeta(sthis: SuperThis, logger: Logger, msg: ReqPutMeta): Promise>; + handleReqDelMeta(sthis: SuperThis, logger: Logger, msg: ReqDelMeta): Promise>; + readonly headers: HttpHeader; +} + +export abstract class HonoServerBase implements HonoServerImpl { + readonly _gs: Gestalt; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly metaMerger: MetaMerger; + readonly headers: HttpHeader; + readonly wsRoom: WSRoom; + constructor(sthis: SuperThis, logger: Logger, gs: Gestalt, sqlDb: SQLDatabase, wsRoom: WSRoom, headers?: HttpHeader) { + this.logger = logger; + this._gs = gs; + this.sthis = sthis; + this.wsRoom = wsRoom; + this.metaMerger = new MetaMerger(sqlDb); + this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); + } + + abstract upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware; + + start(drop = false): Promise { + return this.metaMerger.createSchema(drop).then(() => this); + } + + gestalt(): Gestalt { + return this._gs; + } + + async handleReqPutMeta( + sthis: SuperThis, + logger: Logger, + msg: MsgWithConn + ): Promise> { + const rUrl = await buildRes("PUT", "meta", "resPutMeta", sthis, logger, msg, this); + if (MsgIsError(rUrl)) { + return rUrl; + } + await this.metaMerger.addMeta({ + logger, + connection: msg, + metas: msg.metas, + }); + return buildResPutMeta(sthis, logger, msg, { ...rUrl, metas: await this.metaMerger.metaToSend(msg) }); + } + + async handleReqDelMeta( + sthis: SuperThis, + logger: Logger, + msg: MsgWithConn + ): Promise> { + const rUrl = await buildRes("DELETE", "meta", "resDelMeta", sthis, logger, msg, this); + if (MsgIsError(rUrl)) { + return rUrl; + } + await this.metaMerger.delMeta({ + logger, + connection: msg, + }); + return buildResDelMeta(sthis, logger, msg, rUrl.signedUrl); + } + + async handleBindGetMeta( + sthis: SuperThis, + logger: Logger, + msg: MsgWithConn, + gwCtx: GwCtx = msg + ): Promise> { + const rUrl = await buildRes("GET", "meta", "eventGetMeta", sthis, logger, msg, this); + if (MsgIsError(rUrl)) { + return rUrl; + } + return buildEventGetMeta( + sthis, + logger, + msg, + { + ...rUrl, + metas: await this.metaMerger.metaToSend(msg), + }, + gwCtx + ); + } + + calculatePreSignedUrl(p: PreSignedMsg): Promise> { + const rRes = this.sthis.env.gets({ + STORAGE_URL: param.REQUIRED, + ACCESS_KEY_ID: param.REQUIRED, + SECRET_ACCESS_KEY: param.REQUIRED, + REGION: "us-east-1", + }); + if (rRes.isErr()) { + return Promise.resolve(Result.Err(rRes.Err())); + } + const res = rRes.Ok(); + return calculatePreSignedUrl(p, { + storageUrl: URI.from(res.STORAGE_URL), + aws: { + accessKeyId: res.ACCESS_KEY_ID, + secretAccessKey: res.SECRET_ACCESS_KEY, + region: res.REGION, + }, + }); + } +} + +export interface HonoServerFactory { + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise; + + start(app: Hono): Promise; + serve(app: Hono, port?: number): Promise; + close(): Promise; +} + +export const CORS = HttpHeader.from({ + // "Accept": "application/json", + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET,POST,OPTIONS,PUT,DELETE", + "Access-Control-Max-Age": "86400", // Cache pre-flight response for 24 hours +}); + +export class HonoServer { + // readonly sthis: SuperThis; + // readonly msgP: MsgerParams; + // readonly gestalt: Gestalt; + // readonly logger: Logger; + readonly factory: HonoServerFactory; + constructor(/* sthis: SuperThis, msgP: MsgerParams, gestalt: Gestalt, */ factory: HonoServerFactory) { + // this.sthis = sthis; + // this.logger = ensureLogger(sthis, "HonoServer"); + // this.msgP = msgP; + // this.gestalt = gestalt; + this.factory = factory; + } + readonly _register = new ResolveOnce(); + async register(app: Hono, port?: number): Promise { + return this._register.once(async () => { + await this.factory.start(app); + // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) + // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) + app.put("/fp", (c) => + this.factory.inject(c, async ({ sthis, logger, impl }) => { + impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); + const rMsg = await exception2Result(() => c.req.json() as Promise); + if (rMsg.isErr()) { + c.status(400); + return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); + } + const dispatcher = buildMsgDispatcher(sthis, impl.gestalt()); + return dispatcher.dispatch(impl, rMsg.Ok(), (msg) => c.json(msg)); + }) + ); + app.get("/ws", (c, next) => + this.factory.inject(c, async ({ sthis, logger, ende, impl }) => { + return impl.upgradeWebSocket((_c) => { + let dp: MsgDispatcher; + return { + onOpen: (_e, _ws) => { + dp = buildMsgDispatcher(sthis, impl.gestalt()); + }, + onError: (error) => { + logger.Error().Err(error).Msg("WebSocket error"); + }, + onMessage: async (event, ws) => { + const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); + if (rMsg.isErr()) { + ws.send( + ende.encode( + buildErrorMsg( + sthis, + logger, + { + message: event.data, + } as ErrorMsg, + rMsg.Err() + ) + ) + ); + } else { + await dp.dispatch(impl, rMsg.Ok(), (msg) => { + const str = ende.encode(msg); + ws.send(str); + return new Response(str); + }); + } + }, + onClose: () => { + dp = undefined as unknown as MsgDispatcher; + // console.log('Connection closed') + }, + }; + })(new WSConnection(), c, next); + }) + ); + await this.factory.serve(app, port); + return this; + }); + } + async close() { + const ret = await this.factory.close(); + return ret; + } +} + +// export async function honoServer(_sthis: SuperThis, _msgP: MsgerParams, _gestalt: Gestalt) { +// const rt = runtimeFn(); +// if (rt.isNodeIsh) { +// // const { NodeHonoServer } = await import("./node-hono-server.js"); +// // return new HonoServer(sthis, msgP, gestalt, new NodeHonoServer()); +// } +// throw new Error("Not implemented"); +// } diff --git a/src/fp-cloud/http-connection.ts b/src/fp-cloud/http-connection.ts new file mode 100644 index 00000000..17a07b34 --- /dev/null +++ b/src/fp-cloud/http-connection.ts @@ -0,0 +1,178 @@ +import { HttpHeader, Logger, Result, URI, exception2Result } from "@adviser/cement"; +import { SuperThis, ensureLogger } from "@fireproof/core"; +import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError } from "./msg-types.js"; +import { + ActiveStream, + ExchangedGestalt, + MsgerParamsWithEnDe, + MsgRawConnection, + OnMsgFn, + selectRandom, + timeout, + UnReg, +} from "./msger.js"; +import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; + +export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnection { + readonly logger: Logger; + readonly msgP: MsgerParamsWithEnDe; + + readonly baseURIs: URI[]; + + readonly #onMsg = new Map(); + + constructor(sthis: SuperThis, uris: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { + super(sthis, exGestalt); + this.logger = ensureLogger(sthis, "HttpConnection"); + // this.msgParam = msgP; + this.baseURIs = uris; + this.msgP = msgP; + } + + async start(): Promise> { + // if (this._qsOpen.req) { + // const sOpen = await this.request(this._qsOpen.req, { waitFor: MsgIsResOpen }); + // if (!MsgIsResOpen(sOpen)) { + // return Result.Err(this.logger.Error().Any("Err", sOpen).Msg("unexpected response").AsError()); + // } + // this._qsOpen.res = sOpen; + // } + return Result.Ok(undefined); + } + + async close(): Promise> { + await Promise.all(Array.from(this.activeBinds.values()).map((state) => state.controller?.close())); + this.#onMsg.clear(); + return Result.Ok(undefined); + } + + toMsg(msg: MsgWithError): MsgWithError { + this.#onMsg.forEach((fn) => fn(msg)); + return msg; + } + + onMsg(fn: OnMsgFn): UnReg { + const key = this.sthis.nextId().str; + this.#onMsg.set(key, fn); + return () => this.#onMsg.delete(key); + } + + #poll(state: ActiveStream): void { + this.request(state.bind.msg, state.bind.opts) + .then((msg) => { + try { + state.controller?.enqueue(msg); + if (MsgIsError(msg)) { + state.controller?.close(); + } else { + state.timeout = setTimeout(() => this.#poll(state), state.bind.opts.pollInterval ?? 1000); + } + } catch (err) { + console.log("poll error", err); + state.controller?.error(err); + state.controller?.close(); + } + }) + .catch((err) => { + console.log("poll catch error", err); + state.controller?.error(err); + // state.controller?.close(); + }); + } + + readonly activeBinds = new Map>(); + bind(req: Q, opts: RequestOpts): ReadableStream> { + const state: ActiveStream = { + id: this.sthis.nextId().str, + bind: { + msg: req, + opts, + }, + } satisfies ActiveStream; + this.activeBinds.set(state.id, state); + return new ReadableStream>({ + cancel: () => { + clearTimeout(state.timeout as number); + this.activeBinds.delete(state.id); + }, + start: (controller) => { + state.controller = controller; + this.#poll(state); + }, + }); + } + + async request(req: Q, _opts: RequestOpts): Promise> { + const headers = HttpHeader.from(); + headers.Set("Content-Type", this.msgP.mime); + headers.Set("Accept", this.msgP.mime); + + const rReqBody = exception2Result(() => this.msgP.ende.encode(req)); + if (rReqBody.isErr()) { + return this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + req, + this.logger.Error().Err(rReqBody.Err()).Any("req", req).Msg("encode error").AsError() + ) + ); + } + headers.Set("Content-Length", rReqBody.Ok().byteLength.toString()); + const url = selectRandom(this.baseURIs); + this.logger.Debug().Url(url).Any("body", req).Msg("request"); + const rRes = await exception2Result(() => + timeout( + this.msgP.timeout, + fetch(url.toString(), { + method: "PUT", + headers: headers.AsHeaderInit(), + body: rReqBody.Ok(), + }) + ) + ); + this.logger.Debug().Url(url).Any("body", rRes).Msg("response"); + if (rRes.isErr()) { + return this.toMsg( + buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Err(rRes).Msg("fetch error").AsError()) + ); + } + const res = rRes.Ok(); + if (!res.ok) { + return this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + req, + this.logger + .Error() + .Url(url) + .Str("status", res.status.toString()) + .Str("statusText", res.statusText) + .Msg("HTTP Error") + .AsError(), + await res.text() + ) + ); + } + const data = new Uint8Array(await res.arrayBuffer()); + const ret = await exception2Result(async () => this.msgP.ende.decode(data) as S); + if (ret.isErr()) { + return this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + req, + this.logger.Error().Err(ret.Err()).Msg("decode error").AsError(), + this.sthis.txt.decode(data) + ) + ); + } + return this.toMsg(ret.Ok()); + } + + // toOnMessage(msg: WithErrorMsg): Result> { + // this.mec.msgFn?.(msg as unknown as MessageEvent); + // return Result.Ok(msg); + // } +} diff --git a/src/fp-cloud/meta-merger/abstract-sql.ts b/src/fp-cloud/meta-merger/abstract-sql.ts new file mode 100644 index 00000000..445c5d2a --- /dev/null +++ b/src/fp-cloud/meta-merger/abstract-sql.ts @@ -0,0 +1,53 @@ +// import { RunResult } from "better-sqlite3"; + +// export function now() { +// return new Date().toISOString(); +// } + +// export interface SqlLiteStmt { +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// bind(...args: any[]): any; +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// run(...args: any[]): Promise; +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// get(...args: any[]): Promise; +// } + +// export interface SqlLite { +// prepare(sql: string): SqlLiteStmt; +// } + +// export interface SqlLiteDBDialect { + +// } + +// export type SQLLiteFlavor = BaseSQLiteDatabase<'async', unknown>; + +export interface SQLDatabase { + prepare(sql: string): SQLStatement; +} + +export type SQLParams = (string | number | Date)[]; + +// export type SQLRow = Record; + +export interface SQLStatement { + run(...params: SQLParams): Promise; + all(...params: SQLParams): Promise; +} + +export function conditionalDrop(drop: boolean, tabName: string, create: string): string[] { + if (!drop) { + return [create]; + } + return [`DROP TABLE IF EXISTS ${tabName}`, create]; +} + +export function sqliteCoerceParams(params: SQLParams): (string | number)[] { + return params.map((i) => { + if (i instanceof Date) { + return i.toISOString(); + } + return i; + }); +} diff --git a/src/fp-cloud/meta-merger/bettersql-abstract-sql.ts b/src/fp-cloud/meta-merger/bettersql-abstract-sql.ts new file mode 100644 index 00000000..e28bf869 --- /dev/null +++ b/src/fp-cloud/meta-merger/bettersql-abstract-sql.ts @@ -0,0 +1,36 @@ +import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "./abstract-sql.js"; + +import Database from "better-sqlite3"; + +export class BetterSQLStatement implements SQLStatement { + readonly stmt: Database.Statement; + constructor(stmt: Database.Statement) { + this.stmt = stmt; + } + + async run(...iparams: SQLParams): Promise { + const res = (await this.stmt.run(...sqliteCoerceParams(iparams))) as T; + // console.log("run", res); + return res; + } + async all(...params: SQLParams): Promise { + const res = (await this.stmt.all(...sqliteCoerceParams(params))) as T[]; + // console.log("all", res); + return res; + } +} + +export class BetterSQLDatabase implements SQLDatabase { + readonly db: Database.Database; + constructor(dbOrPath: Database.Database | string) { + if (typeof dbOrPath === "string") { + this.db = new Database(dbOrPath); + } else { + this.db = dbOrPath; + } + } + + prepare(sql: string): SQLStatement { + return new BetterSQLStatement(this.db.prepare(sql)); + } +} diff --git a/src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts b/src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts new file mode 100644 index 00000000..f1cabf1e --- /dev/null +++ b/src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts @@ -0,0 +1,31 @@ +import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "./abstract-sql.js"; + +import type { D1Database } from "@cloudflare/workers-types"; + +export class CFWorkerSQLStatement implements SQLStatement { + readonly stmt: D1PreparedStatement; + constructor(stmt: D1PreparedStatement) { + this.stmt = stmt; + } + + async run(...iparams: SQLParams): Promise { + const bound = this.stmt.bind(...sqliteCoerceParams(iparams)); + // console.log("cf-run", sqliteCoerceParams(iparams), bound); + return bound.run() as T; + } + async all(...params: SQLParams): Promise { + const rows = await this.stmt.bind(...sqliteCoerceParams(params)).run(); + return rows.results as T[]; + } +} + +export class CFWorkerSQLDatabase implements SQLDatabase { + readonly db: D1Database; + constructor(db: D1Database) { + this.db = db; + } + + prepare(sql: string): SQLStatement { + return new CFWorkerSQLStatement(this.db.prepare(sql)); + } +} diff --git a/src/fp-cloud/meta-merger/create-schema-cli.ts b/src/fp-cloud/meta-merger/create-schema-cli.ts new file mode 100644 index 00000000..7ebd858e --- /dev/null +++ b/src/fp-cloud/meta-merger/create-schema-cli.ts @@ -0,0 +1,9 @@ +import { MetaSendSql } from "./meta-send.js"; + +async function main() { + // eslint-disable-next-line no-console + console.log(MetaSendSql.schema(true).join(";\n")); +} + +// eslint-disable-next-line no-console +main().catch(console.error); diff --git a/src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts b/src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts new file mode 100644 index 00000000..b8f0e94b --- /dev/null +++ b/src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts @@ -0,0 +1,173 @@ +import { ResolveOnce } from "@adviser/cement"; +import { CRDTEntry } from "@fireproof/core"; +import { TenantLedgerSql } from "./tenant-ledger.js"; +import { ByConnection } from "./meta-merger.js"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; + +export interface MetaByTenantLedgerRow { + readonly tenant: string; + readonly ledger: string; + readonly reqId: string; + readonly resId: string; + readonly metaCID: string; + readonly meta: CRDTEntry; + readonly updateAt: Date; +} + +interface SQLMetaByTenantLedgerRow { + readonly tenant: string; + readonly ledger: string; + readonly reqId: string; + readonly resId: string; + readonly metaCID: string; + readonly meta: string; + readonly updateAt: string; +} + +/* +SELECT * FROM Mitarbeiter e1 +WHERE NOT EXISTS +( + SELECT 1 FROM Mitarbeiter e2 + WHERE e1.employee_id=e2.employee_id und e2.employee_name LIKE 'A%' +); + */ + +export class MetaByTenantLedgerSql { + static schema(drop = false) { + return [ + ...TenantLedgerSql.schema(drop), + ...conditionalDrop( + drop, + "MetaByTenantLedger", + ` + CREATE TABLE IF NOT EXISTS MetaByTenantLedger( + tenant TEXT NOT NULL, + ledger TEXT NOT NULL, + reqId TEXT NOT NULL, + resId TEXT NOT NULL, + metaCID TEXT NOT NULL, + meta TEXT NOT NULL, + updatedAt TEXT NOT NULL, + PRIMARY KEY (tenant, ledger, reqId, resId, metaCID), + UNIQUE(metaCID), + FOREIGN KEY (tenant, ledger) REFERENCES TenantLedger(tenant, ledger) + ) + ` + ), + ]; + } + + readonly db: SQLDatabase; + readonly tenantLedgerSql: TenantLedgerSql; + constructor(db: SQLDatabase, tenantLedgerSql: TenantLedgerSql) { + this.db = db; + this.tenantLedgerSql = tenantLedgerSql; + } + + readonly #sqlCreateMetaByTenantLedger = new ResolveOnce(); + sqlCreateMetaByTenantLedger(): SQLStatement[] { + return this.#sqlCreateMetaByTenantLedger.once(() => { + return MetaByTenantLedgerSql.schema().map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertMetaByTenantLedger = new ResolveOnce(); + sqlEnsureMetaByTenantLedger(): SQLStatement { + return this.#sqlInsertMetaByTenantLedger.once(() => { + return this.db.prepare(` + INSERT INTO MetaByTenantLedger(tenant, ledger, reqId, resId, metaCID, meta, updatedAt) + SELECT ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS ( + SELECT 1 FROM MetaByTenantLedger WHERE metaCID = ? + ) + `); + }); + } + + readonly #sqlDeleteByConnection = new ResolveOnce(); + sqlDeleteByConnection(): SQLStatement { + return this.#sqlDeleteByConnection.once(() => { + return this.db.prepare(` + DELETE FROM MetaByTenantLedger + WHERE + tenant = ? + AND + ledger = ? + AND + reqId = ? + AND + resId = ? + AND + metaCID NOT IN (SELECT value FROM json_each(?)) + `); + }); + } + + /* + * select * from MetaByTenantLedger where tenant = 'tenant' and ledger = 'ledger' group by metaCID + */ + + // readonly #sqlSelectByMetaCIDs = new ResolveOnce() + // sqlSelectByMetaCIDs(): Statement { + // return this.#sqlSelectByMetaCIDs.once(() => { + // return this.db.prepare(` + // SELECT tenant, ledger, reqId, resId, metaCID, meta, updatedAt + // FROM MetaByTenantLedger + // WHERE metaCID in ? + // `); + // }) + // } + // async selectByMetaCIDs(metaCIDs: string[]): Promise { + // const stmt = this.sqlSelectByMetaCIDs(); + // const rows = await stmt.all(metaCIDs) + // return rows.map(row => ({ + // ...row, + // meta: JSON.parse(row.meta), + // updateAt: new Date(row.updateAt) + // } satisfies MetaByTenantLedgerRow)) + // } + + async deleteByConnection(t: ByConnection & { metaCIDs: string[] }) { + const stmt = this.sqlDeleteByConnection(); + return stmt.run(t.tenant, t.ledger, t.reqId, t.resId, JSON.stringify(t.metaCIDs)); + } + + async ensure(t: MetaByTenantLedgerRow) { + const stmt = this.sqlEnsureMetaByTenantLedger(); + return stmt.run( + t.tenant, + t.ledger, + t.reqId, + t.resId, + t.metaCID, + JSON.stringify(t.meta), + t.updateAt.toISOString(), + t.metaCID + ); + } + + readonly #sqlSelectByConnection = new ResolveOnce(); + sqlSelectByConnection(): SQLStatement { + return this.#sqlSelectByConnection.once(() => { + return this.db.prepare(` + SELECT tenant, ledger, reqId, resId, metaCID, meta, updatedAt + FROM MetaByTenantLedger + WHERE tenant = ? AND ledger = ? AND reqId = ? AND resId = ? + ORDER BY updatedAt + `); + }); + } + + async selectByConnection(conn: ByConnection): Promise { + const stmt = this.sqlSelectByConnection(); + const rows = await stmt.all(conn.tenant, conn.ledger, conn.reqId, conn.resId); + return rows.map( + (row) => + ({ + ...row, + meta: JSON.parse(row.meta), + updateAt: new Date(row.updateAt), + }) satisfies MetaByTenantLedgerRow + ); + } +} diff --git a/src/fp-cloud/meta-merger/meta-merger.test.ts b/src/fp-cloud/meta-merger/meta-merger.test.ts new file mode 100644 index 00000000..621715eb --- /dev/null +++ b/src/fp-cloud/meta-merger/meta-merger.test.ts @@ -0,0 +1,245 @@ +// import type { Database } from "better-sqlite3"; +import { Connection, MetaMerger } from "./meta-merger.js"; +import { CRDTEntry, ensureSuperThis } from "@fireproof/core"; +import { runtimeFn } from "@adviser/cement"; +import { SQLDatabase } from "./abstract-sql.js"; +import type { Env } from "../backend/env.js"; +import { getBackendDurableObject } from "../backend/cf-hono-server.js"; + +function sortCRDTEntries(rows: CRDTEntry[]) { + return rows.sort((a, b) => a.cid.localeCompare(b.cid)); +} + +interface MetaConnection { + readonly metas: CRDTEntry[]; + readonly connection: Connection; +} + +function toCRDTEntries(rows: MetaConnection[]) { + return rows.reduce((r, i) => [...r, ...i.metas], [] as CRDTEntry[]); +} + +// function filterConnection(ref: MetaConnection[], connection: Connection) { +// return toCRDTEntries(ref.filter((r) => +// (r.connection.tenant.tenant === connection.tenant.tenant && +// r.connection.tenant.ledger === connection.tenant.ledger && +// r.connection.conn.reqId === connection.conn.reqId && +// r.connection.conn.resId === connection.conn.resId))) +// } + +function getSQLFlavours(): { name: string; factory: () => Promise }[] { + if (runtimeFn().isCFWorker) { + return [ + { + name: "cf-worker-d1", + factory: async () => { + const { CFWorkerSQLDatabase } = await import("./cf-worker-abstract-sql.js"); + const { env } = await import("cloudflare:test"); + return new CFWorkerSQLDatabase((env as Env).FP_BACKEND_D1); + }, + }, + { + name: "cf-worker-do", + factory: async () => { + const { CFDObjSQLDatabase } = await import("../backend/cf-dobj-abstract-sql.js"); + const { env } = await import("cloudflare:test"); + return new CFDObjSQLDatabase(getBackendDurableObject(env as Env)); + }, + }, + ]; + } else { + return [ + { + name: "bettersql", + factory: async () => { + const { BetterSQLDatabase } = await import("./bettersql-abstract-sql.js"); + return new BetterSQLDatabase("./dist/test.db"); + }, + }, + ]; + } +} + +describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { + // let db: SQLDatabase; + const sthis = ensureSuperThis(); + const logger = sthis.logger; + let mm: MetaMerger; + beforeAll(async () => { + // db = new Database(':memory:'); + const db = await flavour.factory(); + mm = new MetaMerger(db); + await mm.createSchema(); + }); + + let connection: Connection; + beforeEach(() => { + connection = { + tenant: { + tenant: `tenant${sthis.timeOrderedNextId().str}`, + ledger: "ledger", + }, + conn: { + reqId: "reqId", + resId: `resId-${sthis.timeOrderedNextId().str}`, + }, + } satisfies Connection; + }); + + afterEach(async () => { + await mm.delMeta({ + logger, + connection, + }); + }); + + it("insert nothing", async () => { + await mm.addMeta({ + logger, + connection, + metas: [], + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + expect(rows).toEqual([]); + }); + + it("insert one multiple", async () => { + const cid = sthis.timeOrderedNextId().str; + for (let i = 0; i < 10; i++) { + const metas = Array(i).fill({ + cid: cid, + parents: [], + data: "MomRkYXRho", + }); + await mm.addMeta({ + logger, + connection, + metas, + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + if (i === 1) { + expect(rows).toEqual(metas); + } else { + expect(rows).toEqual([]); + } + } + }); + + it("insert multiple", async () => { + const conns = []; + for (let i = 0; i < 10; i++) { + const metas = Array(i) + .fill({ + cid: "x", + parents: [], + data: "MomRkYXRho", + }) + .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); + const conn = { + ...connection.conn, + reqId: sthis.timeOrderedNextId().str, + }; + conns.push(conn); + await mm.addMeta({ + logger, + connection: { + ...connection, + conn, + } satisfies Connection, + metas, + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(metas)); + } + await Promise.all( + conns.map(async (conn) => + mm.delMeta({ + logger, + connection: { ...connection, conn }, + metas: [], + }) + ) + ); + }); + + it("metaToSend to sink", async () => { + const connections = Array(2) + .fill(connection) + .map((c) => ({ ...c, conn: { ...c.conn, reqId: sthis.timeOrderedNextId().str } })); + const ref: MetaConnection[] = []; + for (const connection of connections) { + const metas = Array(2) + .fill({ + cid: "x", + parents: [], + data: "MomRkYXRho", + }) + .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); + ref.push({ metas, connection }); + await mm.addMeta({ + logger, + connection, + metas, + now: new Date(), + }); + } + // wrote 10 connections with 3 metas each + for (const connection of connections) { + const rows = await mm.metaToSend(connection); + expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(toCRDTEntries(ref))); + const rowsEmpty = await mm.metaToSend(connection); + expect(sortCRDTEntries(rowsEmpty)).toEqual([]); + } + const newConnections = Array(2) + .fill(connection) + .map((c) => ({ ...c, conn: { ...c.conn, reqId: sthis.timeOrderedNextId().str } })); + for (const connection of newConnections) { + const rows = await mm.metaToSend(connection); + expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(toCRDTEntries(ref))); + const rowsEmpty = await mm.metaToSend(connection); + expect(sortCRDTEntries(rowsEmpty)).toEqual([]); + } + await Promise.all( + connections.map(async (connection) => + mm.delMeta({ + logger, + connection, + metas: [], + }) + ) + ); + }); + + it("delMeta", async () => { + await mm.addMeta({ + logger, + connection, + metas: [ + { + cid: `del-${sthis.timeOrderedNextId().str}`, + parents: [], + data: "MomRkYXRho", + }, + { + cid: `del-${sthis.timeOrderedNextId().str}`, + parents: [], + data: "MomRkYXRho", + }, + ], + now: new Date(), + }); + const rows = await mm.metaToSend(connection); + expect(rows.length).toBe(2); + await mm.delMeta({ + logger, + connection, + metas: rows, + now: new Date(), + }); + const rowsDel = await mm.metaToSend(connection); + expect(rowsDel.length).toBe(0); + }); +}); diff --git a/src/fp-cloud/meta-merger/meta-merger.ts b/src/fp-cloud/meta-merger/meta-merger.ts new file mode 100644 index 00000000..5b7a62d5 --- /dev/null +++ b/src/fp-cloud/meta-merger/meta-merger.ts @@ -0,0 +1,116 @@ +import { CRDTEntry, Logger } from "@fireproof/core"; +import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; +import { MetaSendSql } from "./meta-send.js"; +import { TenantLedgerSql } from "./tenant-ledger.js"; +import { TenantSql } from "./tenant.js"; +import { SQLDatabase } from "./abstract-sql.js"; +import { QSId, TenantLedger } from "../msg-types.js"; + +export interface Connection { + readonly tenant: TenantLedger; + readonly conn: QSId; +} + +export interface MetaMerge { + readonly logger: Logger; + readonly connection: Connection; + readonly metas: CRDTEntry[]; + readonly now?: Date; +} + +export interface ByConnection { + readonly tenant: string; + readonly ledger: string; + readonly reqId: string; + readonly resId: string; +} + +function toByConnection(connection: Connection): ByConnection { + return { + ...connection.conn, + ...connection.tenant, + }; +} + +export class MetaMerger { + readonly db: SQLDatabase; + // readonly sthis: SuperThis; + readonly sql: { + readonly tenant: TenantSql; + readonly tenantLedger: TenantLedgerSql; + readonly metaByTenantLedger: MetaByTenantLedgerSql; + readonly metaSend: MetaSendSql; + }; + + constructor(db: SQLDatabase) { + this.db = db; + // this.sthis = sthis; + const tenant = new TenantSql(db); + const tenantLedger = new TenantLedgerSql(db, tenant); + this.sql = { + tenant, + tenantLedger, + metaByTenantLedger: new MetaByTenantLedgerSql(db, tenantLedger), + metaSend: new MetaSendSql(db), + }; + } + + async createSchema(drop = false) { + for (const i of this.sql.metaSend.sqlCreateMetaSend(drop)) { + await i.run(); + } + } + + async delMeta( + mm: Omit & { readonly metas?: CRDTEntry[] } + ): Promise<{ now: Date; byConnection: ByConnection }> { + const now = mm.now || new Date(); + const byConnection = toByConnection(mm.connection); + const metaCIDs = (mm.metas ?? []).map((meta) => meta.cid); + const connCIDs = { + ...byConnection, + // needs something with is not empty to delete + metaCIDs: metaCIDs.length ? metaCIDs : [new Date().toISOString()], + }; + await this.sql.metaSend.deleteByConnection(connCIDs); + await this.sql.metaByTenantLedger.deleteByConnection(connCIDs); + return { now, byConnection }; + } + + async addMeta(mm: MetaMerge) { + if (!mm.metas.length) { + return; + } + const { now, byConnection } = await this.delMeta(mm); + await this.sql.tenantLedger.ensure({ + ...mm.connection.tenant, + createdAt: now, + }); + for (const meta of mm.metas) { + try { + await this.sql.metaByTenantLedger.ensure({ + ...byConnection, + metaCID: meta.cid, + meta: meta, + updateAt: now, + }); + } catch (e) { + mm.logger.Warn().Err(e).Str("metaCID", meta.cid).Msg("addMeta"); + } + } + } + + async metaToSend(sink: Connection, now = new Date()): Promise { + const bySink = toByConnection(sink); + const rows = await this.sql.metaSend.selectToAddSend({ ...bySink, now }); + await this.sql.metaSend.insert( + rows.map((row) => ({ + metaCID: row.metaCID, + reqId: row.reqId, + resId: row.resId, + sendAt: row.sendAt, + })) + ); + return rows.map((row) => row.meta); + } +} diff --git a/src/fp-cloud/meta-merger/meta-send.ts b/src/fp-cloud/meta-merger/meta-send.ts new file mode 100644 index 00000000..2debc00a --- /dev/null +++ b/src/fp-cloud/meta-merger/meta-send.ts @@ -0,0 +1,128 @@ +import { ResolveOnce } from "@adviser/cement"; +import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; +import { ByConnection } from "./meta-merger.js"; +import { CRDTEntry } from "@fireproof/core"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; + +export interface MetaSendRow { + readonly metaCID: string; + readonly reqId: string; + readonly resId: string; + readonly sendAt: Date; +} + +type SQLMetaSendRowWithMeta = MetaSendRow & { meta: string }; +export type MetaSendRowWithMeta = MetaSendRow & { meta: CRDTEntry }; + +export class MetaSendSql { + static schema(drop = false) { + return [ + ...MetaByTenantLedgerSql.schema(drop), + ...conditionalDrop( + drop, + "MetaSend", + ` + CREATE TABLE IF NOT EXISTS MetaSend ( + metaCID TEXT NOT NULL, + reqId TEXT NOT NULL, + resId TEXT NOT NULL, + sendAt TEXT NOT NULL, + PRIMARY KEY(metaCID,reqId,resId), + FOREIGN KEY(metaCID) REFERENCES MetaByTenantLedger(metaCID) + ); + ` + ), + ]; + } + + readonly db: SQLDatabase; + constructor(db: SQLDatabase) { + this.db = db; + } + + readonly #sqlCreateMetaSend = new ResolveOnce(); + sqlCreateMetaSend(drop: boolean): SQLStatement[] { + return this.#sqlCreateMetaSend.once(() => { + return MetaSendSql.schema(drop).map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertMetaSend = new ResolveOnce(); + sqlInsertMetaSend(): SQLStatement { + return this.#sqlInsertMetaSend.once(() => { + return this.db.prepare(` + INSERT INTO MetaSend(metaCID, reqId, resId, sendAt) VALUES(?, ?, ?, ?) + `); + }); + } + + readonly #sqlSelectToAddSend = new ResolveOnce(); + sqlSelectToAddSend(): SQLStatement { + return this.#sqlSelectToAddSend.once(() => { + return this.db.prepare(` + SELECT t.metaCID, ? as reqId, ? as resId, ? as sendAt, t.meta FROM MetaByTenantLedger as t + WHERE + t.tenant = ? + AND + t.ledger = ? + AND + NOT EXISTS (SELECT 1 FROM MetaSend AS s WHERE t.metaCID = s.metaCID and s.reqId = ? and s.resId = ?) + `); + }); + } + + async selectToAddSend(conn: ByConnection & { now: Date }): Promise { + const stmt = this.sqlSelectToAddSend(); + const rows = await stmt.all( + conn.reqId, + conn.resId, + conn.now, + conn.tenant, + conn.ledger, + conn.reqId, + conn.resId + ); + return rows.map( + (i) => + ({ + metaCID: i.metaCID, + reqId: i.reqId, + resId: i.resId, + sendAt: new Date(i.sendAt), + meta: JSON.parse(i.meta) as CRDTEntry, + }) satisfies MetaSendRowWithMeta + ); + } + + async insert(t: MetaSendRow[]) { + const stmt = this.sqlInsertMetaSend(); + for (const i of t) { + await stmt.run(i.metaCID, i.reqId, i.resId, i.sendAt.toISOString()); + } + } + + readonly #sqlDeleteByConnection = new ResolveOnce(); + sqlDeleteByMetaCID(): SQLStatement { + return this.#sqlDeleteByConnection.once(() => { + return this.db.prepare(` + DELETE FROM MetaSend + WHERE metaCID in (SELECT metaCID FROM MetaByTenantLedger + WHERE + tenant = ? + AND + ledger = ? + AND + reqId = ? + AND + resId = ? + AND + metaCID NOT IN (SELECT value FROM json_each(?))) + `); + }); + } + + async deleteByConnection(dmi: ByConnection & { metaCIDs: string[] }) { + const stmt = this.sqlDeleteByMetaCID(); + return stmt.run(dmi.tenant, dmi.ledger, dmi.reqId, dmi.resId, JSON.stringify(dmi.metaCIDs)); + } +} diff --git a/src/fp-cloud/meta-merger/tenant-ledger.ts b/src/fp-cloud/meta-merger/tenant-ledger.ts new file mode 100644 index 00000000..b5dc5fa7 --- /dev/null +++ b/src/fp-cloud/meta-merger/tenant-ledger.ts @@ -0,0 +1,62 @@ +import { ResolveOnce } from "@adviser/cement"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; +import { TenantSql } from "./tenant.js"; + +export interface TenantLedgerRow { + readonly tenant: string; + readonly ledger: string; + readonly createdAt: Date; +} + +export class TenantLedgerSql { + static schema(drop = false) { + return [ + ...TenantSql.schema(drop), + ...conditionalDrop( + drop, + "TenantLedger", + ` + CREATE TABLE IF NOT EXISTS TenantLedger( + tenant TEXT NOT NULL, + ledger TEXT NOT NULL, + createdAt TEXT NOT NULL, + PRIMARY KEY(tenant, ledger), + FOREIGN KEY(tenant) REFERENCES Tenant(tenant) + ) + ` + ), + ]; + } + + readonly db: SQLDatabase; + readonly tenantSql: TenantSql; + constructor(db: SQLDatabase, tenantSql: TenantSql) { + this.db = db; + this.tenantSql = tenantSql; + } + + readonly #sqlCreateTenantLedger = new ResolveOnce(); + sqlCreateTenantLedger(): SQLStatement[] { + return this.#sqlCreateTenantLedger.once(() => { + return TenantLedgerSql.schema().map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertTenantLedger = new ResolveOnce(); + sqlEnsureTenantLedger(): SQLStatement { + return this.#sqlInsertTenantLedger.once(() => { + return this.db.prepare(` + INSERT INTO TenantLedger(tenant, ledger, createdAt) + SELECT ?, ?, ? WHERE + NOT EXISTS(SELECT 1 FROM TenantLedger WHERE tenant = ? and ledger = ?) + `); + }); + } + + async ensure(t: TenantLedgerRow) { + await this.tenantSql.ensure({ tenant: t.tenant, createdAt: t.createdAt }); + const stmt = this.sqlEnsureTenantLedger(); + const ret = stmt.run(t.tenant, t.ledger, t.createdAt, t.tenant, t.ledger); + return ret; + } +} diff --git a/src/fp-cloud/meta-merger/tenant.ts b/src/fp-cloud/meta-merger/tenant.ts new file mode 100644 index 00000000..3ee67001 --- /dev/null +++ b/src/fp-cloud/meta-merger/tenant.ts @@ -0,0 +1,51 @@ +import { ResolveOnce } from "@adviser/cement"; +import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; + +export interface TenantRow { + readonly tenant: string; + readonly createdAt: Date; +} + +export class TenantSql { + static schema(drop = false): string[] { + return [ + ...conditionalDrop( + drop, + "Tenant", + ` + CREATE TABLE IF NOT EXISTS Tenant( + tenant TEXT NOT NULL PRIMARY KEY, + createdAt TEXT NOT NULL + ) + ` + ), + ]; + } + + readonly db: SQLDatabase; + constructor(db: SQLDatabase) { + this.db = db; + } + + readonly #sqlCreateTenant = new ResolveOnce(); + sqlCreateTenant(): SQLStatement[] { + return this.#sqlCreateTenant.once(() => { + return TenantSql.schema().map((i) => this.db.prepare(i)); + }); + } + + readonly #sqlInsertTenant = new ResolveOnce(); + sqlEnsureTenant(): SQLStatement { + return this.#sqlInsertTenant.once(() => { + return this.db.prepare(` + INSERT INTO Tenant(tenant, createdAt) + SELECT ?, ? WHERE NOT EXISTS(SELECT 1 FROM Tenant WHERE tenant = ?) + `); + }); + } + + async ensure(t: TenantRow) { + const stmt = this.sqlEnsureTenant(); + return stmt.run(t.tenant, t.createdAt, t.tenant); + } +} diff --git a/src/fp-cloud/msg-dispatch.ts b/src/fp-cloud/msg-dispatch.ts new file mode 100644 index 00000000..dc39b54a --- /dev/null +++ b/src/fp-cloud/msg-dispatch.ts @@ -0,0 +1,139 @@ +import { Logger } from "@adviser/cement"; +import { SuperThis, ensureLogger } from "@fireproof/core"; +import { Gestalt, MsgBase, buildErrorMsg, MsgWithError, MsgIsWithConn, MsgWithConn, QSId } from "./msg-types.js"; + +import { PreSignedMsg } from "./pre-signed-url.js"; +import { HonoServerImpl } from "./hono-server.js"; +import { UnReg } from "./msger.js"; + +export interface MsgContext { + calculatePreSignedUrl(p: PreSignedMsg): Promise; +} + +export interface WSPair { + readonly client: WebSocket; + readonly server: WebSocket; +} + +export class WSConnection { + wspair?: WSPair; + + attachWSPair(wsp: WSPair) { + if (!this.wspair) { + this.wspair = wsp; + } else { + throw new Error("wspair already set"); + } + } +} + +type Promisable = T | Promise; + +// function WithValidConn(msg: T, rri?: ResOpen): msg is MsgWithConn { +// return MsgIsWithConn(msg) && !!rri && rri.conn.resId === msg.conn.resId && rri.conn.reqId === msg.conn.reqId; +// } + +interface ConnItem { + conn: QSId; + touched: Date; +} + +class ConnectionManager { + readonly conns = new Map(); + readonly maxItems: number; + + constructor(maxItems?: number) { + this.maxItems = maxItems || 100; + } + + addConn(conn: QSId): QSId { + if (this.conns.size >= this.maxItems) { + const oldest = Array.from(this.conns.values()); + const oneHourAgo = new Date(new Date().getTime() - 60 * 60 * 1000).getTime(); + oldest + .filter((item) => item.touched.getTime() < oneHourAgo) + .forEach((item) => this.conns.delete(item.conn.resId)); + } + this.conns.set(`${conn.reqId}:${conn.resId}`, { conn, touched: new Date() }); + return conn; + } + + isConnected(msg: MsgBase): msg is MsgWithConn { + if (!MsgIsWithConn(msg)) { + return false; + } + return this.conns.has(`${msg.conn.reqId}:${msg.conn.resId}`); + } +} +const connManager = new ConnectionManager(); + +export interface MsgDispatcherCtx { + readonly impl: HonoServerImpl; +} +export interface MsgDispatchItem { + readonly match: (msg: MsgBase) => boolean; + readonly isNotConn?: boolean; + fn(sthis: SuperThis, logger: Logger, ctx: MsgDispatcherCtx, msg: Q): Promisable>; +} + +export class MsgDispatcher { + readonly sthis: SuperThis; + readonly logger: Logger; + // wsConn?: WSConnection; + readonly gestalt: Gestalt; + readonly id: string; + + readonly connManager = connManager; + + static new(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { + return new MsgDispatcher(sthis, gestalt); + } + + private constructor(sthis: SuperThis, gestalt: Gestalt) { + this.sthis = sthis; + this.logger = ensureLogger(sthis, "Dispatcher"); + this.gestalt = gestalt; + this.id = sthis.nextId().str; + } + + // addConn(msg: MsgBase): Result { + // if (!MsgIsReqOpenWithConn(msg)) { + // return this.logger.Error().Msg("msg missing reqId").ResultError(); + // } + // return Result.Ok(connManager.addConn(msg.conn)); + // } + + readonly items = new Map>(); + registerMsg(...iItems: MsgDispatchItem[]): UnReg { + const items = iItems.flat(); + const ids: string[] = items.map((item) => { + const id = this.sthis.nextId(12).str; + this.items.set(id, item); + return id; + }); + return () => ids.forEach((id) => this.items.delete(id)); + } + + async dispatch(ctx: HonoServerImpl, msg: MsgBase, send: (msg: MsgBase) => Promisable): Promise { + const validateConn = async ( + msg: T, + fn: (msg: MsgWithConn) => Promisable> + ): Promise => { + if (!connManager.isConnected(msg)) { + return send(buildErrorMsg(this.sthis, this.logger, { ...msg }, new Error("dispatch missing connection"))); + // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); + } + // if (WithValidConn(msg, this.myOpen)) { + const r = await fn(msg); + return Promise.resolve(send(r)); + }; + const found = Array.from(this.items.values()).find((item) => item.match(msg)); + if (!found) { + return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); + } + if (!found.isNotConn) { + return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, { impl: ctx }, msg)); + } + return send(await found.fn(this.sthis, this.logger, { impl: ctx }, msg)); + } +} diff --git a/src/fp-cloud/msg-dispatcher-impl.ts b/src/fp-cloud/msg-dispatcher-impl.ts new file mode 100644 index 00000000..1645fc23 --- /dev/null +++ b/src/fp-cloud/msg-dispatcher-impl.ts @@ -0,0 +1,127 @@ +import { SuperThis } from "@fireproof/core"; +import { MsgDispatcher } from "./msg-dispatch.js"; +import { + MsgIsReqGetData, + buildResGetData, + MsgIsReqPutData, + MsgIsReqDelData, + buildResDelData, + buildResPutData, + ReqGetData, + ReqPutData, + ReqDelData, +} from "./msg-types-data.js"; +import { + MsgIsReqDelWAL, + MsgIsReqGetWAL, + MsgIsReqPutWAL, + ReqDelWAL, + ReqGetWAL, + ReqPutWAL, + buildResDelWAL, + buildResGetWAL, + buildResPutWAL, +} from "./msg-types-wal.js"; +import { + MsgIsReqGestalt, + buildResGestalt, + MsgIsReqOpen, + buildErrorMsg, + buildResOpen, + MsgIsReqOpenWithConn, + MsgWithConn, + ReqGestalt, + Gestalt, +} from "./msg-types.js"; +import { + BindGetMeta, + MsgIsBindGetMeta, + MsgIsReqDelMeta, + MsgIsReqPutMeta, + ReqDelMeta, + ReqPutMeta, +} from "./msg-type-meta.js"; + +export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { + const dp = MsgDispatcher.new(sthis, gestalt); + dp.registerMsg( + { + match: MsgIsReqGestalt, + isNotConn: true, + fn: (_sthis, _logger, _ctx, msg: ReqGestalt) => { + return buildResGestalt(msg, dp.gestalt); + }, + }, + { + match: MsgIsReqOpen, + isNotConn: true, + fn: (sthis, logger, _ctx, msg) => { + if (!MsgIsReqOpenWithConn(msg)) { + return buildErrorMsg(sthis, logger, msg, new Error("missing connection")); + } + if (dp.connManager.isConnected(msg)) { + return buildResOpen(sthis, msg, msg.conn.resId); + } + const resId = sthis.nextId(12).str; + const resOpen = buildResOpen(sthis, msg, resId); + dp.connManager.addConn(resOpen.conn); + return resOpen; + }, + }, + { + match: MsgIsReqGetData, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResGetData(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqPutData, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResPutData(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqDelData, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResDelData(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqGetWAL, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResGetWAL(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqPutWAL, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResPutWAL(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsReqDelWAL, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return buildResDelWAL(sthis, logger, msg, ctx.impl); + }, + }, + { + match: MsgIsBindGetMeta, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return ctx.impl.handleBindGetMeta(sthis, logger, msg); + }, + }, + { + match: MsgIsReqPutMeta, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return ctx.impl.handleReqPutMeta(sthis, logger, msg); + }, + }, + { + match: MsgIsReqDelMeta, + fn: (sthis, logger, ctx, msg: MsgWithConn) => { + return ctx.impl.handleReqDelMeta(sthis, logger, msg); + }, + } + ); + return dp; +} diff --git a/src/fp-cloud/msg-processor.ts-off b/src/fp-cloud/msg-processor.ts-off new file mode 100644 index 00000000..788e3884 --- /dev/null +++ b/src/fp-cloud/msg-processor.ts-off @@ -0,0 +1,261 @@ +import { exception2Result, Logger, } from "@adviser/cement"; +import { + buildErrorMsg, + buildResDelMeta, + buildResGestalt, + buildResGetMeta, + buildResPutMeta, + defaultGestalt, + ErrorMsg, + getStoreFromType, + MsgBase, + MsgIsReqDelData, + MsgIsReqDelMeta, + MsgIsReqDelWAL, + MsgIsReqGestalt, + MsgIsReqGetData, + MsgIsReqGetMeta, + MsgIsReqGetWAL, + MsgIsReqPutData, + MsgIsReqPutMeta, + MsgIsReqPutWAL, + MsgIsReqSubscribeMeta, + ReqDelMeta, + ReqGetMeta, + ReqOptRes, + ReqPutMeta, + ReqRes, + ResDelMeta, + ResGetMeta, + ResPutMeta, +} from "./msg-types.js"; +import { calculatePreSignedUrl } from "./pre-signed-url.js"; +import { SuperThis } from "@fireproof/core"; + +export type WithErrorMsg = T | ErrorMsg; + +export interface CtxBase { + readonly logger: Logger; +} + +export interface ReqOptResCtx extends ReqOptRes { + readonly ctx?: C; +} + +export interface ReqResCtx extends ReqRes { + readonly ctx: C; +} + +export interface MsgProcessor { + dispatch( + decodeFn: () => Promise + ): Promise, O>>; + + // signedUrl(req: ReqSignedUrl, ctx: CtxBase): Promise>; + // subscribeMeta(req: ReqSubscribeMeta, ctx: CtxBase): Promise>; + + // delMeta(req: ReqDelMeta, ctx: CtxBase): Promise>; + // putMeta(req: ReqPutMeta, ctx: CtxBase): Promise>; + // getMeta(req: ReqGetMeta, ctx: CtxBase): Promise>; +} + +export interface RequestOpts { + readonly waitFor: (msg: MsgBase) => boolean; + readonly timeout?: number; // ms +} +// export interface Connection { +// readonly ws: WebSocket; +// readonly key: ConnectionKey; +// request(msg: MsgBase, opts: RequestOpts): Promise>; +// onMessage(msgFn: (msg: MsgBase) => void): () => void; +// close(): Promise; +// } + +export abstract class MsgProcessorBase implements MsgProcessor { + readonly logger: Logger; + readonly serverId: string; + readonly ctx: O; + readonly sthis: SuperThis; + constructor(sthis: SuperThis, logger: Logger, ctx: O, serverId: string) { + this.serverId = serverId; + this.logger = logger; + this.ctx = ctx; + this.sthis = sthis; + } + + async dispatch( + decodeFn: () => Promise, + reqFn: (msg: Q, ctx: O) => Promise> = async (req) => ({ req }) + ): Promise> { + const rReqMsg = await exception2Result(async () => (await decodeFn()) as Q); + if (rReqMsg.isErr()) { + const errMsg = buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, rReqMsg.Err()); + return { + req: errMsg as unknown as Q, + res: errMsg, + ctx: this.ctx, + }; + } + const { req, ctx: optCtx } = await reqFn(rReqMsg.Ok() as Q, this.ctx); + const ctx = { ...(optCtx || this.ctx) }; + switch (true) { + case MsgIsReqGestalt(req): + return { + req, + res: buildResGestalt(req, defaultGestalt(this.serverId, true)) as S | ErrorMsg, + ctx, + }; + + case MsgIsReqGetData(req): + case MsgIsReqGetWAL(req): + return { + req, + res: (await this.signedUrl( + { + ...req, + params: { + ...req.params, + method: "GET", + store: getStoreFromType(req).store, + }, + }, + ctx + )) as S | ErrorMsg, + ctx, + }; + + case MsgIsReqPutData(req): + case MsgIsReqPutWAL(req): + if (req.payload) { + return { + req, + res: buildErrorMsg(this.logger, req, new Error("inband payload not implemented")) as S | ErrorMsg, + ctx, + }; + } + return { + req, + res: (await this.signedUrl( + { + ...req, + params: { + ...req.params, + method: "PUT", + store: getStoreFromType(req).store, + }, + }, + ctx + )) as S | ErrorMsg, + ctx, + }; + + case MsgIsReqDelData(req): + case MsgIsReqDelWAL(req): + return { + req, + res: (await this.signedUrl( + { + ...req, + params: { + ...req.params, + method: "DELETE", + store: getStoreFromType(req).store, + }, + }, + ctx + )) as S | ErrorMsg, + ctx, + }; + + // case MsgIsReqSignedUrl(req): + // return { + // req, + // res: (await this.signedUrl(req, ctx)) as S | ErrorMsg, + // ctx, + // }; + case MsgIsReqSubscribeMeta(req): + return { + req, + res: (await this.subscribeMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + case MsgIsReqPutMeta(req): + return { + req, + res: (await this.putMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + case MsgIsReqGetMeta(req): + return { + req, + res: (await this.getMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + case MsgIsReqDelMeta(req): + return { + req, + res: (await this.delMeta(req, ctx)) as S | ErrorMsg, + ctx, + }; + } + return { + req: req, + res: buildErrorMsg(this.logger, req, new Error(`unknown msg.type=${req.type}`)) as S | ErrorMsg, + ctx, + }; + } + + async delMeta(req: ReqDelMeta, ctx: CFCtxWithGroup): Promise { + // delete meta does nothing in this implementation + // if you delete meta basically you are deleting the whole ledger + return buildResDelMeta(req, { + params: req.params, + status: "unsupported", + connId: ctx.group.connId, + }); + } + + async getMeta(req: ReqGetMeta, ctx: CF): Promise { + const rSignedUrl = await calculatePreSignedUrl( + { + tid: req.tid, + type: "reqSignedUrl", + version: req.version, + params: { ...req.params, method: "GET" }, + }, + ctx.env + ); + if (rSignedUrl.isErr()) { + return buildErrorMsg(this.logger, req, rSignedUrl.Err()); + } + return buildResGetMeta(req, { + signedGetUrl: rSignedUrl.Ok().toString(), + status: "found", + metas: [], + connId: "", + }); + } + + async putMeta(req: ReqPutMeta, ctx: CtxHasGroup): Promise { + const rSignedUrl = await calculatePreSignedUrl( + { + tid: req.tid, + type: "reqSignedUrl", + version: req.version, + params: { ...req.params, method: "PUT" }, + }, + ctx.env + ); + if (rSignedUrl.isErr()) { + return buildErrorMsg(this.logger, req, rSignedUrl.Err()); + } + // roughly time ordered + return buildResPutMeta(req, { + // metaId should be a hash of metas. + metaId: new Date().getTime().toString(), + metas: req.metas, + signedPutUrl: rSignedUrl.Ok().toString(), + connId: ctx.group.connId, + }); + } +} diff --git a/src/fp-cloud/msg-raw-connection-base.ts b/src/fp-cloud/msg-raw-connection-base.ts new file mode 100644 index 00000000..66bf75e1 --- /dev/null +++ b/src/fp-cloud/msg-raw-connection-base.ts @@ -0,0 +1,31 @@ +import { Logger } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { MsgBase, ErrorMsg, buildErrorMsg } from "./msg-types.js"; +import { ExchangedGestalt, OnErrorFn, UnReg } from "./msger.js"; + +export class MsgRawConnectionBase { + readonly sthis: SuperThis; + readonly exchangedGestalt: ExchangedGestalt; + + constructor(sthis: SuperThis, exGestalt: ExchangedGestalt) { + this.sthis = sthis; + this.exchangedGestalt = exGestalt; + } + + readonly onErrorFns = new Map(); + onError(fn: OnErrorFn): UnReg { + const key = this.sthis.nextId().str; + this.onErrorFns.set(key, fn); + return () => this.onErrorFns.delete(key); + } + + buildErrorMsg(logger: Logger, msg: Partial, err: Error): ErrorMsg { + // const logLine = this.sthis.logger.Error().Err(err).Any("msg", msg); + const rmsg = Array.from(this.onErrorFns.values()).reduce((msg, fn) => { + return fn(msg, err); + }, msg); + const emsg = buildErrorMsg(this.sthis, logger, rmsg, err); + logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); + return emsg; + } +} diff --git a/src/fp-cloud/msg-request.ts b/src/fp-cloud/msg-request.ts new file mode 100644 index 00000000..51facafe --- /dev/null +++ b/src/fp-cloud/msg-request.ts @@ -0,0 +1,220 @@ +// import { Future, Logger, exception2Result, Result, CoerceURI, KeyedResolvOnce, URI } from "@adviser/cement"; +// import { SuperThis, } from "@fireproof/core"; +// import { RequestOpts, } from "./msg-processor.js"; +// import { MsgBase, AuthType, Gestalt, defaultGestalt, ResGestalt, ReqGestalt, MsgIsResGestalt, MsgIsError, } from "./msg-types.js"; + +// import * as json from 'multiformats/codecs/json'; +// import * as cborg from '@fireproof/vendor/cborg'; +// import { MsgConnection } from "./msger.js"; + +// export interface EnDeCoder { +// encode(node: T): Uint8Array; +// decode(data: Uint8Array): T; +// } + +// export interface WaitForTid { +// readonly tid: string; +// readonly future: Future; +// // undefined match all +// readonly waitFor: (msg: MsgBase) => boolean; +// } + +// export interface FetchGestaltParams { +// readonly auth?: AuthType; +// readonly sthis: SuperThis; +// readonly gestaltURL: URI; +// readonly uniqServerId?: string; +// readonly getConn: () => Promise; +// } + +// export interface HttpConnectionParams { +// readonly gestaltURL: CoerceURI; +// readonly fetchConnection?: MsgConnection; +// readonly ende?: EnDeCoder; +// readonly uniqServerId?: string; +// } + +// export type RequestFN = (req: Q, opts: RequestOpts) => Promise> + +// const serverId = "FP-Universal-Client" + +// export function encoded(logger: Logger, g: "JSON" | "CBOR") { +// let ende: EnDeCoder +// let mime: string +// switch (g) { +// case "JSON": +// ende = json +// mime = "application/json" +// break; +// case "CBOR": +// ende = cborg +// mime = "application/cbor" +// break; +// default: +// throw logger.Error().Str("typ", g).Msg(`Unknown encoding: ${g}`).AsError() +// } +// return { ende, mime } +// } + +// const getGestalts = new KeyedResolvOnce(); + +// async function fetchGestalt(fgp: FetchGestaltParams): Promise { +// return getGestalts.get(fgp.gestaltURL.toString()).once(async () => { +// const conn = await fgp.getConn(); +// const rGestalt = await conn.request({ +// type: "reqGestalt", +// tid: fgp.sthis.nextId().str, +// version: serverId, +// gestalt: defaultGestalt({ id: fgp.uniqServerId || serverId }), +// }, { waitFor: MsgIsResGestalt }); +// if (MsgIsError(rGestalt)) { +// throw Error(rGestalt.message) +// } +// const gestalt = rGestalt.gestalt +// const ende = encoded(fgp.sthis.logger, gestalt.encodings[0]) +// return { +// ende: ende.ende, +// mime: ende.mime, +// auth: gestalt.auth, +// gestalt: gestalt +// } +// }) +// } + +// export function selectRandom(arr: T[]): T { +// return arr[Math.floor(Math.random() * arr.length)]; +// } + +// export interface MsgErrorClose { +// readonly msgFn: (msg: MessageEvent) => void; +// readonly errFn: (err: Event) => void; +// readonly closeFn: () => void; +// readonly openFn: () => void; +// } + +// export interface GestaltParams { +// readonly auth?: AuthType; +// readonly sthis: SuperThis; +// readonly gestaltURL: URI; +// readonly uniqServerId?: string; +// } + +// const keyedHttpConnection = new KeyedResolvOnce(); +// function httpFactory(sthis: SuperThis, uniqServerId: string, auth?: AuthType): (() => Promise) { +// return () => keyedHttpConnection.get(uniqServerId || serverId).once(async () => { +// return new HttpConnection(sthis, { +// ende: json, +// mime: "application/json", +// auth: auth, +// params: defaultGestalt(uniqServerId || serverId, false), +// }) +// }) +// } + +// const keyedWSConnection = new KeyedResolvOnce(); + +// export interface Attachable { +// attach(t: T): Promise +// } + +// export class WSAttachable implements Attachable { +// readonly gestalt: MsgerParams +// readonly sthis: SuperThis +// readonly waitForTid = new Map(); +// constructor(sthis: SuperThis, gestalt: MsgerParams) { +// this.gestalt = gestalt +// this.sthis = sthis +// } +// attach(t: WebSocket): Promise { +// return keyedWSConnection.get(this.gestalt.params.id).once(async () => { +// const c = new WSAttachConnection(this.sthis, t, this.waitForTid, { +// openFn: () => this.open(t), +// errFn: (err) => this.error(t, err), +// msgFn: (msg) => this.msg(t, msg), +// closeFn: () => this.close(t) +// }) +// return c +// }) +// } + +// open(ws: WebSocket) { +// this.sthis.logger.Info().Msg("open") +// } + +// error(ws: WebSocket, err: Event) { +// this.sthis.logger.Error().Msg("error") + +// } +// msg(ws: WebSocket, msg: MessageEvent) { +// this.sthis.logger.Info().Any("msg", msg).Msg("msg") +// ws.onmessage = async (event) => { +// const rMsg = await exception2Result(() => JSON.parse(event.data) as MsgBase); +// if (rMsg.isErr()) { +// this.logger.Error().Err(rMsg).Any(event.data).Msg("Invalid message"); +// return; +// } +// const msg = rMsg.Ok(); +// const waitFor = this.waitForTid.get(msg.tid); +// if (waitFor) { +// if (MsgIsError(msg)) { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// this.waitForTid.delete(msg.tid); +// waitFor.future.resolve(msg); +// } else if (waitFor.type) { +// // what for a specific type +// if (waitFor.type === msg.type) { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// this.waitForTid.delete(msg.tid); +// waitFor.future.resolve(msg); +// } else { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// } +// } else { +// // wild-card +// this.msgCallbacks.forEach((cb) => cb(msg)); +// this.waitForTid.delete(msg.tid); +// waitFor.future.resolve(msg); +// } +// } else { +// this.msgCallbacks.forEach((cb) => cb(msg)); +// } +// }; +// } + +// close(ws: WebSocket) { +// this.sthis.logger.Info().Msg("close") +// } + +// // this.params = params; + +// } + +// export async function getAttachable(p: FetchGestaltParams): Promise> { +// const g = await fetchGestalt({ +// gestaltURL: p.gestaltURL, +// sthis: p.sthis, +// getConn: httpFactory(p.sthis, p.uniqServerId || serverId, p.auth), +// }) +// if (g.params.wsEndpoints.length > 0) { +// return new WSAttachable(p.sthis, g) as Attachable +// } +// return { +// attach: async () => new HttpConnection(p.sthis, g) +// } +// } + +// export class ConnectionImpl implements Connection { +// readonly sthis: SuperThis; +// constructor(sthis: SuperThis) { +// this.sthis = sthis; +// } + +// async request(req: Q, opts: RequestOpts): Promise> { + +// } + +// } + +// export class ConnectionImpl implements Connection { + +// } diff --git a/src/fp-cloud/msg-type-meta.ts b/src/fp-cloud/msg-type-meta.ts new file mode 100644 index 00000000..80c2201d --- /dev/null +++ b/src/fp-cloud/msg-type-meta.ts @@ -0,0 +1,160 @@ +import { Logger, VERSION } from "@adviser/cement"; +import { CRDTEntry } from "@fireproof/core"; +import { + GwCtx, + MsgBase, + MsgWithConn, + MsgWithOptionalConn, + MsgWithTenantLedger, + NextId, + ReqSignedUrlParam, + ResOptionalSignedUrl, +} from "./msg-types.js"; + +/* Put Meta */ +export interface ReqPutMeta extends MsgWithTenantLedger { + readonly type: "reqPutMeta"; + readonly params: ReqSignedUrlParam; + readonly metas: CRDTEntry[]; +} + +export interface ResPutMeta extends MsgWithTenantLedger, QSMeta { + readonly type: "resPutMeta"; +} + +export function buildReqPutMeta( + sthis: NextId, + signedUrlParams: ReqSignedUrlParam, + metas: CRDTEntry[], + gwCtx: GwCtx +): ReqPutMeta { + return { + tid: sthis.nextId().str, + type: "reqPutMeta", + ...gwCtx, + version: VERSION, + params: signedUrlParams, + metas, + }; +} + +export function MsgIsReqPutMeta(msg: MsgBase): msg is ReqPutMeta { + return msg.type === "reqPutMeta"; +} + +export function buildResPutMeta( + _sthis: NextId, + _logger: Logger, + req: MsgWithTenantLedger>, + meta: QSMeta +): ResPutMeta { + return { + ...meta, + tid: req.tid, + conn: req.conn, + tenant: req.tenant, + type: "resPutMeta", + // key: req.key, + version: VERSION, + }; +} + +export function MsgIsResPutMeta(qs: MsgBase): qs is ResPutMeta { + return qs.type === "resPutMeta"; +} + +/* Bind Meta */ +export interface BindGetMeta extends MsgWithTenantLedger { + readonly type: "bindGetMeta"; + readonly params: ReqSignedUrlParam; +} + +export function MsgIsBindGetMeta(msg: MsgBase): msg is BindGetMeta { + return msg.type === "bindGetMeta"; +} + +export interface QSMeta extends ResOptionalSignedUrl { + readonly metas: CRDTEntry[]; + readonly keys?: string[]; +} + +export interface EventGetMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { + readonly type: "eventGetMeta"; +} + +export function buildBindGetMeta(sthis: NextId, params: ReqSignedUrlParam, gwCtx: GwCtx): BindGetMeta { + return { + tid: sthis.nextId().str, + ...gwCtx, + type: "bindGetMeta", + version: VERSION, + params, + }; +} + +export function buildEventGetMeta( + _sthis: NextId, + _logger: Logger, + req: MsgWithTenantLedger>, + metaParam: QSMeta, + gwCtx: GwCtx +): EventGetMeta { + return { + ...metaParam, + ...gwCtx, + tid: req.tid, + type: "eventGetMeta", + params: { ...req.params, method: "GET", store: "meta" }, + version: VERSION, + }; +} + +export function MsgIsEventGetMeta(qs: MsgBase): qs is EventGetMeta { + return qs.type === "eventGetMeta"; +} + +/* Del Meta */ +export interface ReqDelMeta extends MsgWithTenantLedger { + readonly type: "reqDelMeta"; + readonly params: ReqSignedUrlParam; +} + +export function buildReqDelMeta(sthis: NextId, signedUrlParams: ReqSignedUrlParam, gwCtx: GwCtx): ReqDelMeta { + return { + tid: sthis.nextId().str, + ...gwCtx, + type: "reqDelMeta", + version: VERSION, + params: signedUrlParams, + }; +} + +export function MsgIsReqDelMeta(msg: MsgBase): msg is ReqDelMeta { + return msg.type === "reqDelMeta"; +} + +export interface ResDelMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { + readonly type: "resDelMeta"; +} + +export function buildResDelMeta( + _sthis: NextId, + _logger: Logger, + req: MsgWithTenantLedger>, + signedUrl?: string +): ResDelMeta { + return { + params: { ...req.params, method: "DELETE", store: "meta" }, + signedUrl, + tid: req.tid, + conn: req.conn, + tenant: req.tenant, + type: "resDelMeta", + // key: req.key, + version: VERSION, + }; +} + +export function MsgIsResDelMeta(qs: MsgBase): qs is ResDelMeta { + return qs.type === "resDelMeta"; +} diff --git a/src/fp-cloud/msg-types-data.ts b/src/fp-cloud/msg-types-data.ts new file mode 100644 index 00000000..12ceeb77 --- /dev/null +++ b/src/fp-cloud/msg-types-data.ts @@ -0,0 +1,109 @@ +import { Logger, Result, URI } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { + ReqSignedUrl, + NextId, + MsgBase, + ResSignedUrl, + MsgWithError, + buildRes, + ReqSignedUrlParam, + buildReqSignedUrl, + GwCtx, + MsgIsTenantLedger, + MsgWithConn, +} from "./msg-types.js"; +import { PreSignedMsg } from "./pre-signed-url.js"; + +export interface ReqGetData extends ReqSignedUrl { + readonly type: "reqGetData"; +} + +export function buildReqGetData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqGetData { + return buildReqSignedUrl(sthis, "reqGetData", sup, ctx); +} + +export function MsgIsReqGetData(msg: MsgBase): msg is ReqGetData { + return msg.type === "reqGetData"; +} + +export interface ResGetData extends ResSignedUrl { + readonly type: "resGetData"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsResGetData(msg: MsgBase): msg is ResGetData { + return msg.type === "resGetData" && MsgIsTenantLedger(msg); +} + +export interface CalculatePreSignedUrl { + calculatePreSignedUrl(p: PreSignedMsg): Promise>; +} + +export function buildResGetData( + sthis: SuperThis, + logger: Logger, + req: MsgWithConn, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes, ResGetData>("GET", "data", "resGetData", sthis, logger, req, ctx); +} + +export interface ReqPutData extends ReqSignedUrl { + readonly type: "reqPutData"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsReqPutData(msg: MsgBase): msg is ReqPutData { + return msg.type === "reqPutData"; +} + +export function buildReqPutData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqPutData { + return buildReqSignedUrl(sthis, "reqPutData", sup, ctx); +} + +export interface ResPutData extends ResSignedUrl { + readonly type: "resPutData"; +} + +export function MsgIsResPutData(msg: MsgBase): msg is ResPutData { + return msg.type === "resPutData"; +} + +export function buildResPutData( + sthis: SuperThis, + logger: Logger, + req: MsgWithConn, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes, ResPutData>("PUT", "data", "resPutData", sthis, logger, req, ctx); +} + +export interface ReqDelData extends ReqSignedUrl { + readonly type: "reqDelData"; +} + +export function MsgIsReqDelData(msg: MsgBase): msg is ReqDelData { + return msg.type === "reqDelData"; +} + +export function buildReqDelData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqDelData { + return buildReqSignedUrl(sthis, "reqDelData", sup, ctx); +} + +export interface ResDelData extends ResSignedUrl { + readonly type: "resDelData"; +} + +export function MsgIsResDelData(msg: MsgBase): msg is ResDelData { + return msg.type === "resDelData"; +} + +export function buildResDelData( + sthis: SuperThis, + logger: Logger, + req: MsgWithConn, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes, ResDelData>("DELETE", "data", "resDelData", sthis, logger, req, ctx); +} diff --git a/src/fp-cloud/msg-types-wal.ts b/src/fp-cloud/msg-types-wal.ts new file mode 100644 index 00000000..3363cd43 --- /dev/null +++ b/src/fp-cloud/msg-types-wal.ts @@ -0,0 +1,130 @@ +import { Logger } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { + MsgBase, + MsgWithError, + buildRes, + NextId, + ReqSignedUrl, + ResSignedUrl, + ReqSignedUrlParam, + buildReqSignedUrl, + GwCtx, + MsgIsTenantLedger, + MsgWithTenantLedger, + MsgWithConn, +} from "./msg-types.js"; +import { CalculatePreSignedUrl } from "./msg-types-data.js"; + +export interface ReqGetWAL extends ReqSignedUrl { + readonly type: "reqGetWAL"; +} + +export function MsgIsReqGetWAL(msg: MsgBase): msg is ReqGetWAL { + return msg.type === "reqGetWAL"; +} + +export function buildReqGetWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqGetWAL { + return buildReqSignedUrl(sthis, "reqGetWAL", sup, ctx); +} + +export interface ResGetWAL extends ResSignedUrl { + readonly type: "resGetWAL"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsResGetWAL(msg: MsgBase): msg is ResGetWAL { + return msg.type === "resGetWAL"; +} + +export function buildResGetWAL( + sthis: SuperThis, + logger: Logger, + req: MsgWithTenantLedger>, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes>, ResGetWAL>( + "GET", + "wal", + "resGetWAL", + sthis, + logger, + req, + ctx + ); +} + +export interface ReqPutWAL extends Omit { + readonly type: "reqPutWAL"; + // readonly payload: Uint8Array; // transfered via JSON base64 +} + +export function MsgIsReqPutWAL(msg: MsgBase): msg is ReqPutWAL { + return msg.type === "reqPutWAL"; +} + +export function buildReqPutWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqPutWAL { + return buildReqSignedUrl(sthis, "reqPutWAL", sup, ctx); +} + +export interface ResPutWAL extends Omit { + readonly type: "resPutWAL"; +} + +export function MsgIsResPutWAL(msg: MsgBase): msg is ResPutWAL { + return msg.type === "resPutWAL"; +} + +export function buildResPutWAL( + sthis: SuperThis, + logger: Logger, + req: MsgWithTenantLedger>, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes>, ResPutWAL>( + "PUT", + "wal", + "resPutWAL", + sthis, + logger, + req, + ctx + ); +} + +export interface ReqDelWAL extends Omit { + readonly type: "reqDelWAL"; +} + +export function MsgIsReqDelWAL(msg: MsgBase): msg is ReqDelWAL { + return msg.type === "reqDelWAL"; +} + +export function buildReqDelWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqDelWAL { + return buildReqSignedUrl(sthis, "reqDelWAL", sup, ctx); +} + +export interface ResDelWAL extends Omit { + readonly type: "resDelWAL"; +} + +export function MsgIsResDelWAL(msg: MsgBase): msg is ResDelWAL { + return msg.type === "resDelWAL" && MsgIsTenantLedger(msg); +} + +export function buildResDelWAL( + sthis: SuperThis, + logger: Logger, + req: MsgWithTenantLedger>, + ctx: CalculatePreSignedUrl +): Promise> { + return buildRes>, ResDelWAL>( + "DELETE", + "wal", + "resDelWAL", + sthis, + logger, + req, + ctx + ); +} diff --git a/src/fp-cloud/msg-types.ts b/src/fp-cloud/msg-types.ts new file mode 100644 index 00000000..43664e83 --- /dev/null +++ b/src/fp-cloud/msg-types.ts @@ -0,0 +1,567 @@ +import { Future } from "@adviser/cement"; +import { Logger, SuperThis } from "@fireproof/core"; +import { CalculatePreSignedUrl } from "./msg-types-data.js"; +import { PreSignedMsg } from "./pre-signed-url.js"; + +export const VERSION = "FP-MSG-1.0"; + +export type MsgWithError = T | ErrorMsg; + +export interface RequestOpts { + readonly waitFor: (msg: MsgBase) => boolean; + readonly pollInterval?: number; // 1000ms + readonly timeout?: number; // ms +} + +export interface EnDeCoder { + encode(node: T): Uint8Array; + decode(data: Uint8Array): T; +} + +export interface WaitForTid { + readonly tid: string; + readonly future: Future; + readonly timeout?: number; + // undefined match all + readonly waitFor: (msg: MsgBase) => boolean; +} + +// export interface ConnId { +// readonly connId: string; +// } +// type AddConnId = Omit & ConnId & { readonly type: N }; +export interface NextId { + readonly nextId: SuperThis["nextId"]; +} + +export interface AuthType { + readonly type: "ucan"; +} + +export interface UCanAuth { + readonly type: "ucan"; + readonly params: { + readonly tbd: string; + }; +} + +export interface TenantLedger { + readonly tenant: string; + readonly ledger: string; +} + +export function keyTenantLedger(t: TenantLedger): string { + return `${t.tenant}:${t.ledger}`; +} + +export interface QSId { + readonly reqId: string; + readonly resId: string; +} + +// export interface Connection extends ReqResId{ +// readonly key: TenantLedger; +// } + +// export interface Connected { +// readonly conn: Connection; +// } + +export interface MsgBase { + readonly tid: string; + readonly type: string; + readonly version: string; + readonly auth?: AuthType; +} + +export function MsgIsTid(msg: MsgBase, tid: string): boolean { + return msg.tid === tid; +} + +export type MsgWithConn = T & { readonly conn: QSId }; + +export type MsgWithOptionalConn = T & { readonly conn?: QSId }; + +export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; + +export interface ErrorMsg extends MsgBase { + readonly type: "error"; + readonly src: unknown; + readonly message: string; + readonly body?: string; + readonly stack?: string[]; +} + +export function MsgIsError(rq: MsgBase): rq is ErrorMsg { + return rq.type === "error"; +} + +export function MsgIsQSError(rq: ReqRes): rq is ReqRes { + return rq.res.type === "error" || rq.req.type === "error"; +} + +export type HttpMethods = "GET" | "PUT" | "DELETE"; +export type FPStoreTypes = "meta" | "data" | "wal"; + +// reqRes is http +// stream is WebSocket +export type ProtocolCapabilities = "reqRes" | "stream"; + +export interface Gestalt { + /** + * Describes StoreTypes which are handled + */ + readonly storeTypes: FPStoreTypes[]; + /** + * A unique identifier + */ + readonly id: string; + /** + * protocol capabilities + * defaults "stream" + */ + readonly protocolCapabilities: ProtocolCapabilities[]; + /** + * HttpEndpoints (URL) required atleast one + * could be absolute or relative + */ + readonly httpEndpoints: string[]; + /** + * WebsocketEndpoints (URL) required atleast one + * could be absolute or relative + */ + readonly wsEndpoints: string[]; + /** + * Encodings supported + * JSON, CBOR + */ + readonly encodings: ("JSON" | "CBOR")[]; + /** + * Authentication methods supported + */ + readonly auth: AuthType[]; + /** + * Requires Authentication + */ + readonly requiresAuth: boolean; + /** + * In|Outband Data | Meta | WAL Support + * Inband Means that the Payload is part of the message + * Outband Means that the Payload is PUT/GET to a different URL + * A Clien implementation usally not support reading or writing + * support + */ + readonly data?: { + readonly inband: boolean; + readonly outband: boolean; + }; + readonly meta?: { + readonly inband: true; // meta inband is mandatory + readonly outband: boolean; + }; + readonly wal?: { + readonly inband: boolean; + readonly outband: boolean; + }; + /** + * Request Types supported + * reqGestalt, reqSubscribeMeta, reqPutMeta, reqGetMeta, reqDelMeta, reqUpdateMeta + */ + readonly reqTypes: string[]; + /** + * Response Types supported + * resGestalt, resSubscribeMeta, resPutMeta, resGetMeta, resDelMeta, updateMeta + */ + readonly resTypes: string[]; + /** + * Event Types supported + * updateMeta + */ + readonly eventTypes: string[]; +} + +export interface MsgerParams { + readonly mime: string; + readonly auth?: AuthType; + readonly hasPersistent?: boolean; + readonly protocolCapabilities?: ProtocolCapabilities[]; + // readonly protocol: "http" | "ws"; + readonly timeout: number; // msec +} + +// force the server id +export type GestaltParam = Partial & { readonly id: string }; + +export function defaultGestalt(msgP: MsgerParams, gestalt: GestaltParam): Gestalt { + return { + storeTypes: ["meta", "data", "wal"], + httpEndpoints: ["/fp"], + wsEndpoints: ["/ws"], + encodings: ["JSON"], + protocolCapabilities: msgP.protocolCapabilities || ["reqRes", "stream"], + auth: [], + requiresAuth: false, + data: msgP.hasPersistent + ? { + inband: true, + outband: true, + } + : undefined, + meta: msgP.hasPersistent + ? { + inband: true, + outband: true, + } + : undefined, + wal: msgP.hasPersistent + ? { + inband: true, + outband: true, + } + : undefined, + reqTypes: [ + "reqOpen", + "reqGestalt", + // "reqSignedUrl", + "reqSubscribeMeta", + "reqPutMeta", + "reqGetMeta", + "reqDelMeta", + "reqPutData", + "reqGetData", + "reqDelData", + "reqPutWAL", + "reqGetWAL", + "reqDelWAL", + "reqUpdateMeta", + ], + resTypes: [ + "resOpen", + "resGestalt", + // "resSignedUrl", + "resSubscribeMeta", + "resPutMeta", + "resGetMeta", + "resDelMeta", + "resPutData", + "resGetData", + "resDelData", + "resPutWAL", + "resGetWAL", + "resDelWAL", + "updateMeta", + ], + eventTypes: ["updateMeta"], + ...gestalt, + }; +} + +/** + * The ReqGestalt message is used to request the + * features of the Responder. + */ +export interface ReqGestalt extends MsgBase { + readonly type: "reqGestalt"; + readonly gestalt: Gestalt; +} + +export function MsgIsReqGestalt(msg: MsgBase): msg is ReqGestalt { + return msg.type === "reqGestalt"; +} + +export function buildReqGestalt(sthis: NextId, gestalt: Gestalt): ReqGestalt { + return { + tid: sthis.nextId().str, + type: "reqGestalt", + version: VERSION, + gestalt, + }; +} + +/** + * The ResGestalt message is used to respond with + * the features of the Responder. + */ +export interface ResGestalt extends MsgBase { + readonly type: "resGestalt"; + readonly gestalt: Gestalt; +} + +export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt): ResGestalt | ErrorMsg { + return { + tid: req.tid, + type: "resGestalt", + version: VERSION, + gestalt, + }; +} + +export function MsgIsResGestalt(msg: MsgBase): msg is ResGestalt { + return msg.type === "resGestalt"; +} + +export interface ReqOpenConnection { + // readonly key: TenantLedger; + readonly reqId?: string; + readonly resId?: string; // for double open +} + +export interface ReqOpenConn { + readonly reqId: string; + readonly resId?: string; +} + +export interface ReqOpen extends MsgBase { + readonly type: "reqOpen"; + readonly conn: ReqOpenConn; +} + +export function buildReqOpen(sthis: NextId, conn: ReqOpenConnection): ReqOpen { + return { + tid: sthis.nextId().str, + type: "reqOpen", + version: VERSION, + conn: { + ...conn, + reqId: conn.reqId || sthis.nextId().str, + }, + }; +} + +export function MsgIsReqOpenWithConn(imsg: MsgBase): imsg is MsgWithConn { + const msg = imsg as MsgWithConn; + return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; +} + +export function MsgIsReqOpen(imsg: MsgBase): imsg is MsgWithConn { + const msg = imsg as MsgWithConn; + return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; +} + +export interface ResOpen extends MsgBase { + readonly type: "resOpen"; + readonly conn: QSId; +} + +export function MsgIsWithConn(msg: T): msg is MsgWithConn { + const mwc = (msg as MsgWithConn).conn; + return mwc && !!(mwc as QSId).reqId && !!(mwc as QSId).resId; +} + +export function MsgIsConnected(msg: T, qsid: QSId): msg is MsgWithConn { + return MsgIsWithConn(msg) && msg.conn.reqId === qsid.reqId && msg.conn.resId === qsid.resId; +} + +export function buildResOpen(sthis: NextId, req: ReqOpen, resStreamId?: string): ResOpen { + if (!(req.conn && req.conn.reqId)) { + throw new Error("req.conn.reqId is required"); + } + return { + ...req, + type: "resOpen", + conn: { + ...req.conn, + resId: req.conn.resId || resStreamId || sthis.nextId().str, + }, + }; +} + +export function MsgIsResOpen(msg: MsgBase): msg is ResOpen { + return msg.type === "resOpen"; +} + +export interface ReqClose extends Omit { + readonly type: "reqClose"; +} + +export function MsgIsReqClose(msg: MsgBase): msg is ReqClose { + return msg.type === "reqClose" && MsgIsWithConn(msg); +} + +export interface ResClose extends Omit { + readonly type: "resClose"; +} + +export function MsgIsResClose(msg: MsgBase): msg is ResClose { + return msg.type === "resClose" && MsgIsWithConn(msg); +} + +export interface SignedUrlParam { + readonly method: HttpMethods; + readonly store: FPStoreTypes; + // base path + readonly path?: string; + // name of the file + readonly key: string; + readonly expires?: number; // seconds + readonly index?: string; +} + +export type ReqSignedUrlParam = Omit; + +export interface UpdateReqRes { + req: Q; + res: S; +} + +export type ReqRes = Readonly>; + +// export interface ReqOptRes { +// readonly req: Q; +// readonly res?: S; +// } + +// /* Signed URL */ +// export function buildReqSignedUrl(req: ReqSignedUrlParam): ReqSignedUrlParam { +// return { +// tid: req.tid, +// params: { +// // protocol: "wss", +// ...req.params, +// }, +// }; +// } + +// export function MsgIsReqSignedUrl(msg: MsgBase): msg is ReqSignedUrl { +// return msg.type === "reqSignedUrl"; +// } + +// interface StoreAndType { +// readonly store: FPStoreTypes; +// readonly resType: string; +// } +// const reqToRes: Record = { +// reqGetData: { store: "data", resType: "resGetData" }, +// reqPutData: { store: "data", resType: "resPutData" }, +// reqDelData: { store: "data", resType: "resDelData" }, +// reqGetWAL: { store: "wal", resType: "resGetWAL" }, +// reqPutWAL: { store: "wal", resType: "resPutWAL" }, +// reqDelWAL: { store: "wal", resType: "resDelWAL" }, +// }; + +// export function getStoreFromType(req: MsgBase): StoreAndType { +// return ( +// reqToRes[req.type] || +// (() => { +// throw new Error(`unknown req.type=${req.type}`); +// })() +// ); +// } + +// export function buildResSignedUrl(req: ReqSignedUrl, signedUrl: string): ResSignedUrl { +// return { +// tid: req.tid, +// type: getStoreFromType(req).resType, +// version: VERSION, +// params: req.params, +// signedUrl, +// }; +// } + +export function buildErrorMsg( + sthis: SuperThis, + logger: Logger, + base: Partial, + error: Error, + body?: string, + stack?: string[] +): ErrorMsg { + if (!stack && sthis.env.get("FP_STACK")) { + stack = error.stack?.split("\n"); + } + const msg = { + src: base, + type: "error", + tid: base.tid || "internal", + message: error.message, + version: VERSION, + body, + stack, + } satisfies ErrorMsg; + logger.Any("ErrorMsg", msg); + return msg; +} + +// export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; + +export function MsgIsTenantLedger(msg: T): msg is MsgWithTenantLedger { + const t = (msg as MsgWithTenantLedger).tenant; + return !!t && !!t.tenant && !!t.ledger; +} + +export interface ReqSignedUrl extends MsgWithTenantLedger { + // readonly type: "reqSignedUrl"; + readonly params: ReqSignedUrlParam; +} + +export interface GwCtx { + readonly tid?: string; + readonly conn?: QSId; + readonly tenant: TenantLedger; +} + +export interface GwCtxConn { + readonly tid?: string; + readonly conn: QSId; + readonly tenant: TenantLedger; +} + +export function buildReqSignedUrl( + sthis: NextId, + type: string, + params: ReqSignedUrlParam, + gwCtx: GwCtx +): T { + return { + tid: sthis.nextId().str, + type, + version: VERSION, + ...gwCtx, + params, + } as T; +} + +export interface ResSignedUrl extends MsgWithTenantLedger { + // readonly type: "resSignedUrl"; + readonly params: SignedUrlParam; + readonly signedUrl: string; +} + +export interface ResOptionalSignedUrl extends MsgWithTenantLedger { + // readonly type: "resSignedUrl"; + readonly params: SignedUrlParam; + readonly signedUrl?: string; +} + +export async function buildRes>, S extends ResSignedUrl>( + method: SignedUrlParam["method"], + store: FPStoreTypes, + type: string, + sthis: SuperThis, + logger: Logger, + req: Q, + ctx: CalculatePreSignedUrl +): Promise> { + const psm = { + type: "reqSignedUrl", + version: req.version, + params: { + ...req.params, + method, + store, + }, + conn: req.conn, + tenant: req.tenant, + tid: req.tid, + } satisfies PreSignedMsg; + const rSignedUrl = await ctx.calculatePreSignedUrl(psm); + if (rSignedUrl.isErr()) { + return buildErrorMsg(sthis, logger, req, rSignedUrl.Err()); + } + return { + ...req, + params: psm.params, + type, + signedUrl: rSignedUrl.Ok().toString(), + } as unknown as MsgWithError; +} diff --git a/src/fp-cloud/msger.ts b/src/fp-cloud/msger.ts new file mode 100644 index 00000000..d6ea4da2 --- /dev/null +++ b/src/fp-cloud/msger.ts @@ -0,0 +1,274 @@ +import { BuildURI, CoerceURI, Result, runtimeFn, URI } from "@adviser/cement"; +import { + buildReqGestalt, + defaultGestalt, + EnDeCoder, + Gestalt, + MsgBase, + MsgerParams, + MsgIsResGestalt, + RequestOpts, + ResGestalt, + MsgWithError, + MsgWithConn, + buildReqOpen, + MsgIsConnected, + MsgIsError, + MsgIsResOpen, + MsgWithOptionalConn, + QSId, + MsgIsTid, + ReqGestalt, +} from "./msg-types.js"; +import { SuperThis } from "@fireproof/core"; +import { HttpConnection } from "./http-connection.js"; +import { WSConnection } from "./ws-connection.js"; + +// const headers = { +// "Content-Type": "application/json", +// "Accept": "application/json", +// }; + +export function selectRandom(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +export function timeout(ms: number, promise: Promise): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`TIMEOUT after ${ms}ms`)); + }, ms); + promise + .then(resolve) + .catch(reject) + .finally(() => clearTimeout(timer)); + }); +} + +export type OnMsgFn = (msg: MsgWithError) => void; +export type UnReg = () => void; + +export interface ExchangedGestalt { + readonly my: Gestalt; + readonly remote: Gestalt; +} + +export type OnErrorFn = (msg: Partial, err: Error) => Partial; + +export interface ActiveStream { + readonly id: string; + readonly bind: { + readonly msg: Q; + readonly opts: RequestOpts; + }; + timeout?: unknown; + controller?: ReadableStreamDefaultController>; +} + +export interface MsgRawConnection { + // readonly ws: WebSocket; + // readonly params: ConnectionKey; + // qsOpen: ReqRes; + readonly sthis: SuperThis; + readonly exchangedGestalt: ExchangedGestalt; + readonly activeBinds: Map>; + bind(req: Q, opts: RequestOpts): ReadableStream>; + request(req: Q, opts: RequestOpts): Promise>; + start(): Promise>; + close(): Promise>; + onMsg(msg: OnMsgFn): UnReg; +} + +export function jsonEnDe(sthis: SuperThis): EnDeCoder { + return { + encode: (node: unknown) => sthis.txt.encode(JSON.stringify(node)), + decode: (data: Uint8Array) => JSON.parse(sthis.txt.decode(data)), + }; +} + +export type MsgerParamsWithEnDe = MsgerParams & { readonly ende: EnDeCoder }; + +export function defaultMsgParams(sthis: SuperThis, igs: Partial): MsgerParamsWithEnDe { + return { + mime: "application/json", + ende: jsonEnDe(sthis), + timeout: 3000, + protocolCapabilities: ["reqRes", "stream"], + ...igs, + } satisfies MsgerParamsWithEnDe; +} + +export interface OpenParams { + readonly timeout: number; +} + +export async function applyStart(prC: Promise>): Promise> { + const rC = await prC; + if (rC.isErr()) { + return rC; + } + const c = rC.Ok(); + const r = await c.start(); + if (r.isErr()) { + return Result.Err(r.Err()); + } + return rC; +} + +export class MsgConnected implements MsgRawConnection { + static async connect( + mrc: Result | MsgRawConnection, + conn: Partial = {} + ): Promise> { + if (Result.Is(mrc)) { + if (mrc.isErr()) { + return Result.Err(mrc.Err()); + } + mrc = mrc.Ok(); + } + const res = await mrc.request(buildReqOpen(mrc.sthis, conn), { waitFor: MsgIsResOpen }); + if (MsgIsError(res) || !MsgIsResOpen(res)) { + return mrc.sthis.logger.Error().Err(res).Msg("unexpected response").ResultError(); + } + return Result.Ok(new MsgConnected(mrc, res.conn)); + } + + readonly sthis: SuperThis; + readonly conn: QSId; + readonly raw: MsgRawConnection; + readonly exchangedGestalt: ExchangedGestalt; + readonly activeBinds: Map>; + private constructor(raw: MsgRawConnection, conn: QSId) { + this.sthis = raw.sthis; + this.raw = raw; + this.exchangedGestalt = raw.exchangedGestalt; + this.conn = conn; + this.activeBinds = raw.activeBinds; + } + + bind( + req: Q, + opts: RequestOpts + ): ReadableStream> { + const stream = this.raw.bind({ ...req, conn: req.conn || this.conn }, opts); + const ts = new TransformStream, MsgWithError>({ + transform: (chunk, controller) => { + if (!MsgIsTid(chunk, req.tid)) { + return; + } + if (MsgIsConnected(chunk, this.conn)) { + if ((opts.waitFor && opts.waitFor(chunk)) || MsgIsError(chunk)) { + controller.enqueue(chunk); + } + } + }, + }); + // eslint-disable-next-line no-console + // why the hell pipeTo sends an error that is undefined? + stream.pipeThrough(ts); + // stream.pipeTo(ts.writable).catch((err) => err && err.message && console.error("bind error", err)); + return ts.readable; + } + + request(req: Q, opts: RequestOpts): Promise> { + return this.raw.request({ ...req, conn: req.conn || this.conn }, opts); + } + start(): Promise> { + return this.raw.start(); + } + close(): Promise> { + return this.raw.close(); + } + onMsg(msgFn: OnMsgFn): UnReg { + return this.raw.onMsg((msg) => { + if (MsgIsConnected(msg, this.conn)) { + msgFn(msg); + } + }); + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class Msger { + static async openHttp( + sthis: SuperThis, + // reqOpen: ReqOpen | undefined, + urls: URI[], + msgP: MsgerParamsWithEnDe, + exGestalt: ExchangedGestalt + ): Promise> { + return Result.Ok(new HttpConnection(sthis, urls, msgP, exGestalt)); + } + static async openWS( + sthis: SuperThis, + // qOpen: ReqOpen, + url: URI, + msgP: MsgerParamsWithEnDe, + exGestalt: ExchangedGestalt + ): Promise> { + let ws: WebSocket; + // const { encode } = jsonEnDe(sthis); + url = url.build().URI(); + // .setParam("reqOpen", sthis.txt.decode(encode(qOpen))) + if (runtimeFn().isNodeIsh) { + const { WebSocket } = await import("ws"); + ws = new WebSocket(url.toString()) as unknown as WebSocket; + } else { + ws = new WebSocket(url.toString()); + } + return Result.Ok(new WSConnection(sthis, ws, msgP, exGestalt)); + } + static async open( + sthis: SuperThis, + curl: CoerceURI, + imsgP: Partial = {} + ): Promise> { + // initial exchange with JSON encoding + const jsMsgP = defaultMsgParams(sthis, { ...imsgP, mime: "application/json", ende: jsonEnDe(sthis) }); + const url = URI.from(curl); + const gs = defaultGestalt(defaultMsgParams(sthis, imsgP), { id: "FP-Universal-Client" }); + /* + * request Gestalt with Http + */ + const rHC = await Msger.openHttp(sthis, [url], jsMsgP, { my: gs, remote: gs }); + if (rHC.isErr()) { + return rHC; + } + const hc = rHC.Ok(); + const resGestalt = await hc.request(buildReqGestalt(sthis, gs), { + waitFor: MsgIsResGestalt, + }); + if (!MsgIsResGestalt(resGestalt)) { + return Result.Err(new Error("Invalid Gestalt")); + } + await hc.close(); + const exGt = { my: gs, remote: resGestalt.gestalt } satisfies ExchangedGestalt; + const msgP = defaultMsgParams(sthis, imsgP); + if (exGt.remote.protocolCapabilities.includes("reqRes") && !exGt.remote.protocolCapabilities.includes("stream")) { + return applyStart( + Msger.openHttp( + sthis, + exGt.remote.httpEndpoints.map((i) => BuildURI.from(url).resolve(i).URI()), + msgP, + exGt + ) + ); + } + return applyStart( + Msger.openWS(sthis, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) + ); + } + + static connect( + sthis: SuperThis, + curl: CoerceURI, + imsgP: Partial = {}, + conn: Partial = {} + ): Promise> { + return Msger.open(sthis, curl, imsgP).then((srv) => MsgConnected.connect(srv, conn)); + } + + private constructor() { + /* */ + } +} diff --git a/src/fp-cloud/new-websocket.ts b/src/fp-cloud/new-websocket.ts new file mode 100644 index 00000000..1c8ef6fe --- /dev/null +++ b/src/fp-cloud/new-websocket.ts @@ -0,0 +1,11 @@ +import { CoerceURI, runtimeFn, URI } from "@adviser/cement"; + +export async function newWebSocket(url: CoerceURI): Promise { + const wsUrl = URI.from(url).toString(); + if (runtimeFn().isNodeIsh) { + const { WebSocket: MyWS } = await import("ws"); + return new MyWS(wsUrl) as unknown as WebSocket; + } else { + return new WebSocket(wsUrl); + } +} diff --git a/src/fp-cloud/node-hono-server.ts b/src/fp-cloud/node-hono-server.ts new file mode 100644 index 00000000..53f75a1f --- /dev/null +++ b/src/fp-cloud/node-hono-server.ts @@ -0,0 +1,142 @@ +import { UpgradeWebSocket, WSContext, WSContextInit, WSEvents } from "hono/ws"; +import { ConnMiddleware, HonoServerBase, HonoServerFactory, HonoServerImpl, RunTimeParams } from "./hono-server.js"; +import { HttpHeader, URI } from "@adviser/cement"; +import { Context, Hono } from "hono"; +import { ensureLogger, SuperThis } from "@fireproof/core"; +import { defaultMsgParams, jsonEnDe } from "./msger.js"; +import { defaultGestalt, Gestalt, MsgerParams } from "./msg-types.js"; +import { SQLDatabase } from "./meta-merger/abstract-sql.js"; +import { WSRoom } from "./ws-room.js"; + +interface ServerType { + close(fn: () => void): void; +} + +type serveFn = (options: unknown, listeningListener?: ((info: unknown) => void) | undefined) => ServerType; + +export interface NodeHonoFactoryParams { + readonly msgP?: MsgerParams; + readonly gs?: Gestalt; + readonly sql: SQLDatabase; +} + +const wsConnections = new Map(); +class NodeWSRoom implements WSRoom { + readonly sthis: SuperThis; + constructor(sthis: SuperThis) { + this.sthis = sthis; + } + acceptConnection(ws: WebSocket, wse: WSEvents): Promise { + const id = this.sthis.nextId(12).str; + wsConnections.set(id, ws); + + const wsCtx = new WSContext(ws as WSContextInit); + + ws.onerror = (err) => { + // console.log("onerror", err); + wse.onError?.(err, wsCtx); + }; + ws.onclose = (ev) => { + // console.log("onclose", ev); + wse.onClose?.(ev, wsCtx); + }; + ws.onmessage = (evt) => { + // console.log("onmessage", evt); + // wsCtx.send("Hellox from server"); + wse.onMessage?.(evt, wsCtx); + }; + + ws.accept(); + return Promise.resolve(); + } +} + +export class NodeHonoFactory implements HonoServerFactory { + _upgradeWebSocket!: UpgradeWebSocket; + _injectWebSocket!: (t: unknown) => void; + _serve!: serveFn; + _server!: ServerType; + // _env!: Env; + + readonly sthis: SuperThis; + readonly params: NodeHonoFactoryParams; + constructor(sthis: SuperThis, params: NodeHonoFactoryParams) { + this.sthis = sthis; + this.params = params; + } + + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { + // this._env = c.env; + // const sthis = ensureSuperThis(); + const sthis = this.sthis; + const logger = ensureLogger(sthis, `NodeHono[${URI.from(c.req.url).pathname}]`); + const ende = jsonEnDe(sthis); + + const fpProtocol = sthis.env.get("FP_PROTOCOL"); + const msgP = + this.params.msgP ?? + defaultMsgParams(sthis, { + hasPersistent: true, + protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], + }); + const gs = + this.params.gs ?? + defaultGestalt(msgP, { + id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", + }); + const wsRoom = new NodeWSRoom(sthis); + const nhs = new NodeHonoServer(sthis, this, gs, this.params.sql, wsRoom); + return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs })); + } + + async start(app: Hono): Promise { + try { + const { createNodeWebSocket } = await import("@hono/node-ws"); + const { serve } = await import("@hono/node-server"); + this._serve = serve as serveFn; + const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app }); + this._upgradeWebSocket = upgradeWebSocket; + this._injectWebSocket = injectWebSocket as (t: unknown) => void; + } catch (e) { + throw this.sthis.logger.Error().Err(e).Msg("Failed to start NodeHonoFactory").AsError(); + } + } + + async serve(app: Hono, port: number): Promise { + await new Promise((resolve) => { + this._server = this._serve({ fetch: app.fetch, port }, () => { + this._injectWebSocket(this._server); + resolve(); + }); + }); + } + async close(): Promise { + this._server.close(() => { + /* */ + }); + // return new Promise((res) => this._server.close(() => res())); + } +} + +export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { + readonly _upgradeWebSocket: UpgradeWebSocket; + constructor( + sthis: SuperThis, + factory: NodeHonoFactory, + gs: Gestalt, + sqldb: SQLDatabase, + wsRoom: WSRoom, + headers?: HttpHeader + ) { + super(sthis, sthis.logger, gs, sqldb, wsRoom, headers); + this._upgradeWebSocket = factory._upgradeWebSocket; + } + + override upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { + return async (_conn, c, next) => { + // conn.attachWSPair({ client: c.req, server: c.res }); + return this._upgradeWebSocket(createEvents)(c, next); + }; + } +} diff --git a/src/fp-cloud/pre-signed-url.ts b/src/fp-cloud/pre-signed-url.ts new file mode 100644 index 00000000..a5f25de7 --- /dev/null +++ b/src/fp-cloud/pre-signed-url.ts @@ -0,0 +1,80 @@ +import { Result, URI } from "@adviser/cement"; +import { AwsClient } from "aws4fetch"; +import { MsgWithConn, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; + +export interface PreSignedMsg extends MsgWithTenantLedger { + readonly params: SignedUrlParam; +} + +// export interface PreSignedConnMsg { +// readonly params: SignedUrlParam; +// readonly tid: string; +// readonly conn: QSId; +// } + +export interface PreSignedEnv { + readonly storageUrl: URI; + readonly aws: { + readonly accessKeyId: string; + readonly secretAccessKey: string; + readonly region?: string; + }; + readonly test?: { + readonly amzDate?: string; + }; +} + +export async function calculatePreSignedUrl(psm: PreSignedMsg, env: PreSignedEnv): Promise> { + // if (!ipsm.conn) { + // return Result.Err(new Error("Connection is not supported")); + // } + // const psm = ipsm as PreSignedConnMsg; + + // verify if you are not overriding + let store: string = psm.params.store; + if (psm.params.index?.length) { + store = `${store}-${psm.params.index}`; + } + const expiresInSeconds = psm.params.expires || 60 * 60; + + const suffix = ""; + // switch (psm.params.store) { + // case "wal": + // case "meta": + // suffix = ".json"; + // break; + // default: + // break; + // } + + const opUrl = env.storageUrl + .build() + // .protocol(vals.protocuol === "ws" ? "http:" : "https:") + .setParam("X-Amz-Expires", expiresInSeconds.toString()) + .setParam("tid", psm.tid) + .appendRelative(psm.tenant.tenant) + .appendRelative(psm.tenant.ledger) + .appendRelative(store) + .appendRelative(`${psm.params.key}${suffix}`) + .URI(); + const a4f = new AwsClient({ + ...env.aws, + region: env.aws.region || "us-east-1", + service: "s3", + }); + const signedUrl = await a4f + .sign( + new Request(opUrl.toString(), { + method: psm.params.method, + }), + { + aws: { + signQuery: true, + datetime: env.test?.amzDate, + // datetime: env.TEST_DATE, + }, + } + ) + .then((res) => res.url); + return Result.Ok(URI.from(signedUrl)); +} diff --git a/src/fp-cloud/test-helper.ts b/src/fp-cloud/test-helper.ts new file mode 100644 index 00000000..144ff493 --- /dev/null +++ b/src/fp-cloud/test-helper.ts @@ -0,0 +1,217 @@ +import { Future, Result, URI } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; +import { $, fs } from "zx"; +import { HttpConnection } from "./http-connection.js"; +import { + MsgerParams, + Gestalt, + defaultGestalt, + buildReqGestalt, + MsgIsResGestalt, + MsgIsError, + MsgBase, +} from "./msg-types.js"; +import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection } from "./msger.js"; +import { WSConnection } from "./ws-connection.js"; +import * as toml from "smol-toml"; +import { Env } from "./backend/env.js"; +import { HonoServer } from "./hono-server.js"; +import { NodeHonoFactory } from "./node-hono-server.js"; +import { CFHonoFactory } from "./backend/cf-hono-server.js"; +import { BetterSQLDatabase } from "./meta-merger/bettersql-abstract-sql.js"; + +export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { + const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["reqRes"] }), { + id: "HTTP-server", + }); + const exGt = { my, remote }; + return { + name: "HTTP", + remoteGestalt: remote, + cInstance: HttpConnection, + ok: { + url: () => URI.from(`http://127.0.0.1:${port}/fp`), + open: () => + applyStart( + Msger.openHttp( + sthis, + [URI.from(`http://localhost:${port}/fp`)], + { + ...msgP, + // protocol: "http", + timeout: 1000, + }, + exGt + ) + ), + }, + connRefused: { + url: () => URI.from(`http://127.0.0.1:${port - 1}/fp`), + open: async (): Promise>> => { + const ret = await Msger.openHttp( + sthis, + [URI.from(`http://localhost:${port - 1}/fp`)], + { + ...msgP, + // protocol: "http", + timeout: 1000, + }, + exGt + ); + if (ret.isErr()) { + return ret; + } + // should fail + const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); + if (MsgIsError(res)) { + return Result.Err(res.message); + } + return ret; + }, + }, + timeout: { + url: () => URI.from(`http://4.7.1.1:${port}/fp`), + open: async (): Promise>> => { + const ret = await Msger.openHttp( + sthis, + [URI.from(`http://4.7.1.1:${port}/fp`)], + { + ...msgP, + // protocol: "http", + timeout: 500, + }, + exGt + ); + // should fail + const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); + if (MsgIsError(res)) { + return Result.Err(res.message); + } + return ret; + }, + }, + }; +} + +export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { + const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["stream"] }), { + id: "WS-server", + }); + const exGt = { my, remote }; + return { + name: "WS", + remoteGestalt: remote, + cInstance: WSConnection, + ok: { + url: () => URI.from(`http://127.0.0.1:${port}/ws`), + open: () => + applyStart( + Msger.openWS( + sthis, + URI.from(`http://localhost:${port}/ws`), + { + ...msgP, + // protocol: "ws", + timeout: 1000, + }, + exGt + ) + ), + }, + connRefused: { + url: () => URI.from(`http://127.0.0.1:${port - 1}/ws`), + open: () => + Msger.openWS( + sthis, + URI.from(`http://localhost:${port - 1}/ws`), + { + ...msgP, + // protocol: "ws", + timeout: 1000, + }, + exGt + ), + }, + timeout: { + url: () => URI.from(`http://4.7.1.1:${port - 1}/ws`), + open: () => + Msger.openWS( + sthis, + URI.from(`http://4.7.1.1:${port - 1}/ws`), + { + ...msgP, + // protocol: "ws", + timeout: 500, + }, + exGt + ), + }, + }; +} + +export async function resolveToml(backend: "D1" | "DO") { + const tomlFile = "src/cloud/backend/wrangler.toml"; + const tomeStr = await fs.readFile(tomlFile, "utf-8"); + const wranglerFile = toml.parse(tomeStr) as unknown as { + env: Record; + }; + return { + tomlFile, + env: wranglerFile.env[`test-reqRes-${backend}`].vars, + }; +} + +export function NodeHonoServerFactory() { + return { + name: "NodeHonoServer", + factory: async (sthis: SuperThis, msgP: MsgerParams, remoteGestalt: Gestalt, _port: number) => { + const { env } = await resolveToml("D1"); + sthis.env.sets(env as unknown as Record); + const nhf = new NodeHonoFactory(sthis, { + msgP, + gs: remoteGestalt, + sql: new BetterSQLDatabase("./dist/node-meta.sqlite"), + }); + return new HonoServer(nhf); + }, + }; +} +export function CFHonoServerFactory(backend: "D1" | "DO") { + return { + name: `CFHonoServer(${backend})`, + factory: async (_sthis: SuperThis, _msgP: MsgerParams, remoteGestalt: Gestalt, port: number) => { + if (process.env.FP_WRANGLER_PORT) { + return new HonoServer(new CFHonoFactory()); + } + const { tomlFile } = await resolveToml(backend); + $.verbose = !!process.env.FP_DEBUG; + const runningWrangler = $` + wrangler dev -c ${tomlFile} --port ${port} --env test-${remoteGestalt.protocolCapabilities[0]}-${backend} --no-show-interactive-dev-session & + waitPid=$! + echo "PID:$waitPid" + wait $waitPid`; + const waitReady = new Future(); + let pid: number | undefined; + runningWrangler.stdout.on("data", (chunk) => { + // console.log(">>", chunk.toString()) + const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; + if (mightPid) { + pid = +mightPid; + } + if (chunk.includes("Ready on http")) { + waitReady.resolve(true); + } + }); + runningWrangler.stderr.on("data", (chunk) => { + // eslint-disable-next-line no-console + console.error("!!", chunk.toString()); + }); + await waitReady.asPromise(); + return new HonoServer( + new CFHonoFactory(() => { + if (pid) process.kill(pid); + }) + ); + }, + }; +} diff --git a/src/fp-cloud/ws-connection.ts b/src/fp-cloud/ws-connection.ts new file mode 100644 index 00000000..5c29cfd7 --- /dev/null +++ b/src/fp-cloud/ws-connection.ts @@ -0,0 +1,211 @@ +import { exception2Result, Future, Logger, Result } from "@adviser/cement"; +import { SuperThis, ensureLogger } from "@fireproof/core"; +import { + MsgBase, + MsgIsError, + buildErrorMsg, + ReqOpen, + WaitForTid, + MsgWithError, + RequestOpts, + MsgIsTid, +} from "./msg-types.js"; +import { ActiveStream, ExchangedGestalt, MsgerParamsWithEnDe, MsgRawConnection, OnMsgFn, UnReg } from "./msger.js"; +import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; + +export interface WSReqOpen { + readonly reqOpen: ReqOpen; + readonly ws: WebSocket; // this WS is opened with a specific URL-Param +} + +export class WSConnection extends MsgRawConnectionBase implements MsgRawConnection { + readonly logger: Logger; + readonly msgP: MsgerParamsWithEnDe; + readonly ws: WebSocket; + // readonly baseURI: URI; + + readonly #onMsg = new Map(); + readonly #onClose = new Map(); + + readonly waitForTid = new Map(); + + opened = false; + + readonly id: string; + + constructor(sthis: SuperThis, ws: WebSocket, msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { + super(sthis, exGestalt); + this.id = sthis.nextId().str; + this.logger = ensureLogger(sthis, "WSConnection"); + this.msgP = msgP; + this.ws = ws; + // this.wqs = { ...wsq }; + } + + async start(): Promise> { + const onOpenFuture: Future> = new Future>(); + const timer = setTimeout(() => { + const err = this.logger.Error().Dur("timeout", this.msgP.timeout).Msg("Timeout").AsError(); + this.toMsg(buildErrorMsg(this.sthis, this.logger, {} as MsgBase, err)); + onOpenFuture.resolve(Result.Err(err)); + }, this.msgP.timeout); + this.ws.onopen = () => { + onOpenFuture.resolve(Result.Ok(undefined)); + this.opened = true; + }; + this.ws.onerror = (ierr) => { + const err = this.logger.Error().Err(ierr).Msg("WS Error").AsError(); + onOpenFuture.resolve(Result.Err(err)); + const res = this.buildErrorMsg(this.logger.Error(), {}, err); + this.toMsg(res); + }; + this.ws.onmessage = (evt) => { + if (!this.opened) { + this.toMsg( + buildErrorMsg( + this.sthis, + this.logger, + {} as MsgBase, + this.logger.Error().Msg("Received message before onOpen").AsError() + ) + ); + } + this.#wsOnMessage(evt); + }; + this.ws.onclose = () => { + this.opened = false; + this.close().catch((ierr) => { + const err = this.logger.Error().Err(ierr).Msg("close error").AsError(); + onOpenFuture.resolve(Result.Err(err)); + this.toMsg(buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, err)); + }); + }; + /* wait for onOpen */ + const rOpen = await onOpenFuture.asPromise().finally(() => { + clearTimeout(timer); + }); + if (rOpen.isErr()) { + return rOpen; + } + // const resOpen = await this.request(this.wqs.reqOpen, { waitFor: MsgIsResOpen }); + // if (!MsgIsResOpen(resOpen)) { + // return Result.Err(this.logger.Error().Any("ErrMsg", resOpen).Msg("Invalid response").AsError()); + // } + // this.wqs.resOpen = resOpen; + return Result.Ok(undefined); + } + + readonly #wsOnMessage = async (event: MessageEvent) => { + const rMsg = await exception2Result(() => this.msgP.ende.decode(event.data) as MsgBase); + if (rMsg.isErr()) { + this.logger.Error().Err(rMsg).Any(event.data).Msg("Invalid message"); + return; + } + const msg = rMsg.Ok(); + const waitFor = this.waitForTid.get(msg.tid); + this.#onMsg.forEach((cb) => cb(msg)); + if (waitFor) { + if (MsgIsError(msg)) { + this.waitForTid.delete(msg.tid); + waitFor.future.resolve(msg); + } else if (waitFor.waitFor(msg)) { + // what for a specific type + this.waitForTid.delete(msg.tid); + waitFor.future.resolve(msg); + } else { + // wild-card + this.waitForTid.delete(msg.tid); + waitFor.future.resolve(msg); + } + } + }; + + async close(): Promise> { + this.#onClose.forEach((fn) => fn()); + this.#onClose.clear(); + this.#onMsg.clear(); + this.ws.close(); + return Result.Ok(undefined); + } + + toMsg(msg: MsgWithError): MsgWithError { + this.#onMsg.forEach((fn) => fn(msg)); + return msg; + } + + sendMsg(msg: MsgBase): Promise { + this.ws.send(this.msgP.ende.encode(msg)); + return Promise.resolve(); + } + + onMsg(fn: OnMsgFn): UnReg { + const key = this.sthis.nextId().str; + this.#onMsg.set(key, fn as OnMsgFn); + return () => this.#onMsg.delete(key); + } + + onClose(fn: UnReg): UnReg { + const key = this.sthis.nextId().str; + this.#onClose.set(key, fn); + return () => this.#onClose.delete(key); + } + + readonly activeBinds = new Map>(); + bind(req: Q, opts: RequestOpts): ReadableStream> { + const state: ActiveStream = { + id: this.sthis.nextId().str, + bind: { + msg: req, + opts, + }, + // timeout: undefined, + // controller: undefined, + } satisfies ActiveStream; + this.activeBinds.set(state.id, state); + return new ReadableStream>({ + cancel: () => { + // clearTimeout(state.timeout as number); + this.activeBinds.delete(state.id); + }, + start: (controller) => { + this.onMsg((msg) => { + if (MsgIsError(msg)) { + controller.enqueue(msg); + return; + } + if (!MsgIsTid(msg, req.tid)) { + return; + } + if (opts.waitFor && opts.waitFor(msg)) { + controller.enqueue(msg); + } + }); + this.sendMsg(req); + const future = new Future(); + this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); + future.asPromise().then((msg) => { + if (MsgIsError(msg)) { + // double err emitting + controller.enqueue(msg); + controller.close(); + } + }); + }, + }); + } + + async request(req: Q, opts: RequestOpts): Promise> { + if (!this.opened) { + return buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Msg("Connection not open").AsError()); + } + const future = new Future(); + this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); + await this.sendMsg(req); + return future.asPromise(); + } + + // toOnMessage(msg: WithErrorMsg): Result> { + // this.mec.msgFn?.(msg as unknown as MessageEvent); + // return Result.Ok(msg); + // } +} diff --git a/src/fp-cloud/ws-room.ts b/src/fp-cloud/ws-room.ts new file mode 100644 index 00000000..ebe8e33b --- /dev/null +++ b/src/fp-cloud/ws-room.ts @@ -0,0 +1,5 @@ +import { WSEvents } from "hono/ws"; + +export interface WSRoom { + acceptConnection(ws: WebSocket, wse: WSEvents): Promise; +} diff --git a/src/netlify/server.ts b/src/netlify/server.ts index 5c566131..754c1ede 100644 --- a/src/netlify/server.ts +++ b/src/netlify/server.ts @@ -1,4 +1,5 @@ import { getStore } from "@netlify/blobs"; +import { to_blob } from "../coerce-binary.js"; // eslint-disable-next-line no-console console.log("fireproof edge function loaded netlify"); @@ -18,12 +19,12 @@ export default async (req: Request) => { if (req.method === "PUT") { if (carId) { const carFiles = getStore("cars"); - const carArrayBuffer = new Uint8Array(await req.arrayBuffer()); + const carArrayBuffer = to_blob(await req.arrayBuffer()); await carFiles.set(carId, carArrayBuffer); return new Response(JSON.stringify({ ok: true }), { status: 201 }); } else if (metaDb) { const meta = getStore("meta"); - const x = await req.json(); + const x = (await req.json()) as CRDTEntry[]; // fixme, marty changed to [0] as it is a slice of the structure we expected const { data, cid, parents } = x[0]; await meta.setJSON(`${metaDb}/${cid}`, { data, parents }); diff --git a/src/partykit/partykit-gateway.test.ts b/src/partykit/partykit-gateway.test.ts index b1c1af00..77101264 100644 --- a/src/partykit/partykit-gateway.test.ts +++ b/src/partykit/partykit-gateway.test.ts @@ -3,7 +3,6 @@ import { registerPartyKitStoreProtocol } from "./gateway.js"; import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { Future, URI } from "@adviser/cement"; import { smokeDB } from "../../tests/helper.js"; -// import { storageURL } from "../connector.test.js"; // has to leave // interface ExtendedGateway extends bs.Gateway { diff --git a/src/sql/v0.19/sqlite/libsql/sqlite-connection.ts b/src/sql/v0.19/sqlite/libsql/sqlite-connection.ts new file mode 100644 index 00000000..06f5a68d --- /dev/null +++ b/src/sql/v0.19/sqlite/libsql/sqlite-connection.ts @@ -0,0 +1,79 @@ +import type { Client } from "@libsql/client"; +import { KeyedResolvOnce, ResolveOnce, URI } from "@adviser/cement"; + +import { SQLOpts } from "../../../types.js"; +import { ensureSuperLog, SuperThis } from "@fireproof/core"; +import { ensureSQLOpts } from "../../../ensurer.js"; +import { Sqlite3Connection, TasteHandler } from "../../sqlite_factory.js"; + +class LS3Taste implements TasteHandler { + readonly taste = "libsql" as const; + + quoteTemplate(o: unknown): Record { + return o as Record; + } + toBlob(data: Uint8Array): unknown { + return Buffer.from(data); + } + fromBlob(data: unknown): Uint8Array { + return Uint8Array.from(data as Buffer); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const onceImport = new ResolveOnce(); +const onceSQLiteConnections = new KeyedResolvOnce(); +export class V0_19LS3Connection extends Sqlite3Connection { + get client(): Client { + if (!this._client) { + throw this.logger.Error().Msg("client not connected").AsError(); + } + return this._client as Client; + } + + constructor(_sthis: SuperThis, url: URI, opts: Partial) { + const sthis = ensureSuperLog(_sthis, "V0_19LS3Connection", { url }); + super(sthis, url, ensureSQLOpts(sthis, url, opts), new LS3Taste()); + } + async connect(): Promise { + let fName = this.url.pathname; + if (!fName) { + throw this.logger.Error().Url(this.url).Msg("filename is empty").AsError(); + } + // const version = this.url.searchParams.get("version"); + // if (!version) { + // throw this.logger.Error().Str("url", this.url.toString()).Msg("version not found").AsError(); + // } + const hasName = this.url.getParam("name"); + if (hasName) { + fName = this.sthis.pathOps.join(fName, hasName); + if (!fName.endsWith(".sqlite")) { + fName += ".sqlite"; + } + } + this.logger.Debug().Str("filename", fName).Msg("to-connect"); + this._client = await onceSQLiteConnections.get(fName).once(async () => { + this.logger.Debug().Str("filename", fName).Msg("connect"); + const Sqlite3Database = await onceImport.once(async () => { + const sql = await import("better-sqlite3"); + return sql.default; + }); + if (hasName) { + await (await this.fs()).mkdir(this.sthis.pathOps.dirname(fName), { recursive: true }); + } + const db = new Sqlite3Database(fName, { + // verbose: console.log, + nativeBinding: "./node_modules/better-sqlite3/build/Release/better_sqlite3.node", + }); + // this.logger.Debug().Any("client", this.client).Msg("connected") + if (!db) { + throw this.logger.Error().Msg("connect failed").AsError(); + } + return db; + }); + } + async close(): Promise { + this.logger.Debug().Msg("close"); + await this.client.close(); + } +} diff --git a/src/sql/v0.19/sqlite_factory.ts b/src/sql/v0.19/sqlite_factory.ts index 0ac0c57a..0f7900b4 100644 --- a/src/sql/v0.19/sqlite_factory.ts +++ b/src/sql/v0.19/sqlite_factory.ts @@ -28,7 +28,7 @@ export async function v0_19sqliteMetaFactory(sthis: SuperThis, db: DBConnection) // prepare(sql: string): Sqlite3Statement; // } -export type Sqlite3Taste = "better-sqlite3" | "node-sqlite3-wasm"; +export type Sqlite3Taste = "better-sqlite3" | "node-sqlite3-wasm" | "libsql"; export interface TasteHandler { readonly taste: Sqlite3Taste; quoteTemplate(o: unknown): Record; @@ -67,6 +67,15 @@ export async function v0_19sqliteConnectionFactory( ): Promise { const upUrl = url.build().defParam("taste", "better-sqlite3").URI(); switch (upUrl.getParam("taste")) { + case "libsql": { + const { V0_19LS3Connection } = await import("./sqlite/libsql/sqlite-connection.js"); + sthis.logger.Debug().Str("databaseURL", upUrl.toString()).Msg("connecting to better-sqlite3"); + return { + dbConn: new V0_19LS3Connection(sthis, upUrl, opts), + url: upUrl.build().setParam("taste", "libsql").URI(), + }; + } + case "node-sqlite3-wasm": { const { V0_19NSWConnection } = await import("./sqlite/node-sqlite3-wasm/sqlite-connection.js"); sthis.logger.Debug().Str("databaseURL", upUrl.toString()).Msg("connecting to node-sqlite3-wasm"); diff --git a/src/ucan/client.ts b/src/ucan/client.ts index 3e545aa0..382a2d3a 100644 --- a/src/ucan/client.ts +++ b/src/ucan/client.ts @@ -10,6 +10,7 @@ import { sha256 } from "multiformats/hashes/sha2"; import * as ClockCaps from "./clock/capabilities.js"; import * as StoreCaps from "./store/capabilities.js"; import { Server, type Clock, type Service } from "./types.js"; +import { to_uint8 } from "../coerce-binary.js"; //////////////////////////////////////// // CLOCK @@ -140,6 +141,13 @@ export async function registerClock({ //////////////////////////////////////// // CONNECTION //////////////////////////////////////// +export function coerceHeaders(headers: Record | Headers): Record { + if ("entries" in headers) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Object.fromEntries((headers as any).entries()); + } + return headers as Record; +} export function service(server: Server): ConnectionView { const url = server.uri.toString(); @@ -156,8 +164,8 @@ export function service(server: Server): ConnectionView { const buffer = response.ok ? await response.arrayBuffer() : new Uint8Array(); return { - headers: response.headers.entries ? Object.fromEntries(response.headers.entries()) : {}, - body: new Uint8Array(buffer), + headers: coerceHeaders(response.headers), + body: to_uint8(buffer), }; }, }; diff --git a/src/ucan/common.ts b/src/ucan/common.ts index 319033d5..baa1fa0d 100644 --- a/src/ucan/common.ts +++ b/src/ucan/common.ts @@ -3,6 +3,33 @@ import { Delegation } from "@ucanto/interface"; import type { Agent, AgentDataExport, DelegationMeta } from "@web3-storage/access/types"; import { Block } from "multiformats/block"; import { CID } from "multiformats"; +import { to_arraybuf } from "../coerce-binary.js"; + +import type { Service } from "./types.js"; + +export function agentProofs( + agent: Agent, + mailtoDID?: `did:mailto:${string}:${string}` +): { attestations: Delegation[]; delegations: Delegation[] } { + const proofs = agent.proofs([{ with: /did:mailto:.*/, can: "*" }]); + const delegations = proofs.filter( + (p) => p.capabilities[0].can === "*" && (mailtoDID ? p.issuer.did() === mailtoDID : true) + ); + + const delegationCids = delegations.map((d) => d.cid.toString()); + const attestations = proofs.filter((p) => { + const cap = p.capabilities[0]; + return ( + cap.can === "ucan/attest" && + delegationCids.includes((cap.nb as { proof: { toString(): string } }).proof.toString()) + ); + }); + + return { + delegations, + attestations, + }; +} import type { Service } from "./types.js"; @@ -58,7 +85,7 @@ export function exportDelegation(del: Delegation): [ meta: {}, delegation: [...del.export()].map((b) => ({ cid: b.cid.toString(), - bytes: b.bytes, + bytes: to_arraybuf(b.bytes), })), }, ]; diff --git a/tests/Dockerfile.connect-netlify b/tests/Dockerfile.connect-netlify index bd242133..29d10c8b 100644 --- a/tests/Dockerfile.connect-netlify +++ b/tests/Dockerfile.connect-netlify @@ -8,6 +8,7 @@ RUN curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && deno #-- --no-modify-path --yes COPY src/netlify /usr/src/app/src/netlify COPY tests/connect-netlify/app /usr/src/app/tests/connect-netlify/app +COPY dist/netlify/server.js /usr/src/app/tests/connect-netlify/app/netlify/edge-functions/fireproof.js WORKDIR /usr/src/app/tests/connect-netlify/app RUN rm -rf node_modules && pnpm install && pnpm run copy-server && pnpm install -g netlify-cli CMD pnpm dev --no-open diff --git a/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts b/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts index 0dee93de..50049f45 100644 --- a/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts +++ b/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts @@ -1,4 +1,5 @@ import { getStore } from "@netlify/blobs"; +import { to_blob } from "../../../../../src/coerce-binary.js"; // eslint-disable-next-line no-console console.log("fireproof edge function loaded netlify"); @@ -18,12 +19,12 @@ export default async (req: Request) => { if (req.method === "PUT") { if (carId) { const carFiles = getStore("cars"); - const carArrayBuffer = new Uint8Array(await req.arrayBuffer()); + const carArrayBuffer = to_blob(await req.arrayBuffer()); await carFiles.set(carId, carArrayBuffer); return new Response(JSON.stringify({ ok: true }), { status: 201 }); } else if (metaDb) { const meta = getStore("meta"); - const x = await req.json(); + const x = await req.json() as CRDTEntry[]; // fixme, marty changed to [0] as it is a slice of the structure we expected const { data, cid, parents } = x[0]; await meta.setJSON(`${metaDb}/${cid}`, { data, parents }); diff --git a/tsconfig.json b/tsconfig.json index 5d914c56..4093d93f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,11 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "esnext", + "dom" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -119,10 +122,12 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*", "tests/**/*"], - "typeRoots": ["ts-types", "node_modules/@types"], + "typeRoots": ["ts-types", "node_modules/@types"] + /* "ts-node": { "compilerOptions": { "module": "CommonJS" } } + */ } diff --git a/version-copy-package.ts b/version-copy-package.ts index 8f8986bf..1db77a7a 100644 --- a/version-copy-package.ts +++ b/version-copy-package.ts @@ -1,3 +1,26 @@ +/* eslint-disable no-console */ +import { $ } from "zx"; +import { command, flag, positional, run, boolean, string } from "cmd-ts"; +import * as fs from "fs/promises"; +import * as path from "path"; + +async function patchVersion(packageJson: Record) { + let version = "refs/tags/v0.0.0-smoke"; + if (process.env.GITHUB_REF && process.env.GITHUB_REF.startsWith("refs/tags/v")) { + version = process.env.GITHUB_REF; + } + version = version.split("/").slice(-1)[0].replace(/^v/, ""); + console.log(`Patch version ${version} in package.json`); + packageJson.version = version; +} + +async function copyFilesToDist(destDir: string) { + for (const file of ["./.gitignore", "./LICENSE.md"]) { + await fs.copyFile(file, path.join(destDir, file)); + } +} + +async function main() { const cmd = command({ name: "version-copy-package", description: "prepare a package.json for a release", diff --git a/vitest.cf-kv.config.ts b/vitest.cf-worker.config.ts similarity index 63% rename from vitest.cf-kv.config.ts rename to vitest.cf-worker.config.ts index 65b18bea..de606e3f 100644 --- a/vitest.cf-kv.config.ts +++ b/vitest.cf-worker.config.ts @@ -2,16 +2,16 @@ import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineWorkersConfig({ - plugins: [tsconfigPaths()], + plugins: [tsconfigPaths() as Plugin], test: { poolOptions: { workers: { - wrangler: { configPath: "./wrangler.toml" }, + wrangler: { configPath: "./src/cloud/backend/wrangler.toml", environment: "test" }, }, }, - name: "cf-kv", + name: "cf-worker", exclude: ["node_modules/@fireproof/core/tests/react/**"], - include: ["src/cf-inde*.test.ts", "node_modules/@fireproof/core/tests/**/*test.?(c|m)[jt]s?(x)"], + include: ["src/cloud/meta-merger/*.test.ts"], globals: true, setupFiles: "./setup.cf-kv.ts", }, diff --git a/vitest.libsql.config.ts b/vitest.libsql.config.ts new file mode 100644 index 00000000..778772d7 --- /dev/null +++ b/vitest.libsql.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + name: "libsql", + include: ["src/sql/**/*test.?(c|m)[jt]s?(x)", "node_modules/@fireproof/core/tests/**/*test.?(c|m)[jt]s?(x)"], + exclude: [ + "node_modules/@fireproof/core/tests/react/**", + "node_modules/@fireproof/core/tests/fireproof/config.test.ts", + "node_modules/@fireproof/core/tests/fireproof/utils.test.ts", + ], + globals: true, + setupFiles: "./setup.libsql.ts", + }, +}); diff --git a/vitest.v1-cloud.config.ts b/vitest.v1-cloud.config.ts index 5da6fc0b..0f918785 100644 --- a/vitest.v1-cloud.config.ts +++ b/vitest.v1-cloud.config.ts @@ -1,6 +1,6 @@ -import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vitest/config"; -// import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [tsconfigPaths()], @@ -9,8 +9,6 @@ export default defineConfig({ exclude: [ "node_modules/@fireproof/core/tests/react/**", "node_modules/@fireproof/core/tests/fireproof/config.test.ts", - "node_modules/@fireproof/core/tests/blockstore/keyed-crypto*", - "node_modules/@fireproof/core/tests/**/utils.test.ts", ], include: [ // "node_modules/@fireproof/core/tests/**/*test.?(c|m)[jt]s?(x)", @@ -21,8 +19,5 @@ export default defineConfig({ globals: true, setupFiles: "./setup.v1-cloud.ts", testTimeout: 25000, - // poolOptions: { - // workers: { wrangler: { configPath: './src/cloud/backend/wrangler.toml' } }, - // }, }, }); diff --git a/vitest.v2-cloud.config.ts b/vitest.v2-cloud.config.ts new file mode 100644 index 00000000..e4a250ab --- /dev/null +++ b/vitest.v2-cloud.config.ts @@ -0,0 +1,28 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; +// import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; + +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + name: "cloud", + exclude: [ + "node_modules/@fireproof/core/tests/react/**", + "node_modules/@fireproof/core/tests/fireproof/config.test.ts", + "node_modules/@fireproof/core/tests/blockstore/keyed-crypto*", + "node_modules/@fireproof/core/tests/**/utils.test.ts", + ], + include: [ + // "node_modules/@fireproof/core/tests/**/*test.?(c|m)[jt]s?(x)", + // "node_modules/@fireproof/core/tests/**/*gateway.test.?(c|m)[jt]s?(x)", + // "src/connector.test.ts", + "src/cloud/**/*test.?(c|m)[jt]s?(x)", + ], + globals: true, + setupFiles: "./setup.cloud.ts", + testTimeout: 25000, + // poolOptions: { + // workers: { wrangler: { configPath: './src/cloud/backend/wrangler.toml' } }, + // }, + }, +}); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 67824389..c50b34ab 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -3,11 +3,15 @@ import { defineWorkspace } from "vitest/config"; import aws from "./vitest.aws.config.ts"; import betterSqlite3 from "./vitest.better-sqlite3.config.ts"; // import nodeSqlite3Wasm from "./vitest.node-sqlite3-wasm.config.ts"; +import libsql from "./vitest.libsql.config.ts"; +// import nodeSqlite3Wasm from "./vitest.node-sqlite3-wasm.config.ts"; import partykit from "./vitest.partykit.config.ts"; import v1Cloud from "./vitest.v1-cloud.config.ts"; import s3 from "./vitest.s3.config.ts"; // import connector from "./vitest.connector.config.ts"; // import netlify from "./vitest.netlify.config.ts"; +// import ucan from "./vitest.ucan.config.ts"; +import cfWorker from "./vitest.cf-worker.config.ts"; import ucan from "./vitest.ucan.config.ts"; import metaHack from "./vitest.meta-hack.config.ts"; // import cf_kv from "./vitest.cf-kv.config.ts"; @@ -15,6 +19,7 @@ import metaHack from "./vitest.meta-hack.config.ts"; export default defineWorkspace([ // nodeSqlite3Wasm, betterSqlite3, + libsql, // connector, metaHack, s3, @@ -22,6 +27,6 @@ export default defineWorkspace([ // netlify, partykit, v1Cloud, - //cf_kv + cfWorker, ucan, ]); From 9abfa6a95f6aba92252ef99f6d03b23c427d97ab Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 3 Mar 2025 12:39:00 +0100 Subject: [PATCH 03/14] chore: CF and Multiple getWebSockets and hybrid node/cf --- netlify.toml | 5 - package.json | 13 +- pnpm-lock.yaml | 7358 ++++++++++++++++- setup.ucan.ts | 4 +- setup.cloud.ts => setup.v2-cloud.ts | 0 src/aws/gateway.ts | 1 + src/coerce-binary.ts | 16 +- src/fp-cloud/backend/cf-dobj-abstract-sql.ts | 31 - src/fp-cloud/backend/cf-hono-server.ts | 194 - src/fp-cloud/backend/env.d.ts | 59 - src/fp-cloud/backend/fp-meta-groups.ts-off | 47 - src/fp-cloud/backend/server.ts | 72 - src/fp-cloud/backend/wrangler.toml | 143 - src/fp-cloud/client/README.md | 58 - src/fp-cloud/client/cli-pre-signed-url.ts | 119 - src/fp-cloud/client/cloud-gateway.test.ts | 123 - src/fp-cloud/client/gateway.ts | 605 -- src/fp-cloud/client/index.ts | 125 - src/fp-cloud/cloud.test.ts-off | 533 -- src/fp-cloud/connection.test.ts | 362 - src/fp-cloud/hono-server.ts | 265 - src/fp-cloud/http-connection.ts | 178 - src/fp-cloud/meta-merger/abstract-sql.ts | 53 - .../meta-merger/bettersql-abstract-sql.ts | 36 - .../meta-merger/cf-worker-abstract-sql.ts | 31 - src/fp-cloud/meta-merger/create-schema-cli.ts | 9 - .../meta-merger/meta-by-tenant-ledger.ts | 173 - src/fp-cloud/meta-merger/meta-merger.test.ts | 245 - src/fp-cloud/meta-merger/meta-merger.ts | 116 - src/fp-cloud/meta-merger/meta-send.ts | 128 - src/fp-cloud/meta-merger/tenant-ledger.ts | 62 - src/fp-cloud/meta-merger/tenant.ts | 51 - src/fp-cloud/msg-dispatch.ts | 139 - src/fp-cloud/msg-dispatcher-impl.ts | 127 - src/fp-cloud/msg-processor.ts-off | 261 - src/fp-cloud/msg-raw-connection-base.ts | 31 - src/fp-cloud/msg-request.ts | 220 - src/fp-cloud/msg-type-meta.ts | 160 - src/fp-cloud/msg-types-data.ts | 109 - src/fp-cloud/msg-types-wal.ts | 130 - src/fp-cloud/msg-types.ts | 567 -- src/fp-cloud/msger.ts | 274 - src/fp-cloud/new-websocket.ts | 11 - src/fp-cloud/node-hono-server.ts | 142 - src/fp-cloud/pre-signed-url.ts | 80 - src/fp-cloud/test-helper.ts | 217 - src/fp-cloud/ws-connection.ts | 211 - src/fp-cloud/ws-room.ts | 5 - src/netlify/{ => backend}/server.ts | 21 +- src/netlify/gateway.ts | 11 +- src/sql/gateway-sql.ts | 2 +- src/sql/v0.19/sqlite_factory.ts | 2 +- src/ucan/client.ts | 19 +- src/ucan/common.ts | 28 +- src/ucan/ucan-gateway.test.ts | 12 +- src/ucan/ucan-gateway.ts | 2 +- src/v2-cloud/backend/cf-hono-server.ts | 389 +- src/v2-cloud/backend/env.d.ts | 3 + src/v2-cloud/backend/server.ts | 90 +- src/v2-cloud/client/cloud-gateway.test.ts | 32 +- src/v2-cloud/client/gateway.ts | 69 +- src/v2-cloud/client/index.ts | 204 +- src/v2-cloud/hono-server.ts | 94 +- src/v2-cloud/http-connection.ts | 6 +- src/v2-cloud/meta-merger/meta-merger.ts | 3 +- src/v2-cloud/msg-dispatch.ts | 89 +- src/v2-cloud/msg-dispatcher-impl.ts | 46 +- src/v2-cloud/msg-types.ts | 81 +- src/v2-cloud/msger.ts | 20 +- src/v2-cloud/node-hono-server.ts | 167 +- src/v2-cloud/test-helper.ts | 8 +- src/v2-cloud/ws-connection.ts | 14 +- src/v2-cloud/ws-room.ts | 49 +- src/v2-cloud/ws-sockets.test.ts | 78 + tests/Dockerfile.connect-netlify | 27 +- tests/docker-compose.yaml | 3 +- vitest.cf-worker.config.ts | 4 +- vitest.libsql.config.ts | 5 + vitest.v2-cloud.config.ts | 6 +- vitest.workspace.ts | 2 + 80 files changed, 8448 insertions(+), 7037 deletions(-) delete mode 100644 netlify.toml rename setup.cloud.ts => setup.v2-cloud.ts (100%) delete mode 100644 src/fp-cloud/backend/cf-dobj-abstract-sql.ts delete mode 100644 src/fp-cloud/backend/cf-hono-server.ts delete mode 100644 src/fp-cloud/backend/env.d.ts delete mode 100644 src/fp-cloud/backend/fp-meta-groups.ts-off delete mode 100644 src/fp-cloud/backend/server.ts delete mode 100644 src/fp-cloud/backend/wrangler.toml delete mode 100644 src/fp-cloud/client/README.md delete mode 100644 src/fp-cloud/client/cli-pre-signed-url.ts delete mode 100644 src/fp-cloud/client/cloud-gateway.test.ts delete mode 100644 src/fp-cloud/client/gateway.ts delete mode 100644 src/fp-cloud/client/index.ts delete mode 100644 src/fp-cloud/cloud.test.ts-off delete mode 100644 src/fp-cloud/connection.test.ts delete mode 100644 src/fp-cloud/hono-server.ts delete mode 100644 src/fp-cloud/http-connection.ts delete mode 100644 src/fp-cloud/meta-merger/abstract-sql.ts delete mode 100644 src/fp-cloud/meta-merger/bettersql-abstract-sql.ts delete mode 100644 src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts delete mode 100644 src/fp-cloud/meta-merger/create-schema-cli.ts delete mode 100644 src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts delete mode 100644 src/fp-cloud/meta-merger/meta-merger.test.ts delete mode 100644 src/fp-cloud/meta-merger/meta-merger.ts delete mode 100644 src/fp-cloud/meta-merger/meta-send.ts delete mode 100644 src/fp-cloud/meta-merger/tenant-ledger.ts delete mode 100644 src/fp-cloud/meta-merger/tenant.ts delete mode 100644 src/fp-cloud/msg-dispatch.ts delete mode 100644 src/fp-cloud/msg-dispatcher-impl.ts delete mode 100644 src/fp-cloud/msg-processor.ts-off delete mode 100644 src/fp-cloud/msg-raw-connection-base.ts delete mode 100644 src/fp-cloud/msg-request.ts delete mode 100644 src/fp-cloud/msg-type-meta.ts delete mode 100644 src/fp-cloud/msg-types-data.ts delete mode 100644 src/fp-cloud/msg-types-wal.ts delete mode 100644 src/fp-cloud/msg-types.ts delete mode 100644 src/fp-cloud/msger.ts delete mode 100644 src/fp-cloud/new-websocket.ts delete mode 100644 src/fp-cloud/node-hono-server.ts delete mode 100644 src/fp-cloud/pre-signed-url.ts delete mode 100644 src/fp-cloud/test-helper.ts delete mode 100644 src/fp-cloud/ws-connection.ts delete mode 100644 src/fp-cloud/ws-room.ts rename src/netlify/{ => backend}/server.ts (87%) create mode 100644 src/v2-cloud/ws-sockets.test.ts diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index c95da364..00000000 --- a/netlify.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] - base = "tests/connect-netlify/app" - command = "# no build command" - publish = "" - diff --git a/package.json b/package.json index 7e3f7090..3a5f8421 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "pretest-docker": "tsx tests/docker-compose.ts -f tests/docker-compose.yaml up -d --wait --build", "test": "vitest --run", "posttest-docker": "tsx tests/docker-compose.ts -f tests/docker-compose.yaml down", + "dev:netlify": "netlify functions:serve --functions src/netlify/backend --port 8888", "format": "prettier .", "lint": "eslint" }, @@ -68,6 +69,8 @@ "esbuild-plugin-replace": "^1.4.0", "esbuild-plugin-resolve": "^2.0.0", "eslint": "^9.22.0", + "netlify": "^13.3.3", + "netlify-cli": "^19.0.0", "partyserver": "^0.0.65", "prettier": "^3.5.3", "semver": "^7.7.1", @@ -104,11 +107,12 @@ "@cloudflare/workers-types": "^4.20250303.0", "@fireproof/core": "0.20.0-dev-preview-53", "@fireproof/vendor": "~2.0.0", - "@ipld/dag-ucan": "^3.4.5", + "@hono/node-server": "^1.13.8", "@hono/node-ws": "^1.0.4", + "@ipld/dag-ucan": "^3.4.5", "@jspm/core": "^2.1.0", - "@netlify/blobs": "^8.1.1", "@libsql/client": "^0.14.0", + "@netlify/blobs": "^8.1.1", "@ucanto/client": "^9.0.1", "@ucanto/core": "^10.3.1", "@ucanto/interface": "^10.2.0", @@ -126,7 +130,7 @@ "aws4fetch": "^1.0.20", "better-sqlite3": "^11.8.1", "cmd-ts": "^0.13.0", - "dotenv": "^16.4.5", + "dotenv": "^16.4.5", "events": "^3.3.0", "hono": "^4.6.13", "idb-keyval": "^6.2.1", @@ -155,12 +159,15 @@ }, "pnpm": { "onlyBuiltDependencies": [ + "@parcel/watcher", "aws-sdk", "better-sqlite3", "es5-ext", "esbuild", + "netlify-cli", "protobufjs", "sharp", + "unix-dgram", "workerd" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e8d7bf5..723ce34a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 3.758.0 '@cloudflare/vitest-pool-workers': specifier: ^0.7.7 - version: 0.7.7(@cloudflare/workers-types@4.20250303.0)(@vitest/runner@3.0.8)(@vitest/snapshot@3.0.8)(vitest@3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0)) + version: 0.7.7(@cloudflare/workers-types@4.20250303.0)(@vitest/runner@3.0.8)(@vitest/snapshot@3.0.8)(vitest@3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) '@cloudflare/workers-types': specifier: ^4.20250303.0 version: 4.20250303.0 @@ -38,6 +38,9 @@ importers: '@fireproof/vendor': specifier: ~2.0.0 version: 2.0.1 + '@hono/node-server': + specifier: ^1.13.8 + version: 1.13.8(hono@4.7.4) '@hono/node-ws': specifier: ^1.0.4 version: 1.1.0(@hono/node-server@1.13.8(hono@4.7.4))(hono@4.7.4) @@ -203,7 +206,13 @@ importers: version: 2.0.0 eslint: specifier: ^9.22.0 - version: 9.22.0 + version: 9.22.0(jiti@2.4.2) + netlify: + specifier: ^13.3.3 + version: 13.3.3 + netlify-cli: + specifier: ^19.0.0 + version: 19.0.0(@types/node@22.13.10)(aws4fetch@1.0.20)(encoding@0.1.13)(idb-keyval@6.2.1)(picomatch@4.0.2)(rollup@4.35.0) prettier: specifier: ^3.5.3 version: 3.5.3 @@ -215,7 +224,7 @@ importers: version: 3.8.1 tsup: specifier: ^8.4.0 - version: 8.4.0(postcss@8.5.3)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.7.0) + version: 8.4.0(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.7.0) tsx: specifier: ^4.19.3 version: 4.19.3 @@ -224,13 +233,13 @@ importers: version: 5.7.3 typescript-eslint: specifier: ^8.26.0 - version: 8.26.0(eslint@9.22.0)(typescript@5.7.3) + version: 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0)) + version: 5.1.4(typescript@5.7.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) vitest: specifier: ^3.0.8 - version: 3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + version: 3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) wait-on: specifier: ^8.0.2 version: 8.0.2 @@ -434,6 +443,61 @@ packages: resolution: {integrity: sha512-Zrjxi5qwGEcUsJ0ru7fRtW74WcTS0rbLcehoFB+rN1GRi2hbLcFaYs4PwVA5diLeAJH0gszv3x4Hr/S87MfbKQ==} engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.9': + resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.26.5': + resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.9': + resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} + engines: {node: '>=6.9.0'} + + '@bugsnag/browser@7.25.0': + resolution: {integrity: sha512-PzzWy5d9Ly1CU1KkxTB6ZaOw/dO+CYSfVtqxVJccy832e6+7rW/dvSw5Jy7rsNhgcKSKjZq86LtNkPSvritOLA==} + + '@bugsnag/browser@8.2.0': + resolution: {integrity: sha512-C4BfE3eVsjOAqoXbdrPXfKbgp/hz2H7mKBU0p11Jf9uz+5gUCfZK+39JLrQKvRXwqoDcTlBSfz9Xz5kXLyHg2Q==} + + '@bugsnag/core@7.25.0': + resolution: {integrity: sha512-JZLak1b5BVzy77CPcklViZrppac/pE07L3uSDmfSvFYSCGReXkik2txOgV05VlF9EDe36dtUAIIV7iAPDfFpQQ==} + + '@bugsnag/core@8.2.0': + resolution: {integrity: sha512-dFSs80ZwJ508nlC6UTLTUMdHgTaHY5UKvMiuHqstCQrQrOjqFcIv+x4o+l2WrSyOpoYhHAxDlKfzKN8AjwslQw==} + + '@bugsnag/cuid@3.2.1': + resolution: {integrity: sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==} + + '@bugsnag/js@7.25.0': + resolution: {integrity: sha512-d8n8SyKdRUz8jMacRW1j/Sj/ckhKbIEp49+Dacp3CS8afRgfMZ//NXhUFFXITsDP5cXouaejR9fx4XVapYXNgg==} + + '@bugsnag/js@8.2.0': + resolution: {integrity: sha512-DTtQwV1Ly5VXSOnVtzW8gSwB+ld3qIc/h0yMS836DEYUfA3V9JPwJE3+2EbD8Ea2ogkDWZ+a0jl0SNSNGiOmfA==} + + '@bugsnag/node@7.25.0': + resolution: {integrity: sha512-KlxBaJ8EREEsfKInybAjTO9LmdDXV3cUH5+XNXyqUZrcRVuPOu4j4xvljh+n24ifok/wbFZTKVXUzrN4iKIeIA==} + + '@bugsnag/node@8.2.0': + resolution: {integrity: sha512-6XC/KgX61m6YFgsBQP/GaH1UzlJkJmpi3AwlZQLsXloRh3O9lM/0EIk6+2sZm+vlz+GwxCFavcuIDgVmH/qi7Q==} + + '@bugsnag/safe-json-stringify@6.0.0': + resolution: {integrity: sha512-htzFO1Zc57S8kgdRK9mLcPVTW1BY2ijfH7Dk2CeZmspTWKdKqSo1iwmqrq2WtRjFlo8aRZYgLX0wFrDXF/9DLA==} + '@cloudflare/kv-asset-handler@0.3.4': resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} @@ -520,10 +584,21 @@ packages: '@cloudflare/workers-types@4.20250303.0': resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@dependents/detective-less@4.1.0': + resolution: {integrity: sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg==} + engines: {node: '>=14'} + '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} @@ -537,6 +612,18 @@ packages: peerDependencies: esbuild: '*' + '@esbuild/aix-ppc64@0.19.11': + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.21.2': + resolution: {integrity: sha512-/c7hocx0pm14bHQlqUVKmxwdT/e5/KkyoY1W8F9lk/8CkE037STDDz8PXUP/LE6faj2HqchvDs9GcShxFhI78Q==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -555,6 +642,18 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.19.11': + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.2': + resolution: {integrity: sha512-SGZKngoTWVUriO5bDjI4WDGsNx2VKZoXcds+ita/kVYB+8IkSCKDRDaK+5yu0b5S0eq6B3S7fpiEvpsa2ammlQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -573,6 +672,18 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.19.11': + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.2': + resolution: {integrity: sha512-G1ve3b4FeyJeyCjB4MX1CiWyTaIJwT9wAYE+8+IRA53YoN/reC/Bf2GDRXAzDTnh69Fpl+1uIKg76DiB3U6vwQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -591,6 +702,18 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.19.11': + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.2': + resolution: {integrity: sha512-1wzzNoj2QtNkAYwIcWJ66UTRA80+RTQ/kuPMtEuP0X6dp5Ar23Dn566q3aV61h4EYrrgGlOgl/HdcqN/2S/2vg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -609,6 +732,18 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.19.11': + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.2': + resolution: {integrity: sha512-ZyMkPWc5eTROcLOA10lEqdDSTc6ds6nuh3DeHgKip/XJrYjZDfnkCVSty8svWdy+SC1f77ULtVeIqymTzaB6/Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -627,6 +762,18 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.19.11': + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.2': + resolution: {integrity: sha512-K4ZdVq1zP9v51h/cKVna7im7G0zGTKKB6bP2yJiSmHjjOykbd8DdhrSi8V978sF69rkwrn8zCyL2t6I3ei6j9A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -645,6 +792,18 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.19.11': + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.2': + resolution: {integrity: sha512-4kbOGdpA61CXqadD+Gb/Pw3YXamQGiz9mal/h93rFVSjr5cgMnmJd/gbfPRm+3BMifvnaOfS1gNWaIDxkE2A3A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -663,6 +822,18 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.19.11': + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.2': + resolution: {integrity: sha512-ShS+R09nuHzDBfPeMUliKZX27Wrmr8UFp93aFf/S8p+++x5BZ+D344CLKXxmY6qzgTL3mILSImPCNJOzD6+RRg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -681,6 +852,18 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.19.11': + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.2': + resolution: {integrity: sha512-Hdu8BL+AmO+eCDvvT6kz/fPQhvuHL8YK4ExKZfANWsNe1kFGOHw7VJvS/FKSLFqheXmB3rTF3xFQIgUWPYsGnA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -699,6 +882,18 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.19.11': + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.2': + resolution: {integrity: sha512-nnGXjOAv+7cM3LYRx4tJsYdgy8dGDGkAzF06oIDGppWbUkUKN9SmgQA8H0KukpU0Pjrj9XmgbWqMVSX/U7eeTA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -717,6 +912,18 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.19.11': + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.2': + resolution: {integrity: sha512-m73BOCW2V9lcj7RtEMi+gBfHC6n3+VHpwQXP5offtQMPLDkpVolYn1YGXxOZ9hp4h3UPRKuezL7WkBsw+3EB3Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -735,6 +942,18 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.19.11': + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.2': + resolution: {integrity: sha512-84eYHwwWHq3myIY/6ikALMcnwkf6Qo7NIq++xH0x+cJuUNpdwh8mlpUtRY+JiGUc60yu7ElWBbVHGWTABTclGw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -753,6 +972,18 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.19.11': + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.2': + resolution: {integrity: sha512-9siSZngT0/ZKG+AH+/agwKF29LdCxw4ODi/PiE0F52B2rtLozlDP92umf8G2GPoVV611LN4pZ+nSTckebOscUA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -771,6 +1002,18 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.19.11': + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.2': + resolution: {integrity: sha512-y0T4aV2CA+ic04ULya1A/8M2RDpDSK2ckgTj6jzHKFJvCq0jQg8afQQIn4EM0G8u2neyOiNHgSF9YKPfuqKOVw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -789,6 +1032,18 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.19.11': + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.2': + resolution: {integrity: sha512-x5ssCdXmZC86L2Li1qQPF/VaC4VP20u/Zm8jlAu9IiVOVi79YsSz6cpPDYZl1rfKSHYCJW9XBfFCo66S5gVPSA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -807,6 +1062,18 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.19.11': + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.2': + resolution: {integrity: sha512-NP7fTpGSFWdXyvp8iAFU04uFh9ARoplFVM/m+8lTRpaYG+2ytHPZWyscSsMM6cvObSIK2KoPHXiZD4l99WaxbQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -825,6 +1092,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.19.11': + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.2': + resolution: {integrity: sha512-giZ/uOxWDKda44ZuyfKbykeXznfuVNkTgXOUOPJIjbayJV6FRpQ4zxUy9JMBPLaK9IJcdWtaoeQrYBMh3Rr4vQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -849,6 +1128,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.19.11': + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.2': + resolution: {integrity: sha512-IeFMfGFSQfIj1d4XU+6lkbFzMR+mFELUUVYrZ+jvWzG4NGvs6o53ReEHLHpYkjRbdEjJy2W3lTekTxrFHW7YJg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -873,6 +1164,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.19.11': + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.2': + resolution: {integrity: sha512-48QhWD6WxcebNNaE4FCwgvQVUnAycuTd+BdvA/oZu+/MmbpU8pY2dMEYlYzj5uNHWIG5jvdDmFXu0naQeOWUoA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -891,6 +1194,18 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.19.11': + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.2': + resolution: {integrity: sha512-90r3nTBLgdIgD4FCVV9+cR6Hq2Dzs319icVsln+NTmTVwffWcCqXGml8rAoocHuJ85kZK36DCteii96ba/PX8g==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -909,6 +1224,18 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.19.11': + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.2': + resolution: {integrity: sha512-sNndlsBT8OeE/MZDSGpRDJlWuhjuUz/dn80nH0EP4ZzDUYvMDVa7G87DVpweBrn4xdJYyXS/y4CQNrf7R2ODXg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -927,6 +1254,18 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.19.11': + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.2': + resolution: {integrity: sha512-Ti2QChGNFzWhUNNVuU4w21YkYTErsNh3h+CzvlEhzgRbwsJ7TrWQqRzW3bllLKKvTppuF3DJ3XP1GEg11AfrEQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -945,6 +1284,18 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.19.11': + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.2': + resolution: {integrity: sha512-VEfTCZicoZnZ6sGkjFPGRFFJuL2fZn2bLhsekZl1CJslflp2cJS/VoKs1jMk+3pDfsGW6CfQVUckP707HwbXeQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -995,10 +1346,32 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/accept-negotiator@1.1.0': + resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} + engines: {node: '>=14'} + + '@fastify/ajv-compiler@3.6.0': + resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@fastify/error@3.4.1': + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + + '@fastify/fast-json-stringify-compiler@4.3.0': + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + + '@fastify/merge-json-schemas@0.1.1': + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + + '@fastify/send@2.1.0': + resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} + + '@fastify/static@7.0.4': + resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} + '@fireproof/core@0.20.0-dev-preview-53': resolution: {integrity: sha512-Nlfxp/ZFovJfhM+kwNrv0hJPWROA9USvmIZgXaef1ZWKCBE4jikPgf9WziqX5aA8iK8QnOgrggeGaunNRpic8Q==} peerDependencies: @@ -1040,6 +1413,10 @@ packages: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} + '@humanwhocodes/momoa@2.0.4': + resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} + engines: {node: '>=10.10.0'} + '@humanwhocodes/retry@0.3.1': resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} @@ -1048,6 +1425,9 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1153,6 +1533,9 @@ packages: cpu: [x64] os: [win32] + '@import-maps/resolve@1.0.1': + resolution: {integrity: sha512-tWZNBIS1CoekcwlMuyG2mr0a1Wo5lb5lEHwwWvZo+5GLgr3e9LLDTtmgtCWEwBpXMkxn9D+2W9j2FY6eZQq0tA==} + '@ipld/car@5.4.0': resolution: {integrity: sha512-FiGxOhTUh3fn/kkA+YvNYQjA/T8T5DcKG0NZwAi3aXrizN1qm99HzdYTccEwcX/rUCtI8wTUCKDNPBLUb7pBIQ==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} @@ -1179,6 +1562,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jest/types@27.5.1': + resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1254,6 +1641,14 @@ packages: cpu: [x64] os: [win32] + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + '@multiformats/murmur3@2.1.8': resolution: {integrity: sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} @@ -1261,37 +1656,352 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@netlify/binary-info@1.0.0': + resolution: {integrity: sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==} + + '@netlify/blobs@7.4.0': + resolution: {integrity: sha512-7rdPzo8bggt3D2CVO+U1rmEtxxs8X7cLusDbHZRJaMlxqxBD05mXgThj5DUJMFOvmfVjhEH/S/3AyiLUbDQGDg==} + engines: {node: ^14.16.0 || >=16.0.0} + '@netlify/blobs@8.1.1': resolution: {integrity: sha512-7Dg3PzArvQ0Owq4wpkLECC9iaDBOxuWUN2uwbQtUF6tZssyez2QN+eO0CjuIhhZUivbw+X9bwsyiEjWkdJnv/A==} engines: {node: ^14.16.0 || >=16.0.0} - '@noble/curves@1.8.1': - resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} - engines: {node: ^14.21.3 || >=16} + '@netlify/build-info@8.0.0': + resolution: {integrity: sha512-WwExAgIkyznvT55bvS2G0Kk8s+jC/e/3KzrQhSXVrvDunfVRXi66xcIKzaKUcaqnC45odqJXfYs++w4P7QK2xw==} + engines: {node: ^14.16.0 || >=16.0.0} + hasBin: true - '@noble/ed25519@1.7.3': - resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} + '@netlify/build@29.58.10': + resolution: {integrity: sha512-nywuGn+b7ZWyjbPijAM0RQ7C3H9gsoZdjT5rhxSwYjiYn1DdqRCiWka2J9Ehr7MSayVUBMyUmjEAhwusiX+l3g==} + engines: {node: ^14.16.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@netlify/opentelemetry-sdk-setup': ^1.1.0 + '@opentelemetry/api': ~1.8.0 + peerDependenciesMeta: + '@netlify/opentelemetry-sdk-setup': + optional: true - '@noble/hashes@1.5.0': - resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} - engines: {node: ^14.21.3 || >=16} + '@netlify/cache-utils@5.2.0': + resolution: {integrity: sha512-kKzGQ9gKNRUjqFMC1/1goeTe1WfzL6KhphwXac7tialowg10Dtmr2X+eDzfH9enGvD6vhYR4a0QMTQWkjfPVmg==} + engines: {node: ^14.16.0 || >=16.0.0} - '@noble/hashes@1.7.1': - resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} - engines: {node: ^14.21.3 || >=16} + '@netlify/config@20.21.7': + resolution: {integrity: sha512-SoXHbUoJyj/XcIAmyHxJ4orD19nmjQWgycxZsVRUSpUu/JouV8nmD/kzMJinFbzgJdXXTcKWDCnDbpfRZE4q8Q==} + engines: {node: ^14.16.0 || >=16.0.0} + hasBin: true - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@netlify/edge-bundler@12.3.2': + resolution: {integrity: sha512-t1B+Eo5c+R4H7t20BGQHDIi3TwXYN9lPiCezJ16580fnWKGVwKoVW6/GvAPXURdlAHyq4+CZciGcxNWhWnTL7g==} + engines: {node: ^14.16.0 || >=16.0.0} - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@netlify/edge-functions@2.11.1': + resolution: {integrity: sha512-pyQOTZ8a+ge5lZlE+H/UAHyuqQqtL5gE0pXrHT9mOykr3YQqnkB2hZMtx12odatZ87gHg4EA+UPyMZUbLfnXvw==} - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + '@netlify/framework-info@9.9.1': + resolution: {integrity: sha512-UT7ipYfPRNo65S86fL07NECCLfW7yflQNtddJCWbJAYziAv7DRTwplZkaT/RBaKaIfEDC5yV6uumvYRQFy7PCQ==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@netlify/functions-utils@5.3.8': + resolution: {integrity: sha512-YAVztfC6PTQ9zC/nj/kO7yhQMDnBQccmdWo6haqbaOS9/mppt6od+E/yYX5tbV4r8YwL12vCddKRwyZiJjh1Ug==} + engines: {node: ^14.16.0 || >=16.0.0} + + '@netlify/git-utils@5.2.0': + resolution: {integrity: sha512-maNQyhQ6zTS5Kwl03HXoUa7uTNjmCvZea5Jko2pyDWz0xW1cunnil+4s33wXrMZJNDvyv97O2vkC5N1sAS3fyQ==} + engines: {node: ^14.16.0 || >=16.0.0} + + '@netlify/headers-parser@7.3.0': + resolution: {integrity: sha512-4RTR9X3bylRV+q1/OHSwXcXylYKr5+3mkKcL/QLBI+bTqvSO82vjWAQAqQfvWVSCaF6HrYORid3zLGzJ94YOSw==} + engines: {node: ^14.16.0 || >=16.0.0} + + '@netlify/local-functions-proxy-darwin-arm64@1.1.1': + resolution: {integrity: sha512-lphJ9qqZ3glnKWEqlemU1LMqXxtJ/tKf7VzakqqyjigwLscXSZSb6fupSjQfd4tR1xqxA76ylws/2HDhc/gs+Q==} + cpu: [arm64] + os: [darwin] + hasBin: true + + '@netlify/local-functions-proxy-darwin-x64@1.1.1': + resolution: {integrity: sha512-4CRB0H+dXZzoEklq5Jpmg+chizXlVwCko94d8+UHWCgy/bA3M/rU/BJ8OLZisnJaAktHoeLABKtcLOhtRHpxZQ==} + cpu: [x64] + os: [darwin] + hasBin: true + + '@netlify/local-functions-proxy-freebsd-arm64@1.1.1': + resolution: {integrity: sha512-u13lWTVMJDF0A6jX7V4N3HYGTIHLe5d1Z2wT43fSIHwXkTs6UXi72cGSraisajG+5JFIwHfPr7asw5vxFC0P9w==} + cpu: [arm64] + os: [freebsd] + hasBin: true + + '@netlify/local-functions-proxy-freebsd-x64@1.1.1': + resolution: {integrity: sha512-g5xw4xATK5YDzvXtzJ8S1qSkWBiyF8VVRehXPMOAMzpGjCX86twYhWp8rbAk7yA1zBWmmWrWNA2Odq/MgpKJJg==} + cpu: [x64] + os: [freebsd] + hasBin: true + + '@netlify/local-functions-proxy-linux-arm64@1.1.1': + resolution: {integrity: sha512-dPGu1H5n8na7mBKxiXQ+FNmthDAiA57wqgpm5JMAHtcdcmRvcXwJkwWVGvwfj8ShhYJHQaSaS9oPgO+mpKkgmA==} + cpu: [arm64] + os: [linux] + hasBin: true + + '@netlify/local-functions-proxy-linux-arm@1.1.1': + resolution: {integrity: sha512-YsTpL+AbHwQrfHWXmKnwUrJBjoUON363nr6jUG1ueYnpbbv6wTUA7gI5snMi/gkGpqFusBthAA7C30e6bixfiA==} + cpu: [arm] + os: [linux] + hasBin: true + + '@netlify/local-functions-proxy-linux-ia32@1.1.1': + resolution: {integrity: sha512-Ra0FlXDrmPRaq+rYH3/ttkXSrwk1D5Zx/Na7UPfJZxMY7Qo5iY4bgi/FuzjzWzlp0uuKZOhYOYzYzsIIyrSvmw==} + cpu: [ia32] + os: [linux] + hasBin: true + + '@netlify/local-functions-proxy-linux-ppc64@1.1.1': + resolution: {integrity: sha512-oXf1satwqwUUxz7LHS1BxbRqc4FFEKIDFTls04eXiLReFR3sqv9H/QuYNTCCDMuRcCOd92qKyDfATdnxT4HR8w==} + cpu: [ppc64] + os: [linux] + hasBin: true + + '@netlify/local-functions-proxy-linux-x64@1.1.1': + resolution: {integrity: sha512-bS3u4JuDg/eC0y4Na3i/29JBOxrdUvsK5JSjHfzUeZEbOcuXYf4KavTpHS5uikdvTgyczoSrvbmQJ5m0FLXfLA==} + cpu: [x64] + os: [linux] + hasBin: true + + '@netlify/local-functions-proxy-openbsd-x64@1.1.1': + resolution: {integrity: sha512-1xLef/kLRNkBTXJ+ZGoRFcwsFxd/B2H3oeJZyXaZ3CN5umd9Mv9wZuAD74NuMt/535yRva8jtAJqvEgl9xMSdA==} + cpu: [x64] + os: [openbsd] + hasBin: true + + '@netlify/local-functions-proxy-win32-ia32@1.1.1': + resolution: {integrity: sha512-4IOMDBxp2f8VbIkhZ85zGNDrZR4ey8d68fCMSOIwitjsnKav35YrCf8UmAh3UR6CNIRJdJL4MW1GYePJ7iJ8uA==} + cpu: [ia32] + os: [win32] + hasBin: true + + '@netlify/local-functions-proxy-win32-x64@1.1.1': + resolution: {integrity: sha512-VCBXBJWBujVxyo5f+3r8ovLc9I7wJqpmgDn3ixs1fvdrER5Ac+SzYwYH4mUug9HI08mzTSAKZErzKeuadSez3w==} + cpu: [x64] + os: [win32] + hasBin: true + + '@netlify/local-functions-proxy@1.1.1': + resolution: {integrity: sha512-eXSsayLT6PMvjzFQpjC9nkg2Otc3lZ5GoYele9M6f8PmsvWpaXRhwjNQ0NYhQQ2UZbLMIiO2dH8dbRsT3bMkFw==} + + '@netlify/open-api@2.36.0': + resolution: {integrity: sha512-cxdjUkHh0/SLvPusCFOmIoKpXdfvom+cpBT/bUrP2oxxH1htWgJ59GGuu/pJGEU+xhKpPotr+TSJl00u7ktIhg==} + engines: {node: '>=14.8.0'} + + '@netlify/opentelemetry-utils@1.3.0': + resolution: {integrity: sha512-2LpNZpowo7Q4nSNmPYcx4gAAXIhRiSanX7Bux7b0D7ohdaC8NOgmFc7vdVzIsCChLwqbQ4HZpN1fd0W40Cm7bg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@opentelemetry/api': ~1.8.0 + + '@netlify/plugins-list@6.80.0': + resolution: {integrity: sha512-bCKLI51UZ70ziIWsf2nvgPd4XuG6m8AMCoHiYtl/BSsiaSBfmryZnTTqdRXerH09tBRpbPPwzaEgUJwyU9o8Qw==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@netlify/redirect-parser@14.5.0': + resolution: {integrity: sha512-0HxtPj+azmoaQhuZAbyTn6WyMl+PO6XrFU6TRo/0b57jtVz9uTjrvFytjmTqTvVEY1sLePCxbTbgEULm2XDTjQ==} + engines: {node: ^14.16.0 || >=16.0.0} + + '@netlify/run-utils@5.2.0': + resolution: {integrity: sha512-bsrv7Sjge5g71VMgZ65Ioc5q4lHXdLQCmpUU6sY06Aeol1psi1iDOGVMx/7ExJjbCtQgxye35wZjAz60i6X22Q==} + engines: {node: ^14.16.0 || >=16.0.0} + + '@netlify/serverless-functions-api@1.34.0': + resolution: {integrity: sha512-jygLyd/kyaWPpyuHwGUoor6cUf2DXUOZbnU08Zk7nM8E0dNk4F5lCHQKeadgxOBLKL7Osx5NQ6qLtg5EKJf1zA==} + engines: {node: '>=18.0.0'} + + '@netlify/zip-it-and-ship-it@9.42.5': + resolution: {integrity: sha512-Z9nhchO9ZdqFpAVT6+cQMNAyt+MdhAalK4mCmCAe88YflHzZ0x8npevnZxtN4tytbW9nF4mywRMlP04BWOl5Ig==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + + '@netlify/zip-it-and-ship-it@9.42.6': + resolution: {integrity: sha512-1uSQUv/2qXD5TE2Qmf+GN7U7qV1/q0Bk8iQxfSkoc7g9aXScOYLa2e4QQXQ1CKJkZrZXvh3r4Emc2s+Lq44aeA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + + '@netlify/zip-it-and-ship-it@9.43.1': + resolution: {integrity: sha512-NPOntCuGmpulEUc3wpk3Fct7wI2KsrPnx7sCmEotNDJcLUtb0xEgNpBNclSGA6k5uQDhrLkC5TpaEnCkxjxGww==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + + '@noble/curves@1.8.1': + resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/ed25519@1.7.3': + resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} + + '@noble/hashes@1.5.0': + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.4': + resolution: {integrity: sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.3': + resolution: {integrity: sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.1': + resolution: {integrity: sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@23.0.1': + resolution: {integrity: sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==} + + '@octokit/plugin-paginate-rest@11.4.3': + resolution: {integrity: sha512-tBXaAbXkqVJlRoA/zQVe9mUdb8rScmivqtpv3ovsC5xhje/a+NOCivs7eUhWBwCApJVsR4G5HMeaLbq7PxqZGA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.3.1': + resolution: {integrity: sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.7': + resolution: {integrity: sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.2': + resolution: {integrity: sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==} + engines: {node: '>= 18'} + + '@octokit/rest@21.1.1': + resolution: {integrity: sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==} + engines: {node: '>= 18'} + + '@octokit/types@13.8.0': + resolution: {integrity: sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==} + + '@opentelemetry/api@1.8.0': + resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} + engines: {node: '>=8.0.0'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-wasm@2.5.1': + resolution: {integrity: sha512-RJxlQQLkaMMIuWRozy+z2vEqbaQlCuaCgVZIUCzQLYggY22LZbP5Y1+ia+FD724Ids9e+XIyOLXLrLgQSHIthw==} + engines: {node: '>= 10.0.0'} + bundledDependencies: + - napi-wasm + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + '@perma/map@1.0.3': resolution: {integrity: sha512-Bf5njk0fnJGTFE2ETntq0N1oJ6YdCPIpTDn3R3KYZJQdeYSOCNL7mBrFlGnbqav8YQhJA/p81pvHINX9vAtHkQ==} @@ -1299,6 +2009,18 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1329,6 +2051,15 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.35.0': resolution: {integrity: sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==} cpu: [arm] @@ -1445,6 +2176,18 @@ packages: '@sideway/pinpoint@2.0.0': resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@sindresorhus/slugify@2.2.1': + resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==} + engines: {node: '>=12'} + + '@sindresorhus/transliterate@1.6.0': + resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} + engines: {node: '>=12'} + '@smithy/abort-controller@4.0.1': resolution: {integrity: sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==} engines: {node: '>=18.0.0'} @@ -1660,6 +2403,29 @@ packages: '@storacha/one-webcrypto@1.0.1': resolution: {integrity: sha512-bD+vWmcgsEBqU0Dz04BR43SA03bBoLTAY29vaKasY9Oe8cb6XIP0/vkm0OS2UwKC13c8uRgFW4rjJUgDCNLejQ==} + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aws-lambda@8.10.147': resolution: {integrity: sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==} @@ -1673,6 +2439,21 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/http-proxy@1.17.16': + resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1682,9 +2463,18 @@ packages: '@types/node@22.13.10': resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/retry@0.12.1': + resolution: {integrity: sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==} + '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -1694,6 +2484,15 @@ packages: '@types/ws@8.18.0': resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@16.0.9': + resolution: {integrity: sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.26.0': resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1720,10 +2519,23 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/types@8.26.0': resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/typescript-estree@8.26.0': resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1737,6 +2549,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/visitor-keys@8.26.0': resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1762,6 +2578,11 @@ packages: '@ucanto/validator@9.1.0': resolution: {integrity: sha512-2JnEEZYc8kTZz+qbyL7LbI/TSpBRWLOchNhaRB5DTyj38FxMFQUXIB7gQ5lZHv49uYJGx/FXKPw0268WgJdEvg==} + '@vercel/nft@0.27.7': + resolution: {integrity: sha512-FG6H5YkP4bdw9Ll1qhmbxuE8KwW2E/g8fJpM183fWQLeVDGqzeywMIeJ9h2txdWZ03psgWMn6QymTxaDLmdwUg==} + engines: {node: '>=16'} + hasBin: true + '@vitest/expect@3.0.8': resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} @@ -1832,6 +2653,53 @@ packages: resolution: {integrity: sha512-EZdlJKGJtqAq+2BScekOeXkPtpcqiQKUAe94Ww4IHvlP6nm7L932+zTynYkQE0kKnspKH6qiqrv7SUP3oIUxnQ==} engines: {node: '>=18'} + '@xhmikosr/archive-type@6.0.1': + resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@xhmikosr/decompress-tar@7.0.0': + resolution: {integrity: sha512-kyWf2hybtQVbWtB+FdRyOT+jyR5jxCNZPLqvQGB7djZj75lrpLUPEmRbyo86AtJ5OEtivpYaNWjCkqSJ8xtRWw==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@xhmikosr/decompress-tarbz2@7.0.0': + resolution: {integrity: sha512-3QnjipYkRgh3Dee1MWDgKmANWxOQBVN4e1IwiGNe2fHYfMYTeSkVvWREt87UIoSucKUh3E95v8uGFttgTknZcA==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@xhmikosr/decompress-targz@7.0.0': + resolution: {integrity: sha512-7BNHJl92g9OLhw89zqcFS67V1LAtm4Ex02j6OiQzuE8P7Yy9lQcyBuEL3x6v436grLdL+BcFjgbmhWxnem4GHw==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@xhmikosr/decompress-unzip@6.0.0': + resolution: {integrity: sha512-R1HAkjXLS7RAL74YFLxYY9zYflCcYGssld9KKFDu87PnJ4h4btdhzXfSC8J5i5A2njH3oYIoCzx03RIGTH07Sg==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@xhmikosr/decompress@9.0.1': + resolution: {integrity: sha512-9Lvlt6Qdpo9SaRQyRIXCo3lgU++eMZ68lzgjcTwtuKDrlwT635+5zsHZ1yrSx/Blc5IDuVLlPkBPj5CZkx+2+Q==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@xhmikosr/downloader@13.0.1': + resolution: {integrity: sha512-mBvWew1kZJHfNQVVfVllMjUDwCGN9apPa0t4/z1zaUJ9MzpXjRL3w8fsfJKB8gHN/h4rik9HneKfDbh2fErN+w==} + engines: {node: ^14.14.0 || >=16.0.0} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1863,6 +2731,23 @@ packages: actor@2.3.1: resolution: {integrity: sha512-ST/3wnvcP2tKDXnum7nLCLXm+/rsf8vPocXH2Fre6D8FQwNkGDd4JEitBlXj007VQJfiGYRQvXqwOBZVi+JtRg==} + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + aggregate-error@4.0.1: + resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} + engines: {node: '>=12'} + + ajv-errors@3.0.0: + resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} + peerDependencies: + ajv: ^8.0.1 + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1871,12 +2756,51 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@5.0.0: + resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} + engines: {node: '>=12'} + + ansi-escapes@6.2.1: + resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==} + engines: {node: '>=14.16'} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1885,23 +2809,59 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansi-to-html@0.7.2: + resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==} + engines: {node: '>=8.0.0'} + hasBin: true + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} any-signal@3.0.1: resolution: {integrity: sha512-xgZgJtKEa9YmDqXodIgl7Fl1C8yNXr8w6gXjqK3LW4GcEiYT+6AQfJSE/8SPsEpLLmcvbv8YU+qet94UewHxqg==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + archy@1.0.0: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1920,6 +2880,9 @@ packages: resolution: {integrity: sha512-Dui8g/SEu2a8UY5f3GNzXx84WGIMYPrRaQtL5L3h1cxybDT5rsi/bkDpWq7y3/FZ5nvxyJNx4ld5C6qA3lqPDg==} engines: {node: '>=8.11.4', npm: 6.10.1} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-for-each-x@3.1.2: resolution: {integrity: sha512-dv8xr/sVk3d5BVyki+4CGDvJdyREEVnvV+RVLIH6IlQwpYtDpqqkpuL9Wv+io+lFprnCj0SORomlarIMdmNK/A==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -1940,12 +2903,26 @@ packages: resolution: {integrity: sha512-JccQYIaJSaDCcKsB3x7/SYnhpnKbeiyJT4g5DtkZllzfP9GfDCnl53ILaH3saiJcbFg83eWR4VbsGtzsiHLpYg==} engines: {node: '>=8.11.4', npm: 6.10.1} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + arraybuffer-equal@1.0.4: resolution: {integrity: sha512-B/HbicKd82qtq5NiNShqx6oAhHkYi+VFREUyHo9qdjyEwQDLL626XM80M88Pvr2TC7roUX3UsVALGvNIqeqc1g==} + arrify@3.0.0: + resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} + engines: {node: '>=12'} + as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + ascii-table@0.0.9: + resolution: {integrity: sha512-xpkr6sCDIYTPqzvjG8M3ncw1YOTaloWZOyrUmicoEifBEKzQzt+ooUpRpQ/AbOoJfO/p2ZKiyp79qHThzJDulQ==} + assert-is-function-x@3.1.2: resolution: {integrity: sha512-FiMv0RYR/eMQk2EOMYJKvm+QFQa2AQzjccI9rN4cpvNd6M+UuobnV6m0sqIO5U+Q2LillGwaya16MRGa2Sr6+w==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -1961,9 +2938,23 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-module-types@5.0.0: + resolution: {integrity: sha512-JvqziE0Wc0rXQfma0HZC/aY7URXHFuZV84fJRtP8u+lhp0JYCNd5wJzVXP45t0PH0Mej3ynlzvdyITYIu0G4LQ==} + engines: {node: '>=14'} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + atomically@2.0.3: resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} @@ -1975,6 +2966,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@8.4.0: + resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + aws-lambda@1.0.7: resolution: {integrity: sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w==} hasBin: true @@ -1989,12 +2983,53 @@ packages: axios@1.7.9: resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + backoff@2.5.0: + resolution: {integrity: sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==} + engines: {node: '>= 0.6'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.5.4: + resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} + + bare-fs@4.0.1: + resolution: {integrity: sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==} + engines: {bare: '>=1.7.0'} + + bare-os@3.5.1: + resolution: {integrity: sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + + better-ajv-errors@1.2.0: + resolution: {integrity: sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + ajv: 4.11.8 - 8 + better-sqlite3@11.8.1: resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} @@ -2010,6 +3045,10 @@ packages: resolution: {integrity: sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w==} engines: {node: '>=10.4.0'} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2022,9 +3061,20 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2038,6 +3088,19 @@ packages: browser-readablestream-to-it@1.0.3: resolution: {integrity: sha512-+12sHB+Br8HIh6VAMVEG5r3UXCyESIgDW7kzk3BjIXa43DVqVwL7GC5TW3jeh+72dtcH99pPVpw0X8i0jt+/kw==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@4.9.2: resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} @@ -2047,16 +3110,43 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: esbuild: '>=0.18' + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + cached-constructors-x@2.2.1: resolution: {integrity: sha512-GQFEIdT6mBLUfY986TdQA05WnmkkCFf0CDk++VfNraIWSCz0889bAu14FPMuoKNbVySAub7ItDa95ALp2008FQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2065,14 +3155,33 @@ packages: resolution: {integrity: sha512-7TsLYHEPvyCWaFwCQgCWcE6Lt5BowjA/TqggJFhE4hs4c4UM36H3Sodf5Jy0TxVepu0pYTWHtKaeOvoKrRl4JA==} engines: {node: '>=8.11.4', npm: 6.10.1} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + + callsite@1.0.0: + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + capnp-ts@0.7.0: resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} @@ -2087,10 +3196,21 @@ packages: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + charwise@3.0.1: resolution: {integrity: sha512-RcdumNsM6fJZ5HHbYunqj2bpurVRGsXour3OR+SlLEHFhG6ALm54i6Osnh+OvO7kEoSBzwExpblYFH8zKQiEPw==} @@ -2098,6 +3218,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -2105,17 +3229,71 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + clean-deep@3.4.0: + resolution: {integrity: sha512-Lo78NV5ItJL/jl+B5w0BycAisaieJGXK1qYi/9m4SjR8zbqmrUtO7Yhro40wEShGmmxs/aJLI/A+jNhdkXK8mw==} + engines: {node: '>=4'} + + clean-stack@4.2.0: + resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} + engines: {node: '>=12'} + + clean-stack@5.2.0: + resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} + engines: {node: '>=14.16'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-color@2.0.4: resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} engines: {node: '>=0.10'} + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + clipboardy@4.0.0: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} engines: {node: '>=18'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cmd-ts@0.13.0: resolution: {integrity: sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g==} @@ -2123,24 +3301,59 @@ packages: resolution: {integrity: sha512-UBj0YBtdMhubwaYnbfmCA8PHS79mbvCPYTw7YzUGhQzuyH3jf7Bc8RXQHqiZrTDtcl8+4oSbehFyu4jUH8PGag==} engines: {node: '>=8.11.4', npm: 6.10.1} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colors-option@3.0.0: + resolution: {integrity: sha512-DP3FpjsiDDvnQC1OJBsdOJZPuy7r0o6sepY2T5M3L/d2nrE23O/ErFkEqyY3ngVL1ZhTj/H0pCMNObZGkEOaaQ==} + engines: {node: '>=12.20.0'} + + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@3.0.2: resolution: {integrity: sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==} @@ -2148,6 +3361,25 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2155,22 +3387,120 @@ packages: resolution: {integrity: sha512-jjyhlQ0ew/iwmtwsS2RaB6s8DBifcE2GYBEaw2SJDUY/slJJbNfY4GlDVzOs/ff8cM/Wua5CikqXgbFl5eu85A==} engines: {node: '>=14.16'} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@7.0.0: + resolution: {integrity: sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==} + engines: {node: '>=18'} + consola@3.4.0: resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cp-file@10.0.0: + resolution: {integrity: sha512-vy2Vi1r2epK5WqxOLnskeKeZkdZvTKfFZQCplE3XWsP+SUJyd5XAUFC9lFgTjjXJF2GMne/UML14iEmkAaDfFg==} + engines: {node: '>=14.16'} + + cp-file@9.1.0: + resolution: {integrity: sha512-3scnzFj/94eb7y4wyXRWwvzLFaQp87yyfTnChIjlfYrVqp5lVO3E2hIJMeQIltUT0K2ZAB3An1qXcBmwGyvuwA==} + engines: {node: '>=10'} + + cpy@9.0.1: + resolution: {integrity: sha512-D9U0DR5FjTCN3oMTcFGktanHnAG5l020yvOCR1zKILmAyPP7I/9pl6NFgRbDcmSENtbK1sQLBz1p9HIOlroiNg==} + engines: {node: ^12.20.0 || ^14.17.0 || >=16.0.0} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.3.4: + resolution: {integrity: sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==} + + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + cyclist@1.0.2: + resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} + d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -2186,6 +3516,14 @@ packages: resolution: {integrity: sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==} engines: {node: '>=12'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -2204,6 +3542,9 @@ packages: supports-color: optional: true + decache@4.6.2: + resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2219,10 +3560,30 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -2234,6 +3595,29 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} @@ -2242,50 +3626,158 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detective-amd@5.0.2: + resolution: {integrity: sha512-XFd/VEQ76HSpym80zxM68ieB77unNuoMwopU2TFT/ErUk5n4KvUTwW4beafAVUugrjV48l4BmmR0rh2MglBaiA==} + engines: {node: '>=14'} + hasBin: true + + detective-cjs@5.0.1: + resolution: {integrity: sha512-6nTvAZtpomyz/2pmEmGX1sXNjaqgMplhQkskq2MLrar0ZAIkHMrDhLXkRiK2mvbu9wSWr0V5/IfiTrZqAQMrmQ==} + engines: {node: '>=14'} + + detective-es6@4.0.1: + resolution: {integrity: sha512-k3Z5tB4LQ8UVHkuMrFOlvb3GgFWdJ9NqAa2YLUU/jTaWJIm+JJnEh4PsMc+6dfT223Y8ACKOaC0qcj7diIhBKw==} + engines: {node: '>=14'} + + detective-postcss@6.1.3: + resolution: {integrity: sha512-7BRVvE5pPEvk2ukUWNQ+H2XOq43xENWbH0LcdCE14mwgTBEAMoAx+Fc1rdp76SmyZ4Sp48HlV7VedUnP6GA1Tw==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + detective-sass@5.0.3: + resolution: {integrity: sha512-YsYT2WuA8YIafp2RVF5CEfGhhyIVdPzlwQgxSjK+TUm3JoHP+Tcorbk3SfG0cNZ7D7+cYWa0ZBcvOaR0O8+LlA==} + engines: {node: '>=14'} + + detective-scss@4.0.3: + resolution: {integrity: sha512-VYI6cHcD0fLokwqqPFFtDQhhSnlFWvU614J42eY6G0s8c+MBhi9QAWycLwIOGxlmD8I/XvGSOUV1kIDhJ70ZPg==} + engines: {node: '>=14'} + + detective-stylus@4.0.0: + resolution: {integrity: sha512-TfPotjhszKLgFBzBhTOxNHDsutIxx9GTWjrL5Wh7Qx/ydxKhwUrlSFeLIn+ZaHPF+h0siVBkAQSuy6CADyTxgQ==} + engines: {node: '>=14'} + + detective-typescript@11.2.0: + resolution: {integrity: sha512-ARFxjzizOhPqs1fYC/2NMC3N4jrQ6HvVflnXBTRqNEqJuXwyKLRr9CrJwkRcV/SnZt1sNXgsF6FPm0x57Tq0rw==} + engines: {node: ^14.14.0 || >=16.0.0} + devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dot-prop@7.2.0: resolution: {integrity: sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-fetch@1.9.1: resolution: {integrity: sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA==} engines: {node: '>=6'} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + envinfo@7.14.0: + resolution: {integrity: sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==} + engines: {node: '>=4'} + hasBin: true + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + err-code@3.0.1: resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -2293,6 +3785,10 @@ packages: es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es5-ext@0.10.64: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} @@ -2300,6 +3796,9 @@ packages: es6-iterator@2.0.3: resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + es6-promisify@6.1.1: + resolution: {integrity: sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==} + es6-symbol@3.1.4: resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} engines: {node: '>=0.12'} @@ -2318,6 +3817,16 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.2: + resolution: {integrity: sha512-LmHPAa5h4tSxz+g/D8IHY6wCjtIiFx8I7/Q0Aq+NmvtoYvyMnJU0KQJcqB6QH30X9x/W4CemgUtPgQDZFca5SA==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2328,10 +3837,34 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + eslint-scope@8.3.0: resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2382,6 +3915,9 @@ packages: estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2389,13 +3925,24 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + event-target-shim@6.0.2: resolution: {integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==} engines: {node: '>=10.13.0'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -2407,6 +3954,18 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@6.1.0: + resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -2423,15 +3982,52 @@ packages: resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} engines: {node: '>=12.0.0'} + express-logging@1.1.1: + resolution: {integrity: sha512-1KboYwxxCG5kwkJHR5LjFDTD1Mgl8n4PIMcCuhhd/1OqaxlC68P3QKbvvAbZVUtVgtlxEdTgSUwf6yxwzRCuuA==} + engines: {node: '>= 0.10.26'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + exsolve@1.0.4: resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} + ext-list@2.2.2: + resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} + engines: {node: '>=0.10.0'} + + ext-name@5.0.0: + resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} + engines: {node: '>=4'} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@3.0.3: + resolution: {integrity: sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg==} + fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -2442,9 +4038,25 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@5.16.1: + resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@2.4.0: + resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} + fast-uri@3.0.2: resolution: {integrity: sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==} @@ -2452,9 +4064,25 @@ packages: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + + fastify@4.29.0: + resolution: {integrity: sha512-MaaUHUGcCgC8fXQDsDtioaCcag1fmPJ9j64vAKunqZF4aSub040ZGi/ag8NGE2714yREPOKZuHCfpPzuUD3UQQ==} + + fastq@1.19.0: + resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -2463,29 +4091,88 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + figures@4.0.1: + resolution: {integrity: sha512-rElJwkA/xS04Vfg+CaZodpso7VqBknOYbzi6I76hI4X80RUjkSxO2oAyPmGbuXUppywjqndOrQDl817hDnI++w==} + engines: {node: '>=12'} + + figures@5.0.0: + resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} + engines: {node: '>=14'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@18.7.0: + resolution: {integrity: sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==} + engines: {node: '>=14.16'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + filenamify@5.1.1: + resolution: {integrity: sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==} + engines: {node: '>=12.20'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + filter-obj@3.0.0: + resolution: {integrity: sha512-oQZM+QmVni8MsYzcq9lgTHD/qeLqaG8XaOPOW7dzuSafVxSUlH1+1ZDefj2OD9f2XsmG5lFl2Euc9NI4jgwFWg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + find-index-x@3.1.2: resolution: {integrity: sha512-BrDZ6eZDuspxF8qQ0n6044fnOzhY+ITNn7M+iqHekU6l1eeu6UcC5dqm+3tGip5vG4JHEEfbiT7vhcJcj3H9Bg==} engines: {node: '>=8.11.4', npm: 6.10.1} + find-my-way@8.2.2: + resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==} + engines: {node: '>=14'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2493,6 +4180,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flush-write-stream@2.0.0: + resolution: {integrity: sha512-uXClqPxT4xW0lcdSBheb2ObVU+kuqUk3Jk64EwieirEXZx9XUrVwp/JuBfKAWaM4T5Td/VL7QLDWPXp/MvGm/g==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + folder-walker@3.2.0: + resolution: {integrity: sha512-VjAQdSLsl6AkpZNyrQJfO7BXLo4chnStqb055bumZMbRUPpVuPN3a4ktsnRCmrFZjtMlYLkyXiR5rAs4WOpC4Q==} + follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} engines: {node: '>=4.0'} @@ -2509,6 +4205,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} @@ -2517,9 +4217,30 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + from2-array@0.0.4: + resolution: {integrity: sha512-0G0cAp7sYLobH7ALsr835x98PU/YeVF7wlwxdWbCUaea7wsa7lJfKZUAo6p2YZGZ8F94luCuqHZS3JtFER6uPg==} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2528,10 +4249,35 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + fuzzy@0.1.3: + resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==} + engines: {node: '>= 0.6.0'} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + get-amd-module-type@5.0.1: + resolution: {integrity: sha512-jb65zDeHyDjFR1loOVk0HQGM5WNwoGB8aLWy3LKCieMKol0/ProHkhO2X1JxojuN10vbz1qNn09MJ7tNp7qMzw==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-iterator@1.0.2: resolution: {integrity: sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==} @@ -2551,6 +4297,25 @@ packages: resolution: {integrity: sha512-cFVkod03zzfHKZwwluRzva5EmrV6PuhOVNNMILc+qwbshs/tt+zJRROL4oWjtI7k6ERzMkF/PILOL2fhG0AbnA==} engines: {node: '>=8.11.4', npm: 6.10.1} + get-package-name@2.2.0: + resolution: {integrity: sha512-LmCKVxioe63Fy6KDAQ/mmCSOSSRUE/x4zdrMD+7dU8quF3bGpzvP8mOmq4Dgce3nzU9AgkVDotucNOOg7c27BQ==} + engines: {node: '>= 12.0.0'} + + get-port-please@3.1.2: + resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + + get-port@6.1.2: + resolution: {integrity: sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-prototype-of-x@2.1.2: resolution: {integrity: sha512-LBo5X3ed0qI8k6UmEo7JgohXu3SGki60IdpnT1OlNp83GaxK66yiMjFZk67ofPH3aHENEc1+qvCmBhrlliVPPQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2558,6 +4323,14 @@ packages: get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -2565,6 +4338,17 @@ packages: get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + gh-release-fetch@4.0.3: + resolution: {integrity: sha512-TOiP1nwLsH5shG85Yt6v6Kjq5JU/44jXyEpbcfPgmj3C829yeXIlx9nAEwQRaxtRF3SJinn2lz7XUkfG9W/U4g==} + engines: {node: ^14.18.0 || ^16.13.0 || >=18.0.0} + + git-repo-info@2.1.1: + resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} + engines: {node: '>= 4.0'} + + gitconfiglocal@2.1.0: + resolution: {integrity: sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -2583,22 +4367,62 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gonzales-pe@4.3.0: + resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} + engines: {node: '>=0.6.0'} + hasBin: true + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + h3@1.15.1: + resolution: {integrity: sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==} + hamt-sharding@3.0.6: resolution: {integrity: sha512-nZeamxfymIWLpVcAN0CRrb7uVq3hCOGj9IcL6NMA6VVCVWqj+h9Jo/SmaWuS92AEDf1thmHsM5D5c70hM3j2Tg==} @@ -2609,10 +4433,18 @@ packages: resolution: {integrity: sha512-1MmOo3H8UbI6JQ3e3TaxAXHQmpnyrM5DjYRqRXNvjl/UHZqb2fEyfp0UPvtbzvmOf0+sBzcv4ea/LEw87WNyVA==} engines: {node: '>=8.11.4', npm: 6.10.1} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + has-own-property-x@4.1.2: resolution: {integrity: sha512-P6CEga37+01XlBfYBfnp4e2e5Mj0geyW5zvBhbmggkr9DkxtXWlDQIIv2PaJIOWql/rfGm7GDpKBqZ5/PT1wrA==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2632,6 +4464,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-to-string-tag-x@2.1.2: resolution: {integrity: sha512-xVv4BRcvyT9FAZSzvWD6tzVWj5JHK2d+mcv2wRSIfNBhG534hSfDGPdtx3hHJUE94BufO+pFyVpKbnaC6dsalA==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2640,6 +4476,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + has-working-bind-x@1.0.1: resolution: {integrity: sha512-iVknmNYk7CdVSDsdIyaeyPNPHlhoESfNEztH9J6kUayhj123M1so039HfC91vAOtR4NyLXWyvaFiNaXvmT/JRQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2655,10 +4494,78 @@ packages: resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} engines: {node: '>=16.9.0'} + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + hot-shots@10.2.1: + resolution: {integrity: sha512-tmjcyZkG/qADhcdC7UjAp8D7v7W2DOYFgaZ48fYMuayMQmVVUg8fntKmrjes/b40ef6yZ+qt1lB8kuEDfLC4zw==} + engines: {node: '>=10.0.0'} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-middleware@2.0.7: + resolution: {integrity: sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-shutdown@1.2.2: + resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@3.0.1: + resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} + engines: {node: '>=12.20.0'} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2679,6 +4586,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + image-meta@0.2.1: + resolution: {integrity: sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2687,22 +4597,55 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + index-of-x@3.1.2: resolution: {integrity: sha512-IgA7qpd3a0QeGD6GyznKqTz7IGQ6aubo19yi9Canit1ajRnqafF9iTnhuarp+ZEUBEpdIkbHSa9cpKPHA+N9cg==} engines: {node: '>=8.11.4', npm: 6.10.1} + index-to-position@0.1.2: + resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==} + engines: {node: '>=18'} + infinity-x@2.2.1: resolution: {integrity: sha512-aGc1EANwAzNPQYQ1uqZRiEVOK6NktlCQ+72Ey4+bDkKIiA93vPZiwjkz2sxgaRaEBed2FwXDdgPHez0K97sf3w==} engines: {node: '>=8.11.4', npm: 6.10.1} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.3: resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inquirer-autocomplete-prompt@1.4.0: + resolution: {integrity: sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==} + engines: {node: '>=10'} + peerDependencies: + inquirer: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inquirer@8.0.0: + resolution: {integrity: sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==} + engines: {node: '>=8.0.0'} + + inspect-with-kind@1.0.5: + resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==} interface-blockstore@5.3.1: resolution: {integrity: sha512-nhgrQnz6yUQEqxTFLhlOBurQOy5lWlwCpgFmZ3GTObTVTQS9RZjK/JTozY6ty9uz2lZs7VFJSqwjWAltorJ4Vw==} @@ -2710,6 +4653,10 @@ packages: interface-store@6.0.2: resolution: {integrity: sha512-KSFCXtBlNoG0hzwNa0RmhHtrdhzexp+S+UY2s0rWTBJyfdEIgn6i6Zl9otVqrcFYbYrneBT7hbmHQ8gE0C3umA==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + ipfs-unixfs-exporter@13.6.2: resolution: {integrity: sha512-U3NkQHvQn5XzxtjSo1/GfoFIoXYY4hPgOlZG5RUrV5ScBI222b3jAHbHksXZuMy7sqPkA9ieeWdOmnG1+0nxyw==} @@ -2720,6 +4667,13 @@ packages: resolution: {integrity: sha512-zIaiEGX18QATxgaS0/EOQNoo33W0islREABAcxXE8n7y2MGAlB+hdsxXn4J0hGZge8IqVQhW8sWIb+oJz2yEvg==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + ipx@2.1.0: + resolution: {integrity: sha512-AVnPGXJ8L41vjd11Z4akIF2yd14636Klxul3tBySxHA6PKfCOQPxBDkCFK5zcWh0z/keR6toh1eg8qzdBVUgdA==} + hasBin: true + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -2736,12 +4690,19 @@ packages: resolution: {integrity: sha512-XuKYtKwek6xUys0fEpRQXzlvHpuO4WoDrGRHjoHw+f+AevGkiu4CKkqN2bBHKhmlLKAHv+6ekUnK5pnsNM1C4Q==} engines: {node: '>=8.11.4', npm: 6.10.1} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} @@ -2753,10 +4714,18 @@ packages: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-data-view-x@2.1.2: resolution: {integrity: sha512-IUIxqqWkplq+w3oQLmzzPW16OBJLMtPmLknR1tyj8XIPoZKpy6dyCsJs/6+Axzt85KMAGaly8bRZnpBCGwW/TQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2789,10 +4758,18 @@ packages: resolution: {integrity: sha512-Ec5P0yFz5GR8uYQbVNx1QFUzgggsuhv85iDUJzrAe6aiJB8DTIivJKJoDBpHDcb7VXi3tEJTnBIYYRiRwgsIxQ==} engines: {node: '>=8.11.4', npm: 6.10.1} + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + is-function-x@4.1.2: resolution: {integrity: sha512-Ddyyh7mT7Wa4/WCvJ1yHOVQ8Sy+fx76PlfNYoKWJAPUj3O+DjYRQF4NdZ8zRIpkN08Oyto0yPkckUrShYthrFQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2805,6 +4782,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ci@1.0.0: + resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} + engines: {node: '>=18'} + hasBin: true + is-index-x@2.1.2: resolution: {integrity: sha512-AaCvzDt3cpJg+gVeOxferhetS4bMH9Npr7MeNNwA6GZlbY5PxWkMQj8HsezyPRg7TLKO9wrnRBSUNqeg6LpgiQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2814,10 +4796,18 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + is-integer-x@2.2.2: resolution: {integrity: sha512-06SipMhKYqmrf6h6P96d/0QMy7kEjo91dKjlgym9mx3yb8F0DMuLji16QA4iq1tn4ZaWMQg9dK7S57TA/rZayA==} engines: {node: '>=8.11.4', npm: 6.10.1} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-length-x@3.2.2: resolution: {integrity: sha512-FvtrFszHlUmX1LDYLvdd/iooYZWiVzZal0RCEDdx79vqYOChqoIxgiZETppp60znCM37dKubFLIcpQYhDpSwMw==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2842,6 +4832,10 @@ packages: resolution: {integrity: sha512-8PThmdFnNVMfzg+rsUD9lWnZ9PMOPsOzDTxTycc5xIU7nlu8jEFhIj6wk+1yTeu2UHoyj78mueL9kh/eLIGumQ==} engines: {node: '>=8.11.4', npm: 6.10.1} + is-npm@6.0.0: + resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -2854,10 +4848,26 @@ packages: resolution: {integrity: sha512-QledvfxPGy+NdNy5hmB7G4+AnK1y6KaBW55es/K+GfGkvX9Dvf/MgoQB5la002z1Htp3HQiiOfInbpxo4L0V6A==} engines: {node: '>=8.11.4', npm: 6.10.1} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-primitive-x@1.0.1: resolution: {integrity: sha512-Z29Qi/aRkELj6LO3LnnSAPN1929UIpY0h0yha5zULYo3VY1Q8hr2ocAE/fVZf22D8LFwoDoGSZwepbk7nvv+Ag==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -2877,10 +4887,18 @@ packages: resolution: {integrity: sha512-cPidjAosuclJZzYk01eMmTEzNnB/R29a6TgJsXaCzg4q8czSGEJpuslUo4Rk4ZI6aIeQESuqj9VZhUHP5AUTvA==} engines: {node: '>=8.11.4', npm: 6.10.1} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -2897,6 +4915,21 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-url-superb@4.0.0: + resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} + engines: {node: '>=10'} + + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-var-name@2.0.0: resolution: {integrity: sha512-U+7mpdfaV4Qz2hDNy1Vt7TjWwdIp6br/w7CUlTnyMdsJq+IOSZrh+RsJNlSSckPAnTllhWxpiqC7VSxVTbcr2Q==} @@ -2911,9 +4944,16 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + iserror@0.0.2: + resolution: {integrity: sha512-oKGGrFVaWwETimP3SiWwjDeY27ovZoyZPHtxblC4hCq9fXxed/jasx+ATWFFjCVSRZng8VTMsN1nDnGo6zMBSw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + iso-url@1.2.1: resolution: {integrity: sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==} engines: {node: '>=12'} @@ -2958,6 +4998,18 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-get-type@27.5.1: + resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-validate@27.5.1: + resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + jmespath@0.16.0: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} @@ -2989,6 +5041,12 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3001,9 +5059,66 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + junk@4.0.1: + resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} + engines: {node: '>=12.20'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + keep-func-props@4.0.1: + resolution: {integrity: sha512-87ftOIICfdww3SxR5P1veq3ThBNyRPG0JGL//oaR08v0k2yTicEIHd7s0GqSJfQvlb+ybC3GiDepOweo0LDhvw==} + engines: {node: '>=12.20.0'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + ky@1.7.5: + resolution: {integrity: sha512-HzhziW6sc5m0pwi5M196+7cEBtbt0lCYi67wNsiwMUmz833wloE0gbzJPWKs1gliFKQb34huItDQX97LyOdPdA==} + engines: {node: '>=18'} + + lambda-local@2.2.0: + resolution: {integrity: sha512-bPcgpIXbHnVGfI/omZIlgucDqlf4LrsunwoKue5JdZeGybt8L6KyJz2Zu19ffuZwIwLj2NAI2ZyaqNT6/cetcg==} + engines: {node: '>=8'} + hasBin: true + + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3012,6 +5127,9 @@ packages: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} os: [darwin, linux, win32] + light-my-request@5.14.0: + resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3019,6 +5137,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + listhen@1.9.0: + resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} + hasBin: true + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3027,15 +5149,69 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isempty@4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.transform@4.6.0: + resolution: {integrity: sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-process-errors@8.0.0: + resolution: {integrity: sha512-+SNGqNC1gCMJfhwYzAHr/YgNT/ZJc+V2nCkvtPnjrENMeCe+B/jgShBW0lmWoh6uVV2edFAPc/IUOkDdsjTbTg==} + engines: {node: '>=12.20.0'} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + log-symbols@7.0.0: + resolution: {integrity: sha512-zrc91EDk2M+2AXo/9BTvK91pqb7qrPg2nX/Hy+u8a5qQlbaOflCKO+6SqgZ+M+xUFxGdKTgwnGiL96b1W3ikRA==} + engines: {node: '>=18'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + long@5.3.1: resolution: {integrity: sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==} @@ -3046,30 +5222,87 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + + macos-release@3.3.0: + resolution: {integrity: sha512-tPJQ1HeyiU2vRruNGhZ+VleWuMQRro8iFtJxYgnS4NQe+EukKF6aGiIT+7flZhISAt2iaXBCfFGvAyif7/f8nQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + map-obj@5.0.2: + resolution: {integrity: sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + math-clamp-x@3.2.2: resolution: {integrity: sha512-Oi8TI3q8QsIMAR9mhH4u2u52kqUVzvehq6tOoK92ipZO/SNVoFpAgr7alHuIQdevvczP4X71GZfMlkexJAarTw==} engines: {node: '>=8.11.4', npm: 6.10.1} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + math-sign-x@4.2.2: resolution: {integrity: sha512-mQ2o1VayrZ6Jaz8VyP8B7h/hdn2iAnbUOrQh4rTiwbpULhktpD8IpHGBw8Qo2fmv97Z1P3YzLF3X/DkbIHBQmA==} engines: {node: '>=8.11.4', npm: 6.10.1} + maxstache-stream@1.0.4: + resolution: {integrity: sha512-v8qlfPN0pSp7bdSoLo1NTjG43GXGqk5W2NWFnOCq2GlmFFqebGzPCjLKSbShuqIOVorOtZSAy7O/S1OCCRONUw==} + + maxstache@1.0.7: + resolution: {integrity: sha512-53ZBxHrZM+W//5AcRVewiLpDunHnucfdzZUGz54Fnvo4tE+J3p8EL66kBrs2UhBXvYKTWckWYYWBqJqoTcenqg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + memoizee@0.4.17: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-options@3.0.4: resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} engines: {node: '>=10'} @@ -3081,6 +5314,16 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micro-api-client@3.3.0: + resolution: {integrity: sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg==} + + micro-memoize@4.1.3: + resolution: {integrity: sha512-DzRMi8smUZXT7rCGikRwldEh6eO6qzKiPPopcr1+2EY3AYKpy5fu159PKWwIS9A6IWnrvPKDMcuFtyrroZa8Bw==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3093,19 +5336,40 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + miniflare@3.20240718.0: resolution: {integrity: sha512-TKgSeyqPBeT8TBLxbDJOKPWlq/wydoJRHjAyDdgxbw59N6wbP8JucK6AU1vXCfu21eKhrEin77ssXOpbfekzPA==} engines: {node: '>=16.13'} @@ -3119,6 +5383,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3126,20 +5394,59 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mnemonist@0.38.3: resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + module-definition@5.0.1: + resolution: {integrity: sha512-kvw3B4G19IXk+BOXnYq/D/VeO9qfHaapMeuS7w7sNUqmGaA6hywdFHMi+VWeR9wUScXM7XjoryTffCZ5B0/8IA==} + engines: {node: '>=14'} + hasBin: true + + moize@6.1.6: + resolution: {integrity: sha512-vSKdIUO61iCmTqhdoIDrqyrtp87nWZUmBPniNjO0fX49wEYmyDO4lvlnFXiGcaH1JLE/s/9HbiK4LSHsbiUY6Q==} + + move-file@3.1.0: + resolution: {integrity: sha512-4aE3U7CCBWgrQlQDMq8da4woBWDGHioJFiOZ8Ie6Yq2uwYQ9V2kGhTz4x3u6Wc+OU17nw0yc3rJ/lQ4jIiPe3A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3150,6 +5457,10 @@ packages: multiformats@13.3.2: resolution: {integrity: sha512-qbB0CQDt3QKfiAzZ5ZYjLFOs+zW43vA4uyM8g27PeEuXZybUOFyjrVdP93HPBHMoglibwfkdVwbzfUq8qGcH6g==} + multiparty@4.2.3: + resolution: {integrity: sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==} + engines: {node: '>= 0.10'} + murmurhash3js-revisited@3.0.0: resolution: {integrity: sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==} engines: {node: '>=8.0.0'} @@ -3158,6 +5469,12 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true + mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -3165,6 +5482,9 @@ packages: resolution: {integrity: sha512-kZN/tgpupBg+ZVB+25lTDNKwnDGCFia36awekeIRj4AF3zQ/bnRDgr3M3mEV2KmFYC+UW8rVCCPjAz0rGdZk8A==} engines: {node: '>=8.11.4', npm: 6.10.1} + nan@2.22.2: + resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} + nanoid@3.3.9: resolution: {integrity: sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3186,6 +5506,25 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + nested-error-stacks@2.1.1: + resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} + + netlify-cli@19.0.0: + resolution: {integrity: sha512-kHkm7V6y7rkO/WI42PDcmVvDTQupEPMOD2vSOirnWPcyBTw9WO39xcHKdZJMdhoEppkgUz/EjNZHIYMhX/yq6w==} + engines: {node: '>=18.14.0'} + hasBin: true + + netlify-redirector@0.5.0: + resolution: {integrity: sha512-4zdzIP+6muqPCuE8avnrgDJ6KW/2+UpHTRcTbMXCIRxiRmyrX+IZ4WSJGZdHPWF3WmQpXpy603XxecZ9iygN7w==} + + netlify@13.3.3: + resolution: {integrity: sha512-6FKRoqzos9WZi9J2oac8+tEY5wbzhtt/JnTnCW7ymL9coJ47R9aRWFu1LUSJtYUbKq38Bi0o00T7X6XYyrEdrg==} + engines: {node: ^14.16.0 || >=16.0.0} + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -3193,10 +5532,19 @@ packages: resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} engines: {node: '>=10'} + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3210,6 +5558,21 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-mock-http@1.0.0: + resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + + node-source-walk@6.0.2: + resolution: {integrity: sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==} + engines: {node: '>=14'} + node-sql-parser@3.9.4: resolution: {integrity: sha512-U8xa/QBpNz/dc4BERBkMg//XTrBDcj0uIg5YDYPV4ChYgHPEw4JhoT5YWTxQuKBg/3C1kfkTO4MuEYw7fCYHJw==} engines: {node: '>=8'} @@ -3217,18 +5580,58 @@ packages: node-sqlite3-wasm@0.8.36: resolution: {integrity: sha512-pSqKe2JL2I7SxWNEF3HzahdzwxNwYsyj79tsYoZ251+Gd90HcAjyy472UTBSnW8iEztJF9w1JoqZXBlG9fhWEA==} + node-stream-zip@1.15.0: + resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} + engines: {node: '>=0.12.0'} + noop-x@1.2.1: resolution: {integrity: sha512-X+jn3u2YhlAjteZIfrcVxaDLRfnrMvkzQ+jRatOX8UO7PFqU0QKlcyI2DhvLKEK2K4u1cRrZw9QqK3XruvyH2A==} engines: {node: '>=8.11.4', npm: 6.10.1} + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + normalize-space-x@4.1.2: resolution: {integrity: sha512-nADMBxZ8tCAS/VR6s+EU7p2P/+Hs0GSmUvkXyThZmdACqDIeyAmRHi4aRsbR32Ec0H+AC71srh2l3I5CF0NKvw==} engines: {node: '>=8.11.4', npm: 6.10.1} + normalize-url@8.0.1: + resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + engines: {node: '>=14.16'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3249,6 +5652,10 @@ packages: resolution: {integrity: sha512-ukh2PlxrPWenQ7pcCfeUbFg4Bib8Km1CvFVKrgWbPVgkBM7hMC3EhJ3RFK1zl8c3dnp+8l25IGM9RF3wpkcIQA==} engines: {node: '>=8.11.4', npm: 6.10.1} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-is@1.1.6: resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} engines: {node: '>= 0.4'} @@ -3268,23 +5675,76 @@ packages: obliterator@1.6.1: resolution: {integrity: sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==} + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + omit.js@2.0.2: + resolution: {integrity: sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + one-webcrypto@1.0.3: resolution: {integrity: sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==} + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@10.1.0: + resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + os-name@5.1.0: + resolution: {integrity: sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + p-defer@3.0.0: resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} engines: {node: '>=8'} @@ -3293,13 +5753,45 @@ packages: resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} engines: {node: '>=12'} + p-event@4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + + p-event@5.0.1: + resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-event@6.0.1: + resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} + engines: {node: '>=16.17'} + + p-every@2.0.0: + resolution: {integrity: sha512-MCz9DqD5opPC48Zsd+BHm56O/HfhYIQQtupfDzhXoVgQdg/Ux4F8/JcdRuQ+arq7zD5fB6zP3axbH3d9Nr8dlw==} + engines: {node: '>=8'} + p-fifo@1.0.0: resolution: {integrity: sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==} + p-filter@3.0.0: + resolution: {integrity: sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-filter@4.1.0: + resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} + engines: {node: '>=18'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@6.2.0: resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} engines: {node: '>=18'} @@ -3308,6 +5800,18 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-map@5.5.0: + resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} + engines: {node: '>=12'} + p-map@7.0.3: resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} engines: {node: '>=18'} @@ -3316,6 +5820,14 @@ packages: resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} engines: {node: '>=18'} + p-reduce@3.0.0: + resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} + engines: {node: '>=12'} + + p-retry@5.1.2: + resolution: {integrity: sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-retry@6.2.0: resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} engines: {node: '>=16.17'} @@ -3324,21 +5836,69 @@ packages: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@5.1.0: + resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} + engines: {node: '>=12'} + + p-timeout@6.1.2: + resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} + engines: {node: '>=14.16'} + p-timeout@6.1.4: resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} engines: {node: '>=14.16'} + p-wait-for@5.0.2: + resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==} + engines: {node: '>=12'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + + parallel-transform@1.2.0: + resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-github-url@1.0.3: + resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==} + engines: {node: '>= 0.10'} + hasBin: true + + parse-gitignore@2.0.0: + resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} + engines: {node: '>=14'} + parse-int-x@3.2.2: resolution: {integrity: sha512-2hRe7DQZb8+YAXHu1esNwfEp6+yypsl1wayA7Vm7s/atrkUPltuG/y9KKfLcg3Ns/eG9EFJCugVS2eDZ8mJdYA==} engines: {node: '>=8.11.4', npm: 6.10.1} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.1.0: + resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==} + engines: {node: '>=18'} + + parse-ms@3.0.0: + resolution: {integrity: sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==} + engines: {node: '>=12'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + partykit@0.0.111: resolution: {integrity: sha512-pbUzJmulPTqZWgtH++eClnLlddTWS0RPKOAQyexYiSqGEg2/Ff84Ep0qWFqy+53wiz098DPFekRI375Qy2GsZw==} hasBin: true @@ -3355,6 +5915,14 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3363,16 +5931,33 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + path@0.12.7: resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3380,6 +5965,13 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + peek-readable@5.4.2: + resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} + engines: {node: '>=14.16'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3391,10 +5983,27 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.6.0: + resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -3417,6 +6026,16 @@ packages: yaml: optional: true + postcss-values-parser@6.0.2: + resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.2.9 + + postcss@8.5.1: + resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.3: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} @@ -3426,6 +6045,15 @@ packages: engines: {node: '>=10'} hasBin: true + precinct@11.0.5: + resolution: {integrity: sha512-oHSWLC8cL/0znFhvln26D14KfCQFFn4KOLSw6hmLhd+LQ2SKt9Ljm89but76Pc7flM9Ty1TnXyrA2u16MfRV3w==} + engines: {node: ^14.14.0 || >=16.0.0} + hasBin: true + + precond@0.2.3: + resolution: {integrity: sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==} + engines: {node: '>= 0.6'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3435,9 +6063,30 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-ms@8.0.0: + resolution: {integrity: sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==} + engines: {node: '>=14.16'} + + prettyjson@1.2.5: + resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} + hasBin: true + printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -3455,6 +6104,9 @@ packages: resolution: {integrity: sha512-lRqJh4yGnwiZUx7x8abWhfucNsImqwxbFoCQzRjfwJflOBenZoFq/YMlWJShdzBmqd9i2/RrrvFW8cBs1+H+3w==} engines: {node: '>=8.11.4', npm: 6.10.1} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.4.0: resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} engines: {node: '>=12.0.0'} @@ -3462,9 +6114,20 @@ packages: protons-runtime@5.5.0: resolution: {integrity: sha512-EsALjF9QsrEk6gbCx3lmfHxVN0ah7nG3cY7GySD4xf4g8cr7g543zB88Foh897Sr1RQJ9yDCUsoT1i1H/cVUFA==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + ps-list@8.1.1: + resolution: {integrity: sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + pump@1.0.3: + resolution: {integrity: sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -3475,6 +6138,18 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + querystring@0.2.0: resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} engines: {node: '>=0.4.x'} @@ -3483,13 +6158,45 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + quote-unquote@1.0.0: + resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} + rabin-rs@2.1.0: resolution: {integrity: sha512-5y72gAXPzIBsAMHcpxZP8eMDuDT98qMP1BqSDHRbHkJJXEgWIN1lA47LxUqzsK6jknOJtgfkQr9v+7qMlFDm6g==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-native-fetch-api@3.0.0: resolution: {integrity: sha512-g2rtqPjdroaboDKTsJCTlcmtw54E25OjyaunUP0anOZn4Fuo2IKs8BVfe02zVggA/UysbmfSnRJIqtNkAgggNA==} @@ -3497,18 +6204,71 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + read-package-up@11.0.0: + resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} + engines: {node: '>=18'} + + read-pkg@7.1.0: + resolution: {integrity: sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==} + engines: {node: '>=12.20'} + + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + rename-function-x@1.1.2: resolution: {integrity: sha512-3kkzZck51lWM7V71Px8ynhLX1JbS5EQPPeOfPywp41f50VLKdh+1lZJz3VgzTDRYerQIw1uxHUCm5XXFweCWIg==} engines: {node: '>=8.11.4', npm: 6.10.1} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + replace-comments-x@3.1.2: resolution: {integrity: sha512-Ig8PT8++5YLhTDD8EokcTyawaULbOzzeFk8Tb6VVAVqiqZ5ZFcU1/fBw9IEwjd8r3wMm6HSZmQOCYYYny8dGnw==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -3517,6 +6277,10 @@ packages: resolution: {integrity: sha512-/6GKFuhc8z1zBXkTLQDH1OrbkeMWKwvJbjkLFMiKi3LOBpn+5pAdgV5B8Y4vXWrlwhvpIeg3dFRFDZdcyBDQyA==} engines: {node: '>=8.11.4', npm: 6.10.1} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3525,6 +6289,15 @@ packages: resolution: {integrity: sha512-te3nTMzvKCUMIRBQYAtLAPMfXvYnYCijZplqA3/VdSGFFNzgkiuaUGUQdZDSHhK20Z7xewHRWW4XkVpUVrQwMA==} engines: {node: '>=8.11.4', npm: 6.10.1} + require-package-name@2.0.1: + resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3536,6 +6309,30 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + ret@0.4.3: + resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + engines: {node: '>=10'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -3544,6 +6341,14 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rollup-plugin-inject@3.0.2: resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. @@ -3559,9 +6364,21 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -3569,9 +6386,22 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-json-stringify@1.2.0: + resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} + + safe-regex2@3.1.0: + resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3586,15 +6416,47 @@ packages: sax@1.2.1: resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + seek-bzip@1.0.6: + resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.1: resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3607,9 +6469,28 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3635,14 +6516,40 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + smol-toml@1.3.1: resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==} engines: {node: '>= 18'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + sort-keys-length@1.0.1: + resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} + engines: {node: '>=0.10.0'} + + sort-keys@1.1.2: + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -3658,22 +6565,65 @@ packages: sparse-array@1.3.2: resolution: {integrity: sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==} + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + split-if-boxed-bug-x@2.1.2: resolution: {integrity: sha512-s16LLvA6VVqayT560dGsP1AXGWAIBsd/1KotRSjoPiaAMPKLbyio9iAsR+b6RGBF22zdlBLGjc4KyU5KCfmzIA==} engines: {node: '>=8.11.4', npm: 6.10.1} + split2@1.1.1: + resolution: {integrity: sha512-cfurE2q8LamExY+lJ9Ex3ZfBwqAPduzOKVscPDXNCLLMvyaeD3DTz1yk7fVIs6Chco+12XeD0BB6HEoYzPYbXA==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + std-env@3.8.1: resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -3681,6 +6631,13 @@ packages: stream-to-it@0.2.4: resolution: {integrity: sha512-4vEbkSs83OahpmBybNJXlJd7d6/RxzkkSdT3I0mnGt79Xd2Kk+e1JqbvAvsQfCeKj3aKb0QIWkyK3/n0j506vQ==} + streamx@2.22.0: + resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3689,9 +6646,27 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi-control-characters@2.0.0: + resolution: {integrity: sha512-Q0/k5orrVGeaOlIOUn1gybGU0IcAbgHQT1faLo5hik4DqClKVSaka5xOhNNoRgtfztHVxCYxi7j71mrWom0bIw==} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3700,6 +6675,13 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-dirs@3.0.0: + resolution: {integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -3712,9 +6694,17 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-outer@2.0.0: + resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strtok3@7.1.1: + resolution: {integrity: sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==} + engines: {node: '>=16'} + stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} @@ -3723,10 +6713,31 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + symbol-iterator-x@1.1.2: resolution: {integrity: sha512-Zx/FE0En8jKE0iEGO/x1iqBW08dkY29hFrU3VC/gZktCHqQd3rKoXzhP4ArGAS99/jkOC7A0FMEPCGX4EWOG/Q==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -3742,13 +6753,44 @@ packages: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} + tabtab@3.0.2: + resolution: {integrity: sha512-jANKmUe0sIQc/zTALTBy186PoM/k6aPrh3A7p6AaAfF6WPSbTx1JYeGIGH162btpH+mmVEXln+UxwViZHO2Jhg==} + tar-fs@2.1.2: resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==} + tar-fs@3.0.8: + resolution: {integrity: sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} + + terminal-link@3.0.0: + resolution: {integrity: sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==} + engines: {node: '>=12'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3756,6 +6798,26 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + through2-filter@4.0.0: + resolution: {integrity: sha512-P8IpQL19bSdXqGLvLdbidYRxERXgHEXGcQofPxbLpPkqS1ieOrUrocdYRTNv8YwSukaDJWr71s6F2kZ3bvgEhA==} + engines: {node: '>= 6'} + + through2-map@4.0.0: + resolution: {integrity: sha512-+rpmDB5yckiBGEuqJSsWYWMs9e1zdksypDKvByysEyN+knhsPXV9Z6O2mA9meczIa6AON7bi2G3xWk5T8UG4zQ==} + engines: {node: '>= 6'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + timers-ext@0.1.8: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} @@ -3782,6 +6844,17 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + to-boolean-x@2.1.1: resolution: {integrity: sha512-ir6nGXyTFDPIUiMuePbGIx5UnY6JbxMw7e18djKrDHhXvt14pbhsbatFzySIVW++4vevTtQNYinh8bKOCW//KQ==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -3826,6 +6899,24 @@ packages: resolution: {integrity: sha512-4R9y5W1m3YxHM4MGtgudgtVsx7mTTtyI7qC5wiuoJt3fwJEiNBBInmwggGawJELSY7aDajIaVyWziq46bimupw==} engines: {node: '>=8.11.4', npm: 6.10.1} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@5.0.1: + resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} + engines: {node: '>=14.16'} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + tomlify-j0.4@3.0.0: + resolution: {integrity: sha512-2Ulkc8T7mXJ2l0W476YC/A209PR38Nw8PuaCNtk9uI3t1zzFdGQeWYGQvmj2PZkVvRC/Yoi4xQKMRnWc/N29tQ==} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -3840,6 +6931,10 @@ packages: resolution: {integrity: sha512-yJeikzHJkxBygGAhXIVxxFuRqO0jptEx91+U7bS2il6uCZ/CAOWySArfOt35ZwzTyXfnfiB+BhvxvE2TroVcxA==} engines: {node: '>=8.11.4', npm: 6.10.1} + trim-repeated@2.0.0: + resolution: {integrity: sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==} + engines: {node: '>=12'} + trim-right-x@4.1.2: resolution: {integrity: sha512-//Z+2PD4eS9lh2lZjalpBEII7Ef2DaDQ5s1OOx1JK1L90WWfrqdC9/vX8f467lmahNGkxoxmHWwNDN6XdK0oKg==} engines: {node: '>=8.11.4', npm: 6.10.1} @@ -3848,6 +6943,10 @@ packages: resolution: {integrity: sha512-Cr4fnQ52a57JCX20SzyvRyLcMvKgKd9lcf7Jh/oa1w4FfH1kGT6F7/HjNiAJZyDW+xdLLH6KhrfRhvoIYZKROA==} engines: {node: '>=8.11.4', npm: 6.10.1} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@2.0.1: resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} engines: {node: '>=18.12'} @@ -3865,6 +6964,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} @@ -3875,6 +6988,9 @@ packages: typescript: optional: true + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} @@ -3900,6 +7016,12 @@ packages: typescript: optional: true + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tsx@4.19.3: resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} engines: {node: '>=18.0.0'} @@ -3912,6 +7034,14 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -3924,6 +7054,10 @@ packages: resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==} engines: {node: '>=16'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -3942,6 +7076,10 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + uint8-varint@2.0.4: resolution: {integrity: sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==} @@ -3954,6 +7092,16 @@ packages: uint8arrays@5.1.0: resolution: {integrity: sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==} + ulid@2.3.0: + resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + hasBin: true + + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -3968,12 +7116,112 @@ packages: unenv@2.0.0-rc.8: resolution: {integrity: sha512-wj/lN45LvZ4Uz95rti6DC5wg56eocAwA8KYzExk2SN01iuAb9ayzMwN13Kd3EG2eBXu27PzgMIXLTwWfSczKfw==} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + + unix-dgram@2.0.6: + resolution: {integrity: sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==} + engines: {node: '>=0.10.48'} + + unixify@1.0.0: + resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} + engines: {node: '>=0.10.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unstorage@1.15.0: + resolution: {integrity: sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6.0.3 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + untildify@3.0.3: + resolution: {integrity: sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==} + engines: {node: '>=4'} + + untun@0.1.3: + resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} + hasBin: true + + update-notifier@7.3.1: + resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} + engines: {node: '>=18'} + + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} url@0.10.3: resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + urlpattern-polyfill@8.0.2: + resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3991,6 +7239,14 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + uuid@8.0.0: resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} hasBin: true @@ -3999,6 +7255,9 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + valibot@1.0.0-beta.7: resolution: {integrity: sha512-8CsDu3tqyg7quEHMzCOYdQ/d9NlmVQKtd4AlFje6oJpvqo70EIZjSakKIeWltJyNAiUtdtLe0LAk4625gavoeQ==} peerDependencies: @@ -4007,9 +7266,20 @@ packages: typescript: optional: true + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@4.0.0: + resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.0.8: resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4099,6 +7369,11 @@ packages: engines: {node: '>=12.0.0'} hasBin: true + wait-port@1.1.0: + resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} + engines: {node: '>=10'} + hasBin: true + watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -4143,6 +7418,25 @@ packages: engines: {node: '>=8'} hasBin: true + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + windows-release@5.1.1: + resolution: {integrity: sha512-NMD00arvqcq2nwqc5Q6KtrSRHK+fVD31erE5FEMahAw5PmVCgD7MUXodq3pdZSUkqA9Cda2iWx6s1XYwiJWRmw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4176,9 +7470,17 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -4203,6 +7505,10 @@ packages: utf-8-validate: optional: true + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -4211,11 +7517,42 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xss@1.0.15: + resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} + engines: {node: '>= 0.10.0'} + hasBin: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.7.0: resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} engines: {node: '>= 14'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4224,6 +7561,10 @@ packages: resolution: {integrity: sha512-KHBC7z61OJeaMGnF3wqNZj+GGNXOyypZviiKpQeiHirG5Ib1ImwcLBH70rbMSkKfSmUNBsdf2PwaEJtKvgmkNw==} engines: {node: '>=12.20'} + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + yoga-wasm-web@0.3.3: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} @@ -4233,6 +7574,10 @@ packages: youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} @@ -4855,6 +8200,86 @@ snapshots: '@smithy/types': 4.1.0 tslib: 2.8.1 + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.9': + dependencies: + '@babel/types': 7.26.9 + + '@babel/types@7.26.5': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.26.9': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bugsnag/browser@7.25.0': + dependencies: + '@bugsnag/core': 7.25.0 + + '@bugsnag/browser@8.2.0': + dependencies: + '@bugsnag/core': 8.2.0 + + '@bugsnag/core@7.25.0': + dependencies: + '@bugsnag/cuid': 3.2.1 + '@bugsnag/safe-json-stringify': 6.0.0 + error-stack-parser: 2.1.4 + iserror: 0.0.2 + stack-generator: 2.0.10 + + '@bugsnag/core@8.2.0': + dependencies: + '@bugsnag/cuid': 3.2.1 + '@bugsnag/safe-json-stringify': 6.0.0 + error-stack-parser: 2.1.4 + iserror: 0.0.2 + stack-generator: 2.0.10 + + '@bugsnag/cuid@3.2.1': {} + + '@bugsnag/js@7.25.0': + dependencies: + '@bugsnag/browser': 7.25.0 + '@bugsnag/node': 7.25.0 + + '@bugsnag/js@8.2.0': + dependencies: + '@bugsnag/browser': 8.2.0 + '@bugsnag/node': 8.2.0 + + '@bugsnag/node@7.25.0': + dependencies: + '@bugsnag/core': 7.25.0 + byline: 5.0.0 + error-stack-parser: 2.1.4 + iserror: 0.0.2 + pump: 3.0.2 + stack-generator: 2.0.10 + + '@bugsnag/node@8.2.0': + dependencies: + '@bugsnag/core': 8.2.0 + byline: 5.0.0 + error-stack-parser: 2.1.4 + iserror: 0.0.2 + pump: 3.0.2 + stack-generator: 2.0.10 + + '@bugsnag/safe-json-stringify@6.0.0': {} + '@cloudflare/kv-asset-handler@0.3.4': dependencies: mime: 3.0.0 @@ -4865,7 +8290,7 @@ snapshots: optionalDependencies: workerd: 1.20250224.0 - '@cloudflare/vitest-pool-workers@0.7.7(@cloudflare/workers-types@4.20250303.0)(@vitest/runner@3.0.8)(@vitest/snapshot@3.0.8)(vitest@3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0))': + '@cloudflare/vitest-pool-workers@0.7.7(@cloudflare/workers-types@4.20250303.0)(@vitest/runner@3.0.8)(@vitest/snapshot@3.0.8)(vitest@3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/runner': 3.0.8 '@vitest/snapshot': 3.0.8 @@ -4875,7 +8300,7 @@ snapshots: esbuild: 0.17.19 miniflare: 3.20250224.0 semver: 7.7.1 - vitest: 3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + vitest: 3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) wrangler: 3.114.0(@cloudflare/workers-types@4.20250303.0) zod: 3.24.2 transitivePeerDependencies: @@ -4917,10 +8342,23 @@ snapshots: '@cloudflare/workers-types@4.20250303.0': {} + '@colors/colors@1.6.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@dependents/detective-less@4.1.0': + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 6.0.2 + '@emnapi/runtime@1.3.1': dependencies: tslib: 2.8.1 @@ -4936,6 +8374,12 @@ snapshots: escape-string-regexp: 4.0.0 rollup-plugin-node-polyfills: 0.2.1 + '@esbuild/aix-ppc64@0.19.11': + optional: true + + '@esbuild/aix-ppc64@0.21.2': + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -4945,6 +8389,12 @@ snapshots: '@esbuild/android-arm64@0.17.19': optional: true + '@esbuild/android-arm64@0.19.11': + optional: true + + '@esbuild/android-arm64@0.21.2': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true @@ -4954,6 +8404,12 @@ snapshots: '@esbuild/android-arm@0.17.19': optional: true + '@esbuild/android-arm@0.19.11': + optional: true + + '@esbuild/android-arm@0.21.2': + optional: true + '@esbuild/android-arm@0.21.5': optional: true @@ -4963,6 +8419,12 @@ snapshots: '@esbuild/android-x64@0.17.19': optional: true + '@esbuild/android-x64@0.19.11': + optional: true + + '@esbuild/android-x64@0.21.2': + optional: true + '@esbuild/android-x64@0.21.5': optional: true @@ -4972,6 +8434,12 @@ snapshots: '@esbuild/darwin-arm64@0.17.19': optional: true + '@esbuild/darwin-arm64@0.19.11': + optional: true + + '@esbuild/darwin-arm64@0.21.2': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true @@ -4981,6 +8449,12 @@ snapshots: '@esbuild/darwin-x64@0.17.19': optional: true + '@esbuild/darwin-x64@0.19.11': + optional: true + + '@esbuild/darwin-x64@0.21.2': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true @@ -4990,6 +8464,12 @@ snapshots: '@esbuild/freebsd-arm64@0.17.19': optional: true + '@esbuild/freebsd-arm64@0.19.11': + optional: true + + '@esbuild/freebsd-arm64@0.21.2': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true @@ -4999,6 +8479,12 @@ snapshots: '@esbuild/freebsd-x64@0.17.19': optional: true + '@esbuild/freebsd-x64@0.19.11': + optional: true + + '@esbuild/freebsd-x64@0.21.2': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true @@ -5008,6 +8494,12 @@ snapshots: '@esbuild/linux-arm64@0.17.19': optional: true + '@esbuild/linux-arm64@0.19.11': + optional: true + + '@esbuild/linux-arm64@0.21.2': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true @@ -5017,6 +8509,12 @@ snapshots: '@esbuild/linux-arm@0.17.19': optional: true + '@esbuild/linux-arm@0.19.11': + optional: true + + '@esbuild/linux-arm@0.21.2': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true @@ -5026,6 +8524,12 @@ snapshots: '@esbuild/linux-ia32@0.17.19': optional: true + '@esbuild/linux-ia32@0.19.11': + optional: true + + '@esbuild/linux-ia32@0.21.2': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true @@ -5035,6 +8539,12 @@ snapshots: '@esbuild/linux-loong64@0.17.19': optional: true + '@esbuild/linux-loong64@0.19.11': + optional: true + + '@esbuild/linux-loong64@0.21.2': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true @@ -5044,6 +8554,12 @@ snapshots: '@esbuild/linux-mips64el@0.17.19': optional: true + '@esbuild/linux-mips64el@0.19.11': + optional: true + + '@esbuild/linux-mips64el@0.21.2': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true @@ -5053,6 +8569,12 @@ snapshots: '@esbuild/linux-ppc64@0.17.19': optional: true + '@esbuild/linux-ppc64@0.19.11': + optional: true + + '@esbuild/linux-ppc64@0.21.2': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true @@ -5062,6 +8584,12 @@ snapshots: '@esbuild/linux-riscv64@0.17.19': optional: true + '@esbuild/linux-riscv64@0.19.11': + optional: true + + '@esbuild/linux-riscv64@0.21.2': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true @@ -5071,6 +8599,12 @@ snapshots: '@esbuild/linux-s390x@0.17.19': optional: true + '@esbuild/linux-s390x@0.19.11': + optional: true + + '@esbuild/linux-s390x@0.21.2': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true @@ -5080,6 +8614,12 @@ snapshots: '@esbuild/linux-x64@0.17.19': optional: true + '@esbuild/linux-x64@0.19.11': + optional: true + + '@esbuild/linux-x64@0.21.2': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true @@ -5092,6 +8632,12 @@ snapshots: '@esbuild/netbsd-x64@0.17.19': optional: true + '@esbuild/netbsd-x64@0.19.11': + optional: true + + '@esbuild/netbsd-x64@0.21.2': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true @@ -5104,6 +8650,12 @@ snapshots: '@esbuild/openbsd-x64@0.17.19': optional: true + '@esbuild/openbsd-x64@0.19.11': + optional: true + + '@esbuild/openbsd-x64@0.21.2': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true @@ -5113,6 +8665,12 @@ snapshots: '@esbuild/sunos-x64@0.17.19': optional: true + '@esbuild/sunos-x64@0.19.11': + optional: true + + '@esbuild/sunos-x64@0.21.2': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true @@ -5122,6 +8680,12 @@ snapshots: '@esbuild/win32-arm64@0.17.19': optional: true + '@esbuild/win32-arm64@0.19.11': + optional: true + + '@esbuild/win32-arm64@0.21.2': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true @@ -5131,6 +8695,12 @@ snapshots: '@esbuild/win32-ia32@0.17.19': optional: true + '@esbuild/win32-ia32@0.19.11': + optional: true + + '@esbuild/win32-ia32@0.21.2': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true @@ -5140,15 +8710,21 @@ snapshots: '@esbuild/win32-x64@0.17.19': optional: true + '@esbuild/win32-x64@0.19.11': + optional: true + + '@esbuild/win32-x64@0.21.2': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.25.1': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0(jiti@2.4.2))': dependencies: - eslint: 9.22.0 + eslint: 9.22.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -5156,7 +8732,7 @@ snapshots: '@eslint/config-array@0.19.2': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -5170,7 +8746,7 @@ snapshots: '@eslint/eslintrc@3.3.0': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -5190,8 +8766,43 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@fastify/accept-negotiator@1.1.0': {} + + '@fastify/ajv-compiler@3.6.0': + dependencies: + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + fast-uri: 2.4.0 + '@fastify/busboy@2.1.1': {} + '@fastify/error@3.4.1': {} + + '@fastify/fast-json-stringify-compiler@4.3.0': + dependencies: + fast-json-stringify: 5.16.1 + + '@fastify/merge-json-schemas@0.1.1': + dependencies: + fast-deep-equal: 3.1.3 + + '@fastify/send@2.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + + '@fastify/static@7.0.4': + dependencies: + '@fastify/accept-negotiator': 1.1.0 + '@fastify/send': 2.1.0 + content-disposition: 0.5.4 + fastify-plugin: 4.5.1 + fastq: 1.19.0 + glob: 10.4.5 + '@fireproof/core@0.20.0-dev-preview-53(@adviser/cement@0.4.0(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1)': dependencies: '@adviser/cement': 0.4.0(typescript@5.7.3) @@ -5244,10 +8855,14 @@ snapshots: '@humanwhocodes/module-importer@1.0.1': {} + '@humanwhocodes/momoa@2.0.4': {} + '@humanwhocodes/retry@0.3.1': {} '@humanwhocodes/retry@0.4.2': {} + '@iarna/toml@2.2.5': {} + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 @@ -5323,6 +8938,8 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@import-maps/resolve@1.0.1': {} + '@ipld/car@5.4.0': dependencies: '@ipld/dag-cbor': 9.2.2 @@ -5369,6 +8986,14 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jest/types@27.5.1': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.13.10 + '@types/yargs': 16.0.9 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -5449,6 +9074,23 @@ snapshots: '@libsql/win32-x64-msvc@0.4.7': optional: true + '@lukeed/ms@2.0.2': {} + + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)(supports-color@9.4.0)': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1(supports-color@9.4.0) + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.1 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@multiformats/murmur3@2.1.8': dependencies: multiformats: 13.3.2 @@ -5456,8 +9098,399 @@ snapshots: '@neon-rs/load@0.0.4': {} + '@netlify/binary-info@1.0.0': {} + + '@netlify/blobs@7.4.0': {} + '@netlify/blobs@8.1.1': {} + '@netlify/build-info@8.0.0': + dependencies: + '@bugsnag/js': 7.25.0 + '@iarna/toml': 2.2.5 + dot-prop: 7.2.0 + find-up: 6.3.0 + minimatch: 9.0.5 + read-pkg: 7.1.0 + semver: 7.7.1 + yaml: 2.7.0 + yargs: 17.7.2 + + '@netlify/build@29.58.10(@opentelemetry/api@1.8.0)(@types/node@22.13.10)(encoding@0.1.13)(picomatch@4.0.2)(rollup@4.35.0)': + dependencies: + '@bugsnag/js': 7.25.0 + '@netlify/blobs': 7.4.0 + '@netlify/cache-utils': 5.2.0 + '@netlify/config': 20.21.7 + '@netlify/edge-bundler': 12.3.2(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + '@netlify/framework-info': 9.9.1 + '@netlify/functions-utils': 5.3.8(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + '@netlify/git-utils': 5.2.0 + '@netlify/opentelemetry-utils': 1.3.0(@opentelemetry/api@1.8.0) + '@netlify/plugins-list': 6.80.0 + '@netlify/run-utils': 5.2.0 + '@netlify/zip-it-and-ship-it': 9.42.6(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + '@opentelemetry/api': 1.8.0 + '@sindresorhus/slugify': 2.2.1 + ansi-escapes: 6.2.1 + chalk: 5.4.1 + clean-stack: 5.2.0 + execa: 7.2.0 + fdir: 6.4.3(picomatch@4.0.2) + figures: 5.0.0 + filter-obj: 5.1.0 + got: 12.6.1 + hot-shots: 10.2.1 + indent-string: 5.0.0 + is-plain-obj: 4.1.0 + js-yaml: 4.1.0 + keep-func-props: 4.0.1 + locate-path: 7.2.0 + log-process-errors: 8.0.0 + map-obj: 5.0.2 + memoize-one: 6.0.0 + minimatch: 9.0.5 + node-fetch: 3.3.2 + os-name: 5.1.0 + p-event: 6.0.1 + p-every: 2.0.0 + p-filter: 4.1.0 + p-locate: 6.0.0 + p-map: 7.0.3 + p-reduce: 3.0.0 + path-exists: 5.0.0 + path-type: 5.0.0 + pkg-dir: 7.0.0 + pretty-ms: 8.0.0 + ps-list: 8.1.1 + read-package-up: 11.0.0 + readdirp: 3.6.0 + resolve: 2.0.0-next.5 + rfdc: 1.4.1 + safe-json-stringify: 1.2.0 + semver: 7.7.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + supports-color: 9.4.0 + terminal-link: 3.0.0 + ts-node: 10.9.2(@types/node@22.13.10)(typescript@5.7.3) + typescript: 5.7.3 + uuid: 9.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - encoding + - picomatch + - rollup + + '@netlify/cache-utils@5.2.0': + dependencies: + cpy: 9.0.1 + get-stream: 6.0.1 + globby: 13.2.2 + junk: 4.0.1 + locate-path: 7.2.0 + move-file: 3.1.0 + path-exists: 5.0.0 + readdirp: 3.6.0 + + '@netlify/config@20.21.7': + dependencies: + '@iarna/toml': 2.2.5 + '@netlify/headers-parser': 7.3.0 + '@netlify/redirect-parser': 14.5.0 + chalk: 5.4.1 + cron-parser: 4.9.0 + deepmerge: 4.3.1 + dot-prop: 7.2.0 + execa: 7.2.0 + fast-safe-stringify: 2.1.1 + figures: 5.0.0 + filter-obj: 5.1.0 + find-up: 6.3.0 + indent-string: 5.0.0 + is-plain-obj: 4.1.0 + js-yaml: 4.1.0 + map-obj: 5.0.2 + netlify: 13.3.3 + node-fetch: 3.3.2 + omit.js: 2.0.2 + p-locate: 6.0.0 + path-type: 5.0.0 + tomlify-j0.4: 3.0.0 + validate-npm-package-name: 4.0.0 + yargs: 17.7.2 + + '@netlify/edge-bundler@12.3.2(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0)': + dependencies: + '@import-maps/resolve': 1.0.1 + '@vercel/nft': 0.27.7(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + ajv: 8.17.1 + ajv-errors: 3.0.0(ajv@8.17.1) + better-ajv-errors: 1.2.0(ajv@8.17.1) + common-path-prefix: 3.0.0 + env-paths: 3.0.0 + esbuild: 0.21.2 + execa: 7.2.0 + find-up: 6.3.0 + get-package-name: 2.2.0 + get-port: 6.1.2 + is-path-inside: 4.0.0 + jsonc-parser: 3.3.1 + node-fetch: 3.3.2 + node-stream-zip: 1.15.0 + p-retry: 5.1.2 + p-wait-for: 5.0.2 + path-key: 4.0.0 + semver: 7.7.1 + tmp-promise: 3.0.3 + urlpattern-polyfill: 8.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@netlify/edge-functions@2.11.1': {} + + '@netlify/framework-info@9.9.1': + dependencies: + ajv: 8.17.1 + filter-obj: 5.1.0 + find-up: 6.3.0 + is-plain-obj: 4.1.0 + locate-path: 7.2.0 + p-filter: 4.1.0 + p-locate: 6.0.0 + process: 0.11.10 + read-package-up: 11.0.0 + semver: 7.7.1 + + '@netlify/functions-utils@5.3.8(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0)': + dependencies: + '@netlify/zip-it-and-ship-it': 9.43.1(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + cpy: 9.0.1 + path-exists: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@netlify/git-utils@5.2.0': + dependencies: + execa: 6.1.0 + map-obj: 5.0.2 + micromatch: 4.0.8 + moize: 6.1.6 + path-exists: 5.0.0 + + '@netlify/headers-parser@7.3.0': + dependencies: + '@iarna/toml': 2.2.5 + escape-string-regexp: 5.0.0 + fast-safe-stringify: 2.1.1 + is-plain-obj: 4.1.0 + map-obj: 5.0.2 + path-exists: 5.0.0 + + '@netlify/local-functions-proxy-darwin-arm64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-darwin-x64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-freebsd-arm64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-freebsd-x64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-linux-arm64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-linux-arm@1.1.1': + optional: true + + '@netlify/local-functions-proxy-linux-ia32@1.1.1': + optional: true + + '@netlify/local-functions-proxy-linux-ppc64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-linux-x64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-openbsd-x64@1.1.1': + optional: true + + '@netlify/local-functions-proxy-win32-ia32@1.1.1': + optional: true + + '@netlify/local-functions-proxy-win32-x64@1.1.1': + optional: true + + '@netlify/local-functions-proxy@1.1.1': + optionalDependencies: + '@netlify/local-functions-proxy-darwin-arm64': 1.1.1 + '@netlify/local-functions-proxy-darwin-x64': 1.1.1 + '@netlify/local-functions-proxy-freebsd-arm64': 1.1.1 + '@netlify/local-functions-proxy-freebsd-x64': 1.1.1 + '@netlify/local-functions-proxy-linux-arm': 1.1.1 + '@netlify/local-functions-proxy-linux-arm64': 1.1.1 + '@netlify/local-functions-proxy-linux-ia32': 1.1.1 + '@netlify/local-functions-proxy-linux-ppc64': 1.1.1 + '@netlify/local-functions-proxy-linux-x64': 1.1.1 + '@netlify/local-functions-proxy-openbsd-x64': 1.1.1 + '@netlify/local-functions-proxy-win32-ia32': 1.1.1 + '@netlify/local-functions-proxy-win32-x64': 1.1.1 + + '@netlify/open-api@2.36.0': {} + + '@netlify/opentelemetry-utils@1.3.0(@opentelemetry/api@1.8.0)': + dependencies: + '@opentelemetry/api': 1.8.0 + + '@netlify/plugins-list@6.80.0': {} + + '@netlify/redirect-parser@14.5.0': + dependencies: + '@iarna/toml': 2.2.5 + fast-safe-stringify: 2.1.1 + filter-obj: 5.1.0 + is-plain-obj: 4.1.0 + path-exists: 5.0.0 + + '@netlify/run-utils@5.2.0': + dependencies: + execa: 6.1.0 + + '@netlify/serverless-functions-api@1.34.0': {} + + '@netlify/zip-it-and-ship-it@9.42.5(encoding@0.1.13)(rollup@4.35.0)': + dependencies: + '@babel/parser': 7.26.9 + '@babel/types': 7.26.5 + '@netlify/binary-info': 1.0.0 + '@netlify/serverless-functions-api': 1.34.0 + '@vercel/nft': 0.27.7(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + archiver: 7.0.1 + common-path-prefix: 3.0.0 + cp-file: 10.0.0 + es-module-lexer: 1.6.0 + esbuild: 0.19.11 + execa: 7.2.0 + fast-glob: 3.3.3 + filter-obj: 5.1.0 + find-up: 6.3.0 + glob: 8.1.0 + is-builtin-module: 3.2.1 + is-path-inside: 4.0.0 + junk: 4.0.1 + locate-path: 7.2.0 + merge-options: 3.0.4 + minimatch: 9.0.5 + normalize-path: 3.0.0 + p-map: 7.0.3 + path-exists: 5.0.0 + precinct: 11.0.5(supports-color@9.4.0) + require-package-name: 2.0.1 + resolve: 2.0.0-next.5 + semver: 7.7.1 + tmp-promise: 3.0.3 + toml: 3.0.0 + unixify: 1.0.0 + urlpattern-polyfill: 8.0.2 + yargs: 17.7.2 + zod: 3.24.2 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@netlify/zip-it-and-ship-it@9.42.6(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0)': + dependencies: + '@babel/parser': 7.26.9 + '@babel/types': 7.26.9 + '@netlify/binary-info': 1.0.0 + '@netlify/serverless-functions-api': 1.34.0 + '@vercel/nft': 0.27.7(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + archiver: 7.0.1 + common-path-prefix: 3.0.0 + cp-file: 10.0.0 + es-module-lexer: 1.6.0 + esbuild: 0.19.11 + execa: 7.2.0 + fast-glob: 3.3.3 + filter-obj: 5.1.0 + find-up: 6.3.0 + glob: 8.1.0 + is-builtin-module: 3.2.1 + is-path-inside: 4.0.0 + junk: 4.0.1 + locate-path: 7.2.0 + merge-options: 3.0.4 + minimatch: 9.0.5 + normalize-path: 3.0.0 + p-map: 7.0.3 + path-exists: 5.0.0 + precinct: 11.0.5(supports-color@9.4.0) + require-package-name: 2.0.1 + resolve: 2.0.0-next.5 + semver: 7.7.1 + tmp-promise: 3.0.3 + toml: 3.0.0 + unixify: 1.0.0 + urlpattern-polyfill: 8.0.2 + yargs: 17.7.2 + zod: 3.24.2 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@netlify/zip-it-and-ship-it@9.43.1(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0)': + dependencies: + '@babel/parser': 7.26.9 + '@babel/types': 7.26.9 + '@netlify/binary-info': 1.0.0 + '@netlify/serverless-functions-api': 1.34.0 + '@vercel/nft': 0.27.7(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + archiver: 7.0.1 + common-path-prefix: 3.0.0 + cp-file: 10.0.0 + es-module-lexer: 1.6.0 + esbuild: 0.19.11 + execa: 7.2.0 + fast-glob: 3.3.3 + filter-obj: 5.1.0 + find-up: 6.3.0 + glob: 8.1.0 + is-builtin-module: 3.2.1 + is-path-inside: 4.0.0 + junk: 4.0.1 + locate-path: 7.2.0 + merge-options: 3.0.4 + minimatch: 9.0.5 + normalize-path: 3.0.0 + p-map: 7.0.3 + path-exists: 5.0.0 + precinct: 11.0.5(supports-color@9.4.0) + require-package-name: 2.0.1 + resolve: 2.0.0-next.5 + semver: 7.7.1 + tmp-promise: 3.0.3 + toml: 3.0.0 + unixify: 1.0.0 + urlpattern-polyfill: 8.0.2 + yargs: 17.7.2 + zod: 3.24.2 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + '@noble/curves@1.8.1': dependencies: '@noble/hashes': 1.7.1 @@ -5480,6 +9513,135 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.4': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.1 + '@octokit/request': 9.2.2 + '@octokit/request-error': 6.1.7 + '@octokit/types': 13.8.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.3': + dependencies: + '@octokit/types': 13.8.0 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.2.1': + dependencies: + '@octokit/request': 9.2.2 + '@octokit/types': 13.8.0 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@23.0.1': {} + + '@octokit/plugin-paginate-rest@11.4.3(@octokit/core@6.1.4)': + dependencies: + '@octokit/core': 6.1.4 + '@octokit/types': 13.8.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.4)': + dependencies: + '@octokit/core': 6.1.4 + + '@octokit/plugin-rest-endpoint-methods@13.3.1(@octokit/core@6.1.4)': + dependencies: + '@octokit/core': 6.1.4 + '@octokit/types': 13.8.0 + + '@octokit/request-error@6.1.7': + dependencies: + '@octokit/types': 13.8.0 + + '@octokit/request@9.2.2': + dependencies: + '@octokit/endpoint': 10.1.3 + '@octokit/request-error': 6.1.7 + '@octokit/types': 13.8.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.2 + + '@octokit/rest@21.1.1': + dependencies: + '@octokit/core': 6.1.4 + '@octokit/plugin-paginate-rest': 11.4.3(@octokit/core@6.1.4) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.4) + '@octokit/plugin-rest-endpoint-methods': 13.3.1(@octokit/core@6.1.4) + + '@octokit/types@13.8.0': + dependencies: + '@octokit/openapi-types': 23.0.1 + + '@opentelemetry/api@1.8.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-wasm@2.5.1': + dependencies: + is-glob: 4.0.3 + micromatch: 4.0.8 + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + '@perma/map@1.0.3': dependencies: '@multiformats/murmur3': 2.1.8 @@ -5488,6 +9650,18 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -5511,6 +9685,14 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@rollup/pluginutils@5.1.4(rollup@4.35.0)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.35.0 + '@rollup/rollup-android-arm-eabi@4.35.0': optional: true @@ -5590,6 +9772,17 @@ snapshots: '@sideway/pinpoint@2.0.0': {} + '@sindresorhus/is@5.6.0': {} + + '@sindresorhus/slugify@2.2.1': + dependencies: + '@sindresorhus/transliterate': 1.6.0 + escape-string-regexp: 5.0.0 + + '@sindresorhus/transliterate@1.6.0': + dependencies: + escape-string-regexp: 5.0.0 + '@smithy/abort-controller@4.0.1': dependencies: '@smithy/types': 4.1.0 @@ -5923,6 +10116,22 @@ snapshots: '@storacha/one-webcrypto@1.0.1': {} + '@szmarczak/http-timer@5.0.1': + dependencies: + defer-to-connect: 2.0.1 + + '@tokenizer/token@0.3.0': {} + + '@trysound/sax@0.2.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@types/aws-lambda@8.10.147': {} '@types/better-sqlite3@7.6.12': @@ -5935,6 +10144,22 @@ snapshots: '@types/estree@1.0.6': {} + '@types/http-cache-semantics@4.0.4': {} + + '@types/http-proxy@1.17.16': + dependencies: + '@types/node': 22.13.10 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/minimatch@3.0.5': {} @@ -5943,8 +10168,14 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/normalize-package-data@2.4.4': {} + + '@types/retry@0.12.1': {} + '@types/retry@0.12.2': {} + '@types/triple-beam@1.3.5': {} + '@types/uuid@9.0.8': {} '@types/wait-on@5.3.4': @@ -5955,15 +10186,26 @@ snapshots: dependencies: '@types/node': 22.13.10 - '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3)': + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@16.0.9': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.13.10 + optional: true + + '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.26.0 - '@typescript-eslint/type-utils': 8.26.0(eslint@9.22.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.26.0(eslint@9.22.0)(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.26.0 - eslint: 9.22.0 + eslint: 9.22.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -5972,14 +10214,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.26.0 '@typescript-eslint/types': 8.26.0 '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.26.0 - debug: 4.4.0 - eslint: 9.22.0 + debug: 4.4.0(supports-color@9.4.0) + eslint: 9.22.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -5989,24 +10231,40 @@ snapshots: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.26.0(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.26.0(eslint@9.22.0)(typescript@5.7.3) - debug: 4.4.0 - eslint: 9.22.0 + '@typescript-eslint/utils': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) + debug: 4.4.0(supports-color@9.4.0) + eslint: 9.22.0(jiti@2.4.2) ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/types@5.62.0': {} + '@typescript-eslint/types@8.26.0': {} + '@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.7.3)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.0(supports-color@9.4.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.1 + tsutils: 3.21.0(typescript@5.7.3) + optionalDependencies: + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -6016,17 +10274,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.0(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/utils@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.26.0 '@typescript-eslint/types': 8.26.0 '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.7.3) - eslint: 9.22.0 + eslint: 9.22.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.26.0': dependencies: '@typescript-eslint/types': 8.26.0 @@ -6080,6 +10343,25 @@ snapshots: '@ucanto/interface': 10.2.0 multiformats: 13.3.2 + '@vercel/nft@0.27.7(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0)': + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)(supports-color@9.4.0) + '@rollup/pluginutils': 5.1.4(rollup@4.35.0) + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + node-gyp-build: 4.8.4 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + '@vitest/expect@3.0.8': dependencies: '@vitest/spy': 3.0.8 @@ -6087,13 +10369,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.8(vite@6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0))': + '@vitest/mocker@3.0.8(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) '@vitest/pretty-format@3.0.8': dependencies: @@ -6268,6 +10550,76 @@ snapshots: transitivePeerDependencies: - encoding + '@xhmikosr/archive-type@6.0.1': + dependencies: + file-type: 18.7.0 + + '@xhmikosr/decompress-tar@7.0.0': + dependencies: + file-type: 18.7.0 + is-stream: 3.0.0 + tar-stream: 3.1.7 + + '@xhmikosr/decompress-tarbz2@7.0.0': + dependencies: + '@xhmikosr/decompress-tar': 7.0.0 + file-type: 18.7.0 + is-stream: 3.0.0 + seek-bzip: 1.0.6 + unbzip2-stream: 1.4.3 + + '@xhmikosr/decompress-targz@7.0.0': + dependencies: + '@xhmikosr/decompress-tar': 7.0.0 + file-type: 18.7.0 + is-stream: 3.0.0 + + '@xhmikosr/decompress-unzip@6.0.0': + dependencies: + file-type: 18.7.0 + get-stream: 6.0.1 + yauzl: 2.10.0 + + '@xhmikosr/decompress@9.0.1': + dependencies: + '@xhmikosr/decompress-tar': 7.0.0 + '@xhmikosr/decompress-tarbz2': 7.0.0 + '@xhmikosr/decompress-targz': 7.0.0 + '@xhmikosr/decompress-unzip': 6.0.0 + graceful-fs: 4.2.11 + make-dir: 4.0.0 + strip-dirs: 3.0.0 + + '@xhmikosr/downloader@13.0.1': + dependencies: + '@xhmikosr/archive-type': 6.0.1 + '@xhmikosr/decompress': 9.0.1 + content-disposition: 0.5.4 + ext-name: 5.0.0 + file-type: 18.7.0 + filenamify: 5.1.1 + get-stream: 6.0.1 + got: 12.6.1 + merge-options: 3.0.4 + p-event: 5.0.1 + + abbrev@1.1.1: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + abstract-logging@2.0.1: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-import-attributes@1.9.5(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -6286,10 +10638,31 @@ snapshots: actor@2.3.1: {} + agent-base@6.0.2(supports-color@9.4.0): + dependencies: + debug: 4.4.0(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.3: {} + + aggregate-error@4.0.1: + dependencies: + clean-stack: 4.2.0 + indent-string: 5.0.0 + + ajv-errors@3.0.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -6304,22 +10677,90 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-escapes@3.2.0: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@5.0.0: + dependencies: + type-fest: 1.4.0 + + ansi-escapes@6.2.1: {} + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} + ansi-to-html@0.7.2: + dependencies: + entities: 2.2.0 + any-promise@1.3.0: {} any-signal@3.0.1: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.0.0: {} + + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + archy@1.0.0: {} + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + arg@4.1.3: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -6350,6 +10791,8 @@ snapshots: to-boolean-x: 2.1.1 to-object-x: 2.2.1 + array-flatten@1.1.1: {} + array-for-each-x@3.1.2: dependencies: array-all-x: 1.1.2 @@ -6401,12 +10844,20 @@ snapshots: to-boolean-x: 2.1.1 to-object-x: 2.2.1 + array-timsort@1.0.3: {} + + array-union@2.1.0: {} + arraybuffer-equal@1.0.4: {} + arrify@3.0.0: {} + as-table@1.0.55: dependencies: printable-characters: 1.0.42 + ascii-table@0.0.9: {} + assert-is-function-x@3.1.2: dependencies: is-function-x: 4.1.2 @@ -6428,8 +10879,16 @@ snapshots: assertion-error@2.0.1: {} + ast-module-types@5.0.0: {} + + async-sema@3.1.1: {} + + async@3.2.6: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + atomically@2.0.3: dependencies: stubborn-fs: 1.2.5 @@ -6444,6 +10903,11 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + avvio@8.4.0: + dependencies: + '@fastify/error': 3.4.1 + fastq: 1.19.0 + aws-lambda@1.0.7: dependencies: aws-sdk: 2.1692.0 @@ -6468,16 +10932,60 @@ snapshots: axios@1.7.9: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.4.0) form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + b4a@1.6.7: {} + + backoff@2.5.0: + dependencies: + precond: 0.2.3 + balanced-match@1.0.2: {} + bare-events@2.5.4: + optional: true + + bare-fs@4.0.1: + dependencies: + bare-events: 2.5.4 + bare-path: 3.0.0 + bare-stream: 2.6.5(bare-events@2.5.4) + transitivePeerDependencies: + - bare-buffer + optional: true + + bare-os@3.5.1: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.5.1 + optional: true + + bare-stream@2.6.5(bare-events@2.5.4): + dependencies: + streamx: 2.22.0 + optionalDependencies: + bare-events: 2.5.4 + optional: true + base64-js@1.5.1: {} + before-after-hook@3.0.2: {} + + better-ajv-errors@1.2.0(ajv@8.17.1): + dependencies: + '@babel/code-frame': 7.26.2 + '@humanwhocodes/momoa': 2.0.4 + ajv: 8.17.1 + chalk: 4.1.2 + jsonpointer: 5.0.1 + leven: 3.1.0 + better-sqlite3@11.8.1: dependencies: bindings: 1.5.0 @@ -6494,6 +11002,8 @@ snapshots: bigint-mod-arith@3.3.1: {} + binary-extensions@2.3.0: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -6508,8 +11018,38 @@ snapshots: blake3-wasm@2.1.5: {} + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.0: {} + bowser@2.11.0: {} + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.26.1 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -6525,6 +11065,14 @@ snapshots: browser-readablestream-to-it@1.0.3: {} + buffer-crc32@0.2.13: {} + + buffer-crc32@1.0.0: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + buffer@4.9.2: dependencies: base64-js: 1.5.1 @@ -6541,13 +11089,39 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + builtin-modules@3.3.0: {} + + builtins@5.1.0: + dependencies: + semver: 7.7.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + bundle-require@5.1.0(esbuild@0.25.1): dependencies: esbuild: 0.25.1 load-tsconfig: 0.2.5 + byline@5.0.0: {} + + bytes@3.1.2: {} + cac@6.7.14: {} + cacheable-lookup@7.0.0: {} + + cacheable-request@10.2.14: + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.1 + responselike: 3.0.0 + cached-constructors-x@2.2.1: dependencies: noop-x: 1.2.1 @@ -6559,6 +11133,11 @@ snapshots: to-length-x: 4.2.2 to-object-x: 2.2.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -6567,11 +11146,22 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsite@1.0.0: {} + callsites@3.1.0: {} + camelcase@6.3.0: {} + + camelcase@8.0.0: {} + capnp-ts@0.7.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -6592,23 +11182,69 @@ snapshots: loupe: 3.1.3 pathval: 2.0.0 + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.4.1: {} + + chardet@0.7.0: {} + charwise@3.0.1: {} check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 chownr@1.1.4: {} + chownr@2.0.0: {} + + ci-info@4.1.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.0 + cjs-module-lexer@1.4.3: {} + clean-deep@3.4.0: + dependencies: + lodash.isempty: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.transform: 4.6.0 + + clean-stack@4.2.0: + dependencies: + escape-string-regexp: 5.0.0 + + clean-stack@5.2.0: + dependencies: + escape-string-regexp: 5.0.0 + + cli-boxes@3.0.0: {} + cli-color@2.0.4: dependencies: d: 1.0.2 @@ -6617,16 +11253,40 @@ snapshots: memoizee: 0.4.17 timers-ext: 0.1.8 + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@2.2.1: {} + + cli-width@3.0.0: {} + clipboardy@4.0.0: dependencies: execa: 8.0.1 is-wsl: 3.1.0 is64bit: 2.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cmd-ts@0.13.0: dependencies: chalk: 4.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) didyoumean: 1.2.2 strip-ansi: 6.0.1 transitivePeerDependencies: @@ -6663,32 +11323,85 @@ snapshots: symbol-species-x: 1.1.2 to-boolean-x: 2.1.1 + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} color-string@1.9.1: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 - optional: true + + color-support@1.1.3: {} + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 color@4.2.3: dependencies: color-convert: 2.0.1 color-string: 1.9.1 - optional: true + + colors-option@3.0.0: + dependencies: + chalk: 5.4.1 + filter-obj: 3.0.0 + is-plain-obj: 4.1.0 + jest-validate: 27.5.1 + + colors@1.4.0: {} + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@10.0.1: {} + + commander@12.1.0: {} + + commander@2.20.3: {} + commander@3.0.2: {} commander@4.1.1: {} + commander@7.2.0: {} + + commander@9.5.0: {} + + comment-json@4.2.5: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + + common-path-prefix@3.0.0: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + concat-map@0.0.1: {} conf@11.0.2: @@ -6702,17 +11415,122 @@ snapshots: json-schema-typed: 8.0.1 semver: 7.7.1 + confbox@0.1.8: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@7.0.0: + dependencies: + atomically: 2.0.3 + dot-prop: 9.0.0 + graceful-fs: 4.2.11 + xdg-basedir: 5.1.0 + consola@3.4.0: {} + console-control-strings@1.1.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-es@1.2.2: {} + + cookie-signature@1.0.6: {} + cookie@0.5.0: {} + cookie@0.7.1: {} + cookie@0.7.2: {} - cross-spawn@7.0.6: + cookie@1.0.2: {} + + core-util-is@1.0.3: {} + + cp-file@10.0.0: + dependencies: + graceful-fs: 4.2.11 + nested-error-stacks: 2.1.1 + p-event: 5.0.1 + + cp-file@9.1.0: + dependencies: + graceful-fs: 4.2.11 + make-dir: 3.1.0 + nested-error-stacks: 2.1.1 + p-event: 4.2.0 + + cpy@9.0.1: + dependencies: + arrify: 3.0.0 + cp-file: 9.1.0 + globby: 13.2.2 + junk: 4.0.1 + micromatch: 4.0.8 + nested-error-stacks: 2.1.1 + p-filter: 3.0.0 + p-map: 5.5.0 + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + + create-require@1.1.1: {} + + cron-parser@4.9.0: + dependencies: + luxon: 3.5.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.3.4: + dependencies: + uncrypto: 0.1.3 + + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssfilter@0.0.10: {} + + csso@5.0.5: dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 + css-tree: 2.2.1 + + cyclist@1.0.2: {} d@1.0.2: dependencies: @@ -6727,13 +11545,23 @@ snapshots: dependencies: mimic-fn: 4.0.0 + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.3.7: dependencies: ms: 2.1.3 - debug@4.4.0: + debug@4.4.0(supports-color@9.4.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 9.4.0 + + decache@4.6.2: + dependencies: + callsite: 1.0.0 decompress-response@6.0.0: dependencies: @@ -6745,12 +11573,25 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 es-errors: 1.3.0 gopd: 1.0.1 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -6761,30 +11602,133 @@ snapshots: delayed-stream@1.0.0: {} + delegates@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + destr@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@1.0.3: {} + detect-libc@2.0.2: {} detect-libc@2.0.3: {} + detective-amd@5.0.2: + dependencies: + ast-module-types: 5.0.0 + escodegen: 2.1.0 + get-amd-module-type: 5.0.1 + node-source-walk: 6.0.2 + + detective-cjs@5.0.1: + dependencies: + ast-module-types: 5.0.0 + node-source-walk: 6.0.2 + + detective-es6@4.0.1: + dependencies: + node-source-walk: 6.0.2 + + detective-postcss@6.1.3: + dependencies: + is-url: 1.2.4 + postcss: 8.5.1 + postcss-values-parser: 6.0.2(postcss@8.5.1) + + detective-sass@5.0.3: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 6.0.2 + + detective-scss@4.0.3: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 6.0.2 + + detective-stylus@4.0.0: {} + + detective-typescript@11.2.0(supports-color@9.4.0): + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(supports-color@9.4.0)(typescript@5.7.3) + ast-module-types: 5.0.0 + node-source-walk: 6.0.2 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + devalue@4.3.3: {} didyoumean@1.2.2: {} + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dot-prop@7.2.0: dependencies: type-fest: 2.19.0 + dot-prop@9.0.0: + dependencies: + type-fest: 4.26.1 + dotenv@16.4.7: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + electron-fetch@1.9.1: dependencies: encoding: 0.1.13 + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + enabled@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -6793,18 +11737,40 @@ snapshots: dependencies: once: 1.4.0 + entities@2.2.0: {} + + entities@4.5.0: {} + env-paths@3.0.0: {} + envinfo@7.14.0: {} + + environment@1.1.0: {} + err-code@3.0.1: {} + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-module-lexer@1.6.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es5-ext@0.10.64: dependencies: es6-iterator: 2.0.3 @@ -6818,6 +11784,8 @@ snapshots: es5-ext: 0.10.64 es6-symbol: 3.1.4 + es6-promisify@6.1.1: {} + es6-symbol@3.1.4: dependencies: d: 1.0.2 @@ -6861,6 +11829,58 @@ snapshots: '@esbuild/win32-ia32': 0.17.19 '@esbuild/win32-x64': 0.17.19 + esbuild@0.19.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + + esbuild@0.21.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.2 + '@esbuild/android-arm': 0.21.2 + '@esbuild/android-arm64': 0.21.2 + '@esbuild/android-x64': 0.21.2 + '@esbuild/darwin-arm64': 0.21.2 + '@esbuild/darwin-x64': 0.21.2 + '@esbuild/freebsd-arm64': 0.21.2 + '@esbuild/freebsd-x64': 0.21.2 + '@esbuild/linux-arm': 0.21.2 + '@esbuild/linux-arm64': 0.21.2 + '@esbuild/linux-ia32': 0.21.2 + '@esbuild/linux-loong64': 0.21.2 + '@esbuild/linux-mips64el': 0.21.2 + '@esbuild/linux-ppc64': 0.21.2 + '@esbuild/linux-riscv64': 0.21.2 + '@esbuild/linux-s390x': 0.21.2 + '@esbuild/linux-x64': 0.21.2 + '@esbuild/netbsd-x64': 0.21.2 + '@esbuild/openbsd-x64': 0.21.2 + '@esbuild/sunos-x64': 0.21.2 + '@esbuild/win32-arm64': 0.21.2 + '@esbuild/win32-ia32': 0.21.2 + '@esbuild/win32-x64': 0.21.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -6915,8 +11935,26 @@ snapshots: '@esbuild/win32-ia32': 0.25.1 '@esbuild/win32-x64': 0.25.1 + escalade@3.2.0: {} + + escape-goat@4.0.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + eslint-scope@8.3.0: dependencies: esrecurse: 4.3.0 @@ -6926,9 +11964,9 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.22.0: + eslint@9.22.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 '@eslint/config-helpers': 0.1.0 @@ -6944,7 +11982,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) escape-string-regexp: 4.0.0 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -6963,6 +12001,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 transitivePeerDependencies: - supports-color @@ -6993,25 +12033,69 @@ snapshots: estree-walker@0.6.1: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 esutils@2.0.3: {} + etag@1.8.1: {} + event-emitter@0.3.5: dependencies: d: 1.0.2 es5-ext: 0.10.64 + event-target-shim@5.0.1: {} + event-target-shim@6.0.2: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.1: {} events@1.1.1: {} events@3.3.0: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@6.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 3.0.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + + execa@7.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -7030,14 +12114,87 @@ snapshots: expect-type@1.2.0: {} + express-logging@1.1.1: + dependencies: + on-headers: 1.0.2 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.4: {} + ext-list@2.2.2: + dependencies: + mime-db: 1.52.0 + + ext-name@5.0.0: + dependencies: + ext-list: 2.2.2 + sort-keys-length: 1.0.1 + ext@1.7.0: dependencies: type: 2.7.3 + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extract-zip@2.0.1: + dependencies: + debug: 4.4.0(supports-color@9.4.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-content-type-parse@1.1.0: {} + + fast-content-type-parse@2.0.1: {} + + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} + fast-equals@3.0.3: {} + fast-fifo@1.3.2: {} fast-glob@3.3.3: @@ -7050,37 +12207,138 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@5.16.1: + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-deep-equal: 3.1.3 + fast-uri: 2.4.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@2.4.0: {} + fast-uri@3.0.2: {} fast-xml-parser@4.4.1: dependencies: strnum: 1.1.2 + fastest-levenshtein@1.0.16: {} + + fastify-plugin@4.5.1: {} + + fastify@4.29.0: + dependencies: + '@fastify/ajv-compiler': 3.6.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.4.0 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.16.1 + find-my-way: 8.2.2 + light-my-request: 5.14.0 + pino: 9.6.0 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.4.1 + secure-json-parse: 2.7.0 + semver: 7.7.1 + toad-cache: 3.7.0 + + fastq@1.19.0: + dependencies: + reusify: 1.1.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 + fecha@4.2.3: {} + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@4.0.1: + dependencies: + escape-string-regexp: 5.0.0 + is-unicode-supported: 1.3.0 + + figures@5.0.0: + dependencies: + escape-string-regexp: 5.0.0 + is-unicode-supported: 1.3.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-type@18.7.0: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 7.1.1 + token-types: 5.0.1 + file-uri-to-path@1.0.0: {} + filename-reserved-regex@3.0.0: {} + + filenamify@5.1.1: + dependencies: + filename-reserved-regex: 3.0.0 + strip-outer: 2.0.0 + trim-repeated: 2.0.0 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + filter-obj@3.0.0: {} + + filter-obj@5.1.0: {} + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + find-index-x@3.1.2: dependencies: assert-is-function-x: 3.1.2 @@ -7089,11 +12347,30 @@ snapshots: to-length-x: 4.2.2 to-object-x: 2.2.1 + find-my-way@8.2.2: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 3.1.0 + + find-up-simple@1.0.1: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -7101,7 +12378,20 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + flush-write-stream@2.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + fn.name@1.1.0: {} + + folder-walker@3.2.0: + dependencies: + from2: 2.3.0 + + follow-redirects@1.15.9(debug@4.4.0): + optionalDependencies: + debug: 4.4.0(supports-color@9.4.0) for-each@0.3.3: dependencies: @@ -7112,6 +12402,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data-encoder@2.1.4: {} + form-data@4.0.1: dependencies: asynckit: 0.4.0 @@ -7122,13 +12414,55 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + from2-array@0.0.4: + dependencies: + from2: 2.3.0 + + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + fs-constants@1.0.0: {} + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + fuzzy@0.1.3: {} + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + get-amd-module-type@5.0.1: + dependencies: + ast-module-types: 5.0.0 + node-source-walk: 6.0.2 + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -7137,6 +12471,19 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-iterator@1.0.2: {} get-own-enumerable-keys-x@2.1.2: @@ -7168,6 +12515,19 @@ snapshots: has-symbol-support-x: 2.1.2 to-object-x: 2.2.1 + get-package-name@2.2.0: {} + + get-port-please@3.1.2: {} + + get-port@6.1.2: {} + + get-port@7.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-prototype-of-x@2.1.2: dependencies: is-function-x: 4.1.2 @@ -7179,12 +12539,30 @@ snapshots: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.2 + + get-stream@6.0.1: {} + get-stream@8.0.1: {} get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 + gh-release-fetch@4.0.3: + dependencies: + '@xhmikosr/downloader': 13.0.1 + node-fetch: 3.3.2 + semver: 7.7.1 + + git-repo-info@2.1.1: {} + + gitconfiglocal@2.1.0: + dependencies: + ini: 1.3.8 + github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -7206,18 +12584,90 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@14.0.0: {} + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + globrex@0.1.2: {} + gonzales-pe@4.3.0: + dependencies: + minimist: 1.2.8 + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + + got@12.6.1: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + + graceful-fs@4.2.10: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} + h3@1.15.1: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.4 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.0 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + hamt-sharding@3.0.6: dependencies: sparse-array: 1.3.2 @@ -7227,8 +12677,12 @@ snapshots: has-boxed-string-x@2.1.1: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} + has-own-prop@2.0.0: {} + has-own-property-x@4.1.2: dependencies: simple-methodize-x: 1.0.4 @@ -7248,6 +12702,8 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-to-string-tag-x@2.1.2: dependencies: has-symbol-support-x: 2.1.2 @@ -7257,6 +12713,8 @@ snapshots: dependencies: has-symbols: 1.0.3 + has-unicode@2.0.1: {} + has-working-bind-x@1.0.1: dependencies: noop-x: 1.2.1 @@ -7269,8 +12727,87 @@ snapshots: hono@4.7.4: {} + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + hot-shots@10.2.1: + optionalDependencies: + unix-dgram: 2.0.6 + + http-cache-semantics@4.1.1: {} + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-middleware@2.0.7(debug@4.4.0): + dependencies: + '@types/http-proxy': 1.17.16 + http-proxy: 1.18.1(debug@4.4.0) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1(debug@4.4.0): + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9(debug@4.4.0) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-shutdown@1.2.2: {} + + http2-wrapper@2.2.1: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1(supports-color@9.4.0): + dependencies: + agent-base: 6.0.2(supports-color@9.4.0) + debug: 4.4.0(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@3.0.1: {} + + human-signals@4.3.1: {} + human-signals@5.0.0: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -7285,6 +12822,8 @@ snapshots: ignore@5.3.2: {} + image-meta@0.2.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7292,6 +12831,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@5.0.0: {} + index-of-x@3.1.2: dependencies: attempt-x: 2.1.2 @@ -7308,14 +12849,68 @@ snapshots: to-length-x: 4.2.2 to-object-x: 2.2.1 + index-to-position@0.1.2: {} + infinity-x@2.2.1: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.3: {} inherits@2.0.4: {} ini@1.3.8: {} + ini@4.1.1: {} + + inquirer-autocomplete-prompt@1.4.0(inquirer@8.0.0): + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + figures: 3.2.0 + inquirer: 8.0.0 + run-async: 2.4.1 + rxjs: 6.6.7 + + inquirer@6.5.2: + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.17.21 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 + + inquirer@8.0.0: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + + inspect-with-kind@1.0.5: + dependencies: + kind-of: 6.0.3 + interface-blockstore@5.3.1: dependencies: interface-store: 6.0.2 @@ -7323,6 +12918,8 @@ snapshots: interface-store@6.0.2: {} + ipaddr.js@1.9.1: {} + ipfs-unixfs-exporter@13.6.2: dependencies: '@ipld/dag-cbor': 9.2.2 @@ -7368,6 +12965,47 @@ snapshots: transitivePeerDependencies: - encoding + ipx@2.1.0(@netlify/blobs@8.1.1)(aws4fetch@1.0.20)(idb-keyval@6.2.1): + dependencies: + '@fastify/accept-negotiator': 1.1.0 + citty: 0.1.6 + consola: 3.4.0 + defu: 6.1.4 + destr: 2.0.3 + etag: 1.8.1 + h3: 1.15.1 + image-meta: 0.2.1 + listhen: 1.9.0 + ofetch: 1.4.1 + pathe: 1.1.2 + sharp: 0.32.6 + svgo: 3.3.2 + ufo: 1.5.4 + unstorage: 1.15.0(@netlify/blobs@8.1.1)(aws4fetch@1.0.20)(idb-keyval@6.2.1) + xss: 1.0.15 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bare-buffer + - db0 + - idb-keyval + - ioredis + - uploadthing + + iron-webcrypto@1.2.1: {} + is-arguments@1.1.1: dependencies: call-bind: 1.0.7 @@ -7394,13 +13032,18 @@ snapshots: attempt-x: 2.1.2 to-string-tag-x: 2.1.2 - is-arrayish@0.3.2: - optional: true + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-boolean-object@1.1.2: dependencies: call-bind: 1.0.7 @@ -7412,8 +13055,16 @@ snapshots: is-buffer@2.0.5: {} + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + is-callable@1.2.7: {} + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-data-view-x@2.1.2: dependencies: attempt-x: 2.1.2 @@ -7482,8 +13133,14 @@ snapshots: infinity-x: 2.2.1 is-nan-x: 2.1.1 + is-fullwidth-code-point@2.0.0: {} + is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + is-function-x@4.1.2: dependencies: attempt-x: 2.1.2 @@ -7503,6 +13160,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-in-ci@1.0.0: {} + is-index-x@2.1.2: dependencies: math-clamp-x: 3.2.2 @@ -7514,11 +13173,18 @@ snapshots: dependencies: is-docker: 3.0.0 + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + is-integer-x@2.2.2: dependencies: is-finite-x: 4.2.1 to-integer-x: 4.2.2 + is-interactive@2.0.0: {} + is-length-x@3.2.2: dependencies: is-safe-integer-x: 2.2.2 @@ -7542,6 +13208,8 @@ snapshots: is-nil-x@2.1.1: {} + is-npm@6.0.0: {} + is-number-object@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -7553,8 +13221,16 @@ snapshots: is-function-x: 4.1.2 is-primitive-x: 1.0.1 + is-path-inside@4.0.0: {} + + is-plain-obj@1.1.0: {} + is-plain-obj@2.1.0: {} + is-plain-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + is-primitive-x@1.0.1: {} is-promise@2.2.2: {} @@ -7581,8 +13257,12 @@ snapshots: simple-call-x: 1.0.3 util-get-getter-x: 1.0.2 + is-stream@2.0.1: {} + is-stream@3.0.0: {} + is-stream@4.0.1: {} + is-string@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -7600,6 +13280,14 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-url-superb@4.0.0: {} + + is-url@1.2.4: {} + is-var-name@2.0.0: {} is-wsl@3.1.0: @@ -7612,8 +13300,12 @@ snapshots: isarray@1.0.0: {} + iserror@0.0.2: {} + isexe@2.0.0: {} + isexe@3.1.1: {} + iso-url@1.2.1: {} it-all@1.0.6: {} @@ -7670,6 +13362,19 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jest-get-type@27.5.1: {} + + jest-validate@27.5.1: + dependencies: + '@jest/types': 27.5.1 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 27.5.1 + leven: 3.1.0 + pretty-format: 27.5.1 + + jiti@2.4.2: {} + jmespath@0.16.0: {} joi@17.13.3: @@ -7699,6 +13404,12 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + + json-schema-ref-resolver@1.0.1: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -7707,10 +13418,68 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonc-parser@3.3.1: {} + + jsonpointer@5.0.1: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.1 + + junk@4.0.1: {} + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + + jwt-decode@4.0.0: {} + + keep-func-props@4.0.1: + dependencies: + mimic-fn: 4.0.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + + kuler@2.0.0: {} + + ky@1.7.5: {} + + lambda-local@2.2.0: + dependencies: + commander: 10.0.1 + dotenv: 16.4.7 + winston: 3.17.0 + + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -7729,22 +13498,110 @@ snapshots: '@libsql/linux-x64-musl': 0.4.7 '@libsql/win32-x64-msvc': 0.4.7 + light-my-request@5.14.0: + dependencies: + cookie: 0.7.2 + process-warning: 3.0.0 + set-cookie-parser: 2.7.1 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + listhen@1.9.0: + dependencies: + '@parcel/watcher': 2.5.1 + '@parcel/watcher-wasm': 2.5.1 + citty: 0.1.6 + clipboardy: 4.0.0 + consola: 3.4.0 + crossws: 0.3.4 + defu: 6.1.4 + get-port-please: 3.1.2 + h3: 1.15.1 + http-shutdown: 1.2.2 + jiti: 2.4.2 + mlly: 1.7.4 + node-forge: 1.3.1 + pathe: 1.1.2 + std-env: 3.8.0 + ufo: 1.5.4 + untun: 0.1.3 + uqr: 0.1.2 + load-tsconfig@0.2.5: {} locate-path@6.0.0: dependencies: p-locate: 5.0.0 + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isempty@4.4.0: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.sortby@4.7.0: {} + lodash.transform@4.6.0: {} + lodash@4.17.21: {} + log-process-errors@8.0.0: + dependencies: + colors-option: 3.0.0 + figures: 4.0.1 + filter-obj: 3.0.0 + jest-validate: 27.5.1 + map-obj: 5.0.2 + moize: 6.1.6 + semver: 7.7.1 + + log-symbols@6.0.0: + dependencies: + chalk: 5.4.1 + is-unicode-supported: 1.3.0 + + log-symbols@7.0.0: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.1 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + long@5.3.1: {} loose-envify@1.4.0: @@ -7753,12 +13610,22 @@ snapshots: loupe@3.1.3: {} + lowercase-keys@3.0.0: {} + lru-cache@10.4.3: {} + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lru-queue@0.1.0: dependencies: es5-ext: 0.10.64 + luxon@3.5.0: {} + + macos-release@3.3.0: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 @@ -7767,15 +13634,46 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.1 + + make-error@1.3.6: {} + + map-obj@5.0.2: {} + math-clamp-x@3.2.2: dependencies: to-number-x: 3.2.2 + math-intrinsics@1.1.0: {} + math-sign-x@4.2.2: dependencies: is-nan-x: 2.1.1 to-number-x: 3.2.2 + maxstache-stream@1.0.4: + dependencies: + maxstache: 1.0.7 + pump: 1.0.3 + split2: 1.1.1 + through2: 2.0.5 + + maxstache@1.0.7: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + media-typer@0.3.0: {} + + memoize-one@6.0.0: {} + memoizee@0.4.17: dependencies: d: 1.0.2 @@ -7787,6 +13685,8 @@ snapshots: next-tick: 1.1.0 timers-ext: 0.1.8 + merge-descriptors@1.0.3: {} + merge-options@3.0.4: dependencies: is-plain-obj: 2.1.0 @@ -7795,6 +13695,12 @@ snapshots: merge2@1.4.1: {} + methods@1.1.2: {} + + micro-api-client@3.3.0: {} + + micro-memoize@4.1.3: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -7806,12 +13712,22 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@1.6.0: {} + mime@3.0.0: {} + mimic-fn@1.2.0: {} + + mimic-fn@2.1.0: {} + mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} + mimic-response@3.1.0: {} + mimic-response@4.0.0: {} + miniflare@3.20240718.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -7852,32 +13768,86 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 minimist@1.2.8: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + minipass@7.1.2: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + mkdirp-classic@0.5.3: {} + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mlly@1.7.4: + dependencies: + acorn: 8.14.1 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.5.4 + mnemonist@0.38.3: dependencies: obliterator: 1.6.1 + module-definition@5.0.1: + dependencies: + ast-module-types: 5.0.0 + node-source-walk: 6.0.2 + + moize@6.1.6: + dependencies: + fast-equals: 3.0.3 + micro-memoize: 4.1.3 + + move-file@3.1.0: + dependencies: + path-exists: 5.0.0 + mri@1.2.0: {} + ms@2.0.0: {} + ms@2.1.3: {} multiformats@12.1.3: {} multiformats@13.3.2: {} + multiparty@4.2.3: + dependencies: + http-errors: 1.8.1 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + murmurhash3js-revisited@3.0.0: {} mustache@4.2.0: {} + mute-stream@0.0.7: {} + + mute-stream@0.0.8: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -7886,17 +13856,172 @@ snapshots: nan-x@2.2.1: {} + nan@2.22.2: + optional: true + nanoid@3.3.9: {} nanoid@5.1.3: {} - napi-build-utils@1.0.2: {} + napi-build-utils@1.0.2: {} + + native-fetch@3.0.0(node-fetch@2.7.0(encoding@0.1.13)): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + nested-error-stacks@2.1.1: {} + + netlify-cli@19.0.0(@types/node@22.13.10)(aws4fetch@1.0.20)(encoding@0.1.13)(idb-keyval@6.2.1)(picomatch@4.0.2)(rollup@4.35.0): + dependencies: + '@bugsnag/js': 8.2.0 + '@fastify/static': 7.0.4 + '@netlify/blobs': 8.1.1 + '@netlify/build': 29.58.10(@opentelemetry/api@1.8.0)(@types/node@22.13.10)(encoding@0.1.13)(picomatch@4.0.2)(rollup@4.35.0) + '@netlify/build-info': 8.0.0 + '@netlify/config': 20.21.7 + '@netlify/edge-bundler': 12.3.2(encoding@0.1.13)(rollup@4.35.0)(supports-color@9.4.0) + '@netlify/edge-functions': 2.11.1 + '@netlify/headers-parser': 7.3.0 + '@netlify/local-functions-proxy': 1.1.1 + '@netlify/redirect-parser': 14.5.0 + '@netlify/zip-it-and-ship-it': 9.42.5(encoding@0.1.13)(rollup@4.35.0) + '@octokit/rest': 21.1.1 + '@opentelemetry/api': 1.8.0 + ansi-escapes: 7.0.0 + ansi-to-html: 0.7.2 + ascii-table: 0.0.9 + backoff: 2.5.0 + boxen: 8.0.1 + chalk: 5.4.1 + chokidar: 3.6.0 + ci-info: 4.1.0 + clean-deep: 3.4.0 + commander: 12.1.0 + comment-json: 4.2.5 + configstore: 7.0.0 + content-type: 1.0.5 + cookie: 1.0.2 + cron-parser: 4.9.0 + debug: 4.4.0(supports-color@9.4.0) + decache: 4.6.2 + dot-prop: 9.0.0 + dotenv: 16.4.7 + env-paths: 3.0.0 + envinfo: 7.14.0 + etag: 1.8.1 + execa: 5.1.1 + express: 4.21.2 + express-logging: 1.1.1 + extract-zip: 2.0.1 + fastest-levenshtein: 1.0.16 + fastify: 4.29.0 + find-up: 7.0.0 + flush-write-stream: 2.0.0 + folder-walker: 3.2.0 + from2-array: 0.0.4 + fuzzy: 0.1.3 + get-port: 7.1.0 + gh-release-fetch: 4.0.3 + git-repo-info: 2.1.1 + gitconfiglocal: 2.1.0 + http-proxy: 1.18.1(debug@4.4.0) + http-proxy-middleware: 2.0.7(debug@4.4.0) + https-proxy-agent: 7.0.6 + inquirer: 8.0.0 + inquirer-autocomplete-prompt: 1.4.0(inquirer@8.0.0) + ipx: 2.1.0(@netlify/blobs@8.1.1)(aws4fetch@1.0.20)(idb-keyval@6.2.1) + is-docker: 3.0.0 + is-stream: 4.0.1 + is-wsl: 3.1.0 + isexe: 3.1.1 + jsonwebtoken: 9.0.2 + jwt-decode: 4.0.0 + lambda-local: 2.2.0 + locate-path: 7.2.0 + lodash: 4.17.21 + log-symbols: 7.0.0 + log-update: 6.1.0 + maxstache: 1.0.7 + maxstache-stream: 1.0.4 + multiparty: 4.2.3 + netlify: 13.3.3 + netlify-redirector: 0.5.0 + node-fetch: 3.3.2 + open: 10.1.0 + ora: 8.2.0 + p-filter: 4.1.0 + p-map: 7.0.3 + p-wait-for: 5.0.2 + parallel-transform: 1.2.0 + parse-github-url: 1.0.3 + parse-gitignore: 2.0.0 + prettyjson: 1.2.5 + pump: 3.0.2 + raw-body: 3.0.0 + read-package-up: 11.0.0 + readdirp: 4.1.1 + semver: 7.7.1 + source-map-support: 0.5.21 + strip-ansi-control-characters: 2.0.0 + tabtab: 3.0.2 + tempy: 3.1.0 + terminal-link: 3.0.0 + through2-filter: 4.0.0 + through2-map: 4.0.0 + toml: 3.0.0 + tomlify-j0.4: 3.0.0 + ulid: 2.3.0 + update-notifier: 7.3.1 + uuid: 11.0.5 + wait-port: 1.1.0 + write-file-atomic: 5.0.1 + ws: 8.18.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/opentelemetry-sdk-setup' + - '@planetscale/database' + - '@swc/core' + - '@swc/wasm' + - '@types/express' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bare-buffer + - bufferutil + - db0 + - encoding + - idb-keyval + - ioredis + - picomatch + - rollup + - supports-color + - uploadthing + - utf-8-validate + + netlify-redirector@0.5.0: {} - native-fetch@3.0.0(node-fetch@2.7.0(encoding@0.1.13)): + netlify@13.3.3: dependencies: - node-fetch: 2.7.0(encoding@0.1.13) - - natural-compare@1.4.0: {} + '@netlify/open-api': 2.36.0 + lodash-es: 4.17.21 + micro-api-client: 3.3.0 + node-fetch: 3.3.2 + omit.js: 2.0.2 + p-wait-for: 5.0.2 + qs: 6.14.0 next-tick@1.1.0: {} @@ -7904,8 +14029,14 @@ snapshots: dependencies: semver: 7.7.1 + node-addon-api@6.1.0: {} + + node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} + node-fetch-native@1.6.6: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -7918,24 +14049,76 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-forge@1.3.1: {} + + node-gyp-build@4.8.4: {} + + node-mock-http@1.0.0: {} + + node-source-walk@6.0.2: + dependencies: + '@babel/parser': 7.26.9 + node-sql-parser@3.9.4: dependencies: big-integer: 1.6.52 node-sqlite3-wasm@0.8.36: {} + node-stream-zip@1.15.0: {} + noop-x@1.2.1: {} + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.1 + semver: 7.7.1 + validate-npm-package-license: 3.0.4 + + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.7.1 + validate-npm-package-license: 3.0.4 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + normalize-path@3.0.0: {} + normalize-space-x@4.1.2: dependencies: simple-methodize-x: 1.0.4 trim-x: 4.1.2 white-space-x: 4.1.1 + normalize-url@8.0.1: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-create-x@3.1.2: @@ -7979,6 +14162,8 @@ snapshots: to-object-x: 2.2.1 to-property-key-x: 3.1.2 + object-inspect@1.13.4: {} + object-is@1.1.6: dependencies: call-bind: 1.0.7 @@ -8008,18 +14193,57 @@ snapshots: obliterator@1.6.1: {} + ofetch@1.4.1: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.6 + ufo: 1.5.4 + ohash@2.0.11: {} + omit.js@2.0.2: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + one-webcrypto@1.0.3: {} + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@10.1.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + is-wsl: 3.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8029,19 +14253,70 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@8.2.0: + dependencies: + chalk: 5.4.1 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + os-name@5.1.0: + dependencies: + macos-release: 3.3.0 + windows-release: 5.1.1 + + os-tmpdir@1.0.2: {} + + p-cancelable@3.0.0: {} + p-defer@3.0.0: {} p-defer@4.0.1: {} + p-event@4.2.0: + dependencies: + p-timeout: 3.2.0 + + p-event@5.0.1: + dependencies: + p-timeout: 5.1.0 + + p-event@6.0.1: + dependencies: + p-timeout: 6.1.2 + + p-every@2.0.0: + dependencies: + p-map: 2.1.0 + p-fifo@1.0.0: dependencies: fast-fifo: 1.3.2 p-defer: 3.0.0 + p-filter@3.0.0: + dependencies: + p-map: 5.5.0 + + p-filter@4.1.0: + dependencies: + p-map: 7.0.3 + + p-finally@1.0.0: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.0 + p-limit@6.2.0: dependencies: yocto-queue: 1.2.0 @@ -8050,6 +14325,16 @@ snapshots: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@2.1.0: {} + + p-map@5.5.0: + dependencies: + aggregate-error: 4.0.1 + p-map@7.0.3: {} p-queue@8.1.0: @@ -8057,6 +14342,13 @@ snapshots: eventemitter3: 5.0.1 p-timeout: 6.1.4 + p-reduce@3.0.0: {} + + p-retry@5.1.2: + dependencies: + '@types/retry': 0.12.1 + retry: 0.13.1 + p-retry@6.2.0: dependencies: '@types/retry': 0.12.2 @@ -8069,14 +14361,43 @@ snapshots: is-network-error: 1.1.0 retry: 0.13.1 + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@5.1.0: {} + + p-timeout@6.1.2: {} + p-timeout@6.1.4: {} + p-wait-for@5.0.2: + dependencies: + p-timeout: 6.1.2 + package-json-from-dist@1.0.1: {} + package-json@10.0.1: + dependencies: + ky: 1.7.5 + registry-auth-token: 5.1.0 + registry-url: 6.0.1 + semver: 7.7.1 + + parallel-transform@1.2.0: + dependencies: + cyclist: 1.0.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-github-url@1.0.3: {} + + parse-gitignore@2.0.0: {} + parse-int-x@3.2.2: dependencies: nan-x: 2.2.1 @@ -8084,6 +14405,23 @@ snapshots: to-string-x: 2.1.1 trim-left-x: 4.1.2 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-json@8.1.0: + dependencies: + '@babel/code-frame': 7.26.2 + index-to-position: 0.1.2 + type-fest: 4.26.1 + + parse-ms@3.0.0: {} + + parseurl@1.3.3: {} + partykit@0.0.111: dependencies: '@cloudflare/workers-types': 4.20240718.0 @@ -8109,44 +14447,106 @@ snapshots: path-exists@4.0.0: {} + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-key@4.0.0: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} + + path-type@5.0.0: {} + path@0.12.7: dependencies: process: 0.11.10 util: 0.10.4 + pathe@1.1.2: {} + pathe@2.0.3: {} pathval@2.0.0: {} + peek-readable@5.4.2: {} + + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.2: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.6.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.1 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pirates@4.0.6: {} + pkg-dir@7.0.0: + dependencies: + find-up: 6.3.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + possible-typed-array-names@1.0.0: {} - postcss-load-config@6.0.1(postcss@8.5.3)(tsx@4.19.3)(yaml@2.7.0): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.3)(yaml@2.7.0): dependencies: lilconfig: 3.1.3 optionalDependencies: - postcss: 8.5.3 + jiti: 2.4.2 + postcss: 8.5.1 tsx: 4.19.3 yaml: 2.7.0 + postcss-values-parser@6.0.2(postcss@8.5.1): + dependencies: + color-name: 1.1.4 + is-url-superb: 4.0.0 + postcss: 8.5.1 + quote-unquote: 1.0.0 + + postcss@8.5.1: + dependencies: + nanoid: 3.3.9 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.3: dependencies: nanoid: 3.3.9 @@ -8168,12 +14568,52 @@ snapshots: tar-fs: 2.1.2 tunnel-agent: 0.6.0 + precinct@11.0.5(supports-color@9.4.0): + dependencies: + '@dependents/detective-less': 4.1.0 + commander: 10.0.1 + detective-amd: 5.0.2 + detective-cjs: 5.0.1 + detective-es6: 4.0.1 + detective-postcss: 6.1.3 + detective-sass: 5.0.3 + detective-scss: 4.0.3 + detective-stylus: 4.0.0 + detective-typescript: 11.2.0(supports-color@9.4.0) + module-definition: 5.0.1 + node-source-walk: 6.0.2 + transitivePeerDependencies: + - supports-color + + precond@0.2.3: {} + prelude-ls@1.2.1: {} prettier@3.5.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-ms@8.0.0: + dependencies: + parse-ms: 3.0.0 + + prettyjson@1.2.5: + dependencies: + colors: 1.4.0 + minimist: 1.2.8 + printable-characters@1.0.42: {} + process-nextick-args@2.0.1: {} + + process-warning@3.0.0: {} + + process-warning@4.0.1: {} + process@0.11.10: {} progress-events@1.0.1: {} @@ -8191,6 +14631,8 @@ snapshots: to-object-x: 2.2.1 to-property-key-x: 3.1.2 + proto-list@1.2.4: {} + protobufjs@7.4.0: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -8212,8 +14654,20 @@ snapshots: uint8arraylist: 2.4.8 uint8arrays: 5.1.0 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + ps-list@8.1.1: {} + + pump@1.0.3: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -8223,12 +14677,50 @@ snapshots: punycode@2.3.1: {} + pupa@3.1.0: + dependencies: + escape-goat: 4.0.0 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + querystring@0.2.0: {} queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + + quick-lru@5.1.1: {} + + quote-unquote@1.0.0: {} + rabin-rs@2.1.0: {} + radix3@1.1.2: {} + + random-bytes@1.0.0: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -8236,6 +14728,8 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-is@17.0.2: {} + react-native-fetch-api@3.0.0: dependencies: p-defer: 3.0.0 @@ -8244,14 +14738,79 @@ snapshots: dependencies: loose-envify: 1.4.0 + read-package-up@11.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 9.0.1 + type-fest: 4.26.1 + + read-pkg@7.1.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 2.19.0 + + read-pkg@9.0.1: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 8.1.0 + type-fest: 4.26.1 + unicorn-magic: 0.1.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.1: {} + readdirp@4.1.2: {} + real-require@0.2.0: {} + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + remove-trailing-separator@1.1.0: {} + rename-function-x@1.1.2: dependencies: assert-is-function-x: 3.1.2 @@ -8262,6 +14821,8 @@ snapshots: to-boolean-x: 2.1.1 to-string-x: 2.1.1 + repeat-string@1.6.1: {} + replace-comments-x@3.1.2: dependencies: require-coercible-to-string-x: 2.1.1 @@ -8273,22 +14834,63 @@ snapshots: require-object-coercible-x: 2.1.1 to-string-x: 2.1.1 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} require-object-coercible-x@2.1.1: dependencies: is-nil-x: 2.1.1 + require-package-name@2.0.1: {} + + requires-port@1.0.0: {} + + resolve-alpn@1.2.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@3.0.0: + dependencies: + lowercase-keys: 3.0.0 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + ret@0.4.3: {} + retry@0.13.1: {} reusify@1.1.0: {} + rfdc@1.4.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + rollup-plugin-inject@3.0.2: dependencies: estree-walker: 0.6.1 @@ -8328,19 +14930,37 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.35.0 fsevents: 2.3.3 + run-applescript@7.0.0: {} + + run-async@2.4.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + rxjs@7.8.1: dependencies: tslib: 2.8.1 - sade@1.8.1: + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-json-stringify@1.2.0: {} + + safe-regex2@3.1.0: dependencies: - mri: 1.2.0 + ret: 0.4.3 - safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -8354,8 +14974,47 @@ snapshots: sax@1.2.1: {} + secure-json-parse@2.7.0: {} + + seek-bzip@1.0.6: + dependencies: + commander: 2.20.3 + + semver@6.3.1: {} + semver@7.7.1: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8365,6 +15024,21 @@ snapshots: gopd: 1.0.1 has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + + sharp@0.32.6: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + node-addon-api: 6.1.0 + prebuild-install: 7.1.2 + semver: 7.7.1 + simple-get: 4.0.1 + tar-fs: 3.0.8 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - bare-buffer + sharp@0.33.5: dependencies: color: 4.2.3 @@ -8398,8 +15072,38 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} simple-bind-x@1.0.3: @@ -8428,12 +15132,37 @@ snapshots: simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - optional: true + + slash@3.0.0: {} + + slash@4.0.0: {} + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 smol-toml@1.3.1: {} + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + sort-keys-length@1.0.1: + dependencies: + sort-keys: 1.1.2 + + sort-keys@1.1.2: + dependencies: + is-plain-obj: 1.1.0 + source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} source-map@0.8.0-beta.0: @@ -8444,29 +15173,77 @@ snapshots: sparse-array@1.3.2: {} + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + split-if-boxed-bug-x@2.1.2: dependencies: has-boxed-string-x: 2.1.1 is-string: 1.0.7 simple-methodize-x: 1.0.4 + split2@1.1.1: + dependencies: + through2: 2.0.5 + + split2@4.2.0: {} + sprintf-js@1.0.3: {} + stack-generator@2.0.10: + dependencies: + stackframe: 1.3.4 + + stack-trace@0.0.10: {} + stackback@0.0.2: {} + stackframe@1.3.4: {} + stacktracey@2.1.8: dependencies: as-table: 1.0.55 get-source: 2.0.12 + statuses@1.5.0: {} + + statuses@2.0.1: {} + + std-env@3.8.0: {} + std-env@3.8.1: {} + stdin-discarder@0.2.2: {} + stoppable@1.1.0: {} stream-to-it@0.2.4: dependencies: get-iterator: 1.0.2 + streamx@2.22.0: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.5.4 + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -8479,10 +15256,30 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-ansi-control-characters@2.0.0: {} + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8491,14 +15288,28 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-dirs@3.0.0: + dependencies: + inspect-with-kind: 1.0.5 + is-plain-obj: 1.1.0 + + strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} + strip-outer@2.0.0: {} + strnum@1.1.2: {} + strtok3@7.1.1: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.4.2 + stubborn-fs@1.2.5: {} sucrase@3.35.0: @@ -8511,10 +15322,33 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + supports-color@9.4.0: {} + + supports-hyperlinks@2.3.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + symbol-iterator-x@1.1.2: dependencies: has-symbol-support-x: 2.1.2 @@ -8530,6 +15364,17 @@ snapshots: system-architecture@0.1.0: {} + tabtab@3.0.2: + dependencies: + debug: 4.4.0(supports-color@9.4.0) + es6-promisify: 6.1.1 + inquirer: 6.5.2 + minimist: 1.2.8 + mkdirp: 0.5.6 + untildify: 3.0.3 + transitivePeerDependencies: + - supports-color + tar-fs@2.1.2: dependencies: chownr: 1.1.4 @@ -8537,6 +15382,16 @@ snapshots: pump: 3.0.2 tar-stream: 2.2.0 + tar-fs@3.0.8: + dependencies: + pump: 3.0.2 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.0.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -8545,6 +15400,41 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.22.0 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-dir@3.0.0: {} + + tempy@3.1.0: + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + + terminal-link@3.0.0: + dependencies: + ansi-escapes: 5.0.0 + supports-hyperlinks: 2.3.0 + + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + + text-hex@1.0.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -8553,6 +15443,29 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + through2-filter@4.0.0: + dependencies: + through2: 4.0.2 + + through2-map@4.0.0: + dependencies: + through2: 4.0.2 + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + through@2.3.8: {} + timers-ext@0.1.8: dependencies: es5-ext: 0.10.64 @@ -8573,6 +15486,16 @@ snapshots: tinyspy@3.0.2: {} + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.2.3: {} + to-boolean-x@2.1.1: {} to-integer-x@4.2.2: @@ -8634,6 +15557,19 @@ snapshots: dependencies: is-symbol: 1.0.4 + toad-cache@3.7.0: {} + + toidentifier@1.0.1: {} + + token-types@5.0.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + toml@3.0.0: {} + + tomlify-j0.4@3.0.0: {} + tr46@0.0.3: {} tr46@1.0.1: @@ -8648,6 +15584,10 @@ snapshots: simple-methodize-x: 1.0.4 white-space-x: 4.1.1 + trim-repeated@2.0.0: + dependencies: + escape-string-regexp: 5.0.0 + trim-right-x@4.1.2: dependencies: require-coercible-to-string-x: 2.1.1 @@ -8659,6 +15599,8 @@ snapshots: trim-left-x: 4.1.2 trim-right-x: 4.1.2 + triple-beam@1.4.1: {} + ts-api-utils@2.0.1(typescript@5.7.3): dependencies: typescript: 5.7.3 @@ -8669,25 +15611,45 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@22.13.10)(typescript@5.7.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.13.10 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.7.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsconfck@3.1.4(typescript@5.7.3): optionalDependencies: typescript: 5.7.3 + tslib@1.14.1: {} + tslib@2.7.0: {} tslib@2.8.1: {} - tsup@8.4.0(postcss@8.5.3)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.7.0): + tsup@8.4.0(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.7.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.1) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) esbuild: 0.25.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.3)(tsx@4.19.3)(yaml@2.7.0) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.3)(yaml@2.7.0) resolve-from: 5.0.0 rollup: 4.35.0 source-map: 0.8.0-beta.0 @@ -8696,7 +15658,7 @@ snapshots: tinyglobby: 0.2.12 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.3 + postcss: 8.5.1 typescript: 5.7.3 transitivePeerDependencies: - jiti @@ -8704,6 +15666,11 @@ snapshots: - tsx - yaml + tsutils@3.21.0(typescript@5.7.3): + dependencies: + tslib: 1.14.1 + typescript: 5.7.3 + tsx@4.19.3: dependencies: esbuild: 0.25.1 @@ -8719,20 +15686,29 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.21.3: {} + + type-fest@1.4.0: {} + type-fest@2.19.0: {} type-fest@4.26.1: {} type-fest@4.37.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type@2.7.3: {} - typescript-eslint@8.26.0(eslint@9.22.0)(typescript@5.7.3): + typescript-eslint@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3) - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.26.0(eslint@9.22.0)(typescript@5.7.3) - eslint: 9.22.0 + '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.26.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.22.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -8741,6 +15717,10 @@ snapshots: ufo@1.5.4: {} + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + uint8-varint@2.0.4: dependencies: uint8arraylist: 2.4.8 @@ -8758,6 +15738,15 @@ snapshots: dependencies: multiformats: 13.3.2 + ulid@2.3.0: {} + + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + + uncrypto@0.1.3: {} + undici-types@6.20.0: {} undici@5.28.4: @@ -8776,6 +15765,64 @@ snapshots: pathe: 2.0.3 ufo: 1.5.4 + unicorn-magic@0.1.0: {} + + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + universal-user-agent@7.0.2: {} + + unix-dgram@2.0.6: + dependencies: + bindings: 1.5.0 + nan: 2.22.2 + optional: true + + unixify@1.0.0: + dependencies: + normalize-path: 2.1.1 + + unpipe@1.0.0: {} + + unstorage@1.15.0(@netlify/blobs@8.1.1)(aws4fetch@1.0.20)(idb-keyval@6.2.1): + dependencies: + anymatch: 3.1.3 + chokidar: 4.0.3 + destr: 2.0.3 + h3: 1.15.1 + lru-cache: 10.4.3 + node-fetch-native: 1.6.6 + ofetch: 1.4.1 + ufo: 1.5.4 + optionalDependencies: + '@netlify/blobs': 8.1.1 + aws4fetch: 1.0.20 + idb-keyval: 6.2.1 + + untildify@3.0.3: {} + + untun@0.1.3: + dependencies: + citty: 0.1.6 + consola: 3.4.0 + pathe: 1.1.2 + + update-notifier@7.3.1: + dependencies: + boxen: 8.0.1 + chalk: 5.4.1 + configstore: 7.0.0 + is-in-ci: 1.0.0 + is-installed-globally: 1.0.0 + is-npm: 6.0.0 + latest-version: 9.0.0 + pupa: 3.1.0 + semver: 7.7.1 + xdg-basedir: 5.1.0 + + uqr@0.1.2: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8785,6 +15832,8 @@ snapshots: punycode: 1.3.2 querystring: 0.2.0 + urlpattern-polyfill@8.0.2: {} + util-deprecate@1.0.2: {} util-get-getter-x@1.0.2: @@ -8813,23 +15862,40 @@ snapshots: is-typed-array: 1.1.13 which-typed-array: 1.1.15 + utils-merge@1.0.1: {} + + uuid@11.0.5: {} + uuid@8.0.0: {} uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} + valibot@1.0.0-beta.7(typescript@5.7.3): optionalDependencies: typescript: 5.7.3 + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@4.0.0: + dependencies: + builtins: 5.1.0 + varint@6.0.0: {} - vite-node@3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0): + vary@1.1.2: {} + + vite-node@3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: cac: 6.7.14 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -8844,18 +15910,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.3) optionalDependencies: - vite: 6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite@6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0): + vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: esbuild: 0.25.1 postcss: 8.5.3 @@ -8863,22 +15929,23 @@ snapshots: optionalDependencies: '@types/node': 22.13.10 fsevents: 2.3.3 + jiti: 2.4.2 tsx: 4.19.3 yaml: 2.7.0 vitest-pool-workers@0.0.1: {} - vitest@3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0): + vitest@3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0)) + '@vitest/mocker': 3.0.8(vite@6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.8 '@vitest/runner': 3.0.8 '@vitest/snapshot': 3.0.8 '@vitest/spy': 3.0.8 '@vitest/utils': 3.0.8 chai: 5.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@9.4.0) expect-type: 1.2.0 magic-string: 0.30.17 pathe: 2.0.3 @@ -8887,8 +15954,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.1(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) - vite-node: 3.0.8(@types/node@22.13.10)(tsx@4.19.3)(yaml@2.7.0) + vite: 6.2.1(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) + vite-node: 3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.13.10 @@ -8916,6 +15983,14 @@ snapshots: transitivePeerDependencies: - debug + wait-port@1.1.0: + dependencies: + chalk: 4.1.2 + commander: 9.5.0 + debug: 4.4.0(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 @@ -8967,6 +16042,38 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + windows-release@5.1.1: + dependencies: + execa: 5.1.1 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + word-wrap@1.2.5: {} workerd@1.20240718.0: @@ -9017,12 +16124,25 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + ws@8.18.0: {} ws@8.18.1: {} + xdg-basedir@5.1.0: {} + xml2js@0.6.2: dependencies: sax: 1.2.1 @@ -9030,12 +16150,44 @@ snapshots: xmlbuilder@11.0.1: {} + xss@1.0.15: + dependencies: + commander: 2.20.3 + cssfilter: 0.0.10 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + yaml@2.7.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yn@3.1.1: {} + yocto-queue@0.1.0: {} yocto-queue@1.2.0: {} + yoctocolors@2.1.1: {} + yoga-wasm-web@0.3.3: {} youch@3.2.3: @@ -9050,6 +16202,12 @@ snapshots: mustache: 4.2.0 stacktracey: 2.1.8 + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@3.22.3: {} zod@3.24.2: {} diff --git a/setup.ucan.ts b/setup.ucan.ts index 638642cd..f7c04c4f 100644 --- a/setup.ucan.ts +++ b/setup.ucan.ts @@ -30,5 +30,7 @@ const uri = server.uri "MgCZc476L5pn6Kiw5YdLHEy5CHZgw5gRWxNj/UcLRQoxaHu0BREgGEsI7N8cQxjO6fdgA/lEAphNmR/um1DEfmBTBByY" ); +// console.log(">>>>>>>", uri.toString()); + process.env.FP_STORAGE_URL = uri.toString(); -process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-ucan?fs=mem&extractKey=_deprecated_internal_api"; +process.env.FP_KEYBAG_URL = "memory://kb-dir-ucan?extractKey=_deprecated_internal_api"; diff --git a/setup.cloud.ts b/setup.v2-cloud.ts similarity index 100% rename from setup.cloud.ts rename to setup.v2-cloud.ts diff --git a/src/aws/gateway.ts b/src/aws/gateway.ts index 94e170be..7c6bfadf 100644 --- a/src/aws/gateway.ts +++ b/src/aws/gateway.ts @@ -1,6 +1,7 @@ import { BuildURI, CoerceURI, exception2Result, KeyedResolvOnce, Logger, param, Result, URI } from "@adviser/cement"; import { bs, getStore, NotFoundError, SuperThis, ensureSuperLog } from "@fireproof/core"; import { AddKeyToDbMetaGateway } from "../meta-key-hack.js"; +import { to_uint8 } from "../coerce-binary.js"; async function resultFetch(logger: Logger, curl: CoerceURI, init?: RequestInit): Promise> { const url = URI.from(curl); diff --git a/src/coerce-binary.ts b/src/coerce-binary.ts index b76afc73..e9106db7 100644 --- a/src/coerce-binary.ts +++ b/src/coerce-binary.ts @@ -1,5 +1,5 @@ export async function top_uint8( - input: string | ArrayBuffer | ArrayBufferView | Uint8Array | SharedArrayBuffer | Blob + input: string | ArrayBufferLike | ArrayBufferView | Uint8Array | SharedArrayBuffer | Blob ): Promise { if (input instanceof Blob) { return new Uint8Array(await input.arrayBuffer()); @@ -7,7 +7,9 @@ export async function top_uint8( return to_uint8(input); } -export function to_uint8(input: string | ArrayBuffer | ArrayBufferView | Uint8Array | SharedArrayBuffer): Uint8Array { +export function to_uint8( + input: string | ArrayBufferLike | ArrayBufferView | Uint8Array | SharedArrayBuffer +): Uint8Array { if (typeof input === "string") { // eslint-disable-next-line no-restricted-globals return new TextEncoder().encode(input); @@ -20,19 +22,21 @@ export function to_uint8(input: string | ArrayBuffer | ArrayBufferView | Uint8Ar return input; } // not nice but we make the cloudflare types happy - return new Uint8Array(input as unknown as ArrayBuffer); + return new Uint8Array(input as unknown as ArrayBufferLike); } -export function to_blob(input: ArrayBuffer | ArrayBufferView | Uint8Array | Blob): Blob { +export function to_blob(input: ArrayBufferLike | ArrayBufferView | Uint8Array | Blob): Blob { if (input instanceof Blob) { return input; } return new Blob([to_uint8(input)]); } -export function to_arraybuf(input: ArrayBuffer | ArrayBufferView | Uint8Array): ArrayBuffer { +export function to_arraybuf(input: ArrayBufferLike | ArrayBufferView | Uint8Array): ArrayBuffer { if (input instanceof ArrayBuffer) { return input; } - return to_uint8(input).buffer as ArrayBuffer; + const u8 = to_uint8(input); + return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; + // return to_uint8(input).buffer; // as ArrayBuffer; } diff --git a/src/fp-cloud/backend/cf-dobj-abstract-sql.ts b/src/fp-cloud/backend/cf-dobj-abstract-sql.ts deleted file mode 100644 index 37e44ab6..00000000 --- a/src/fp-cloud/backend/cf-dobj-abstract-sql.ts +++ /dev/null @@ -1,31 +0,0 @@ -// import { DurableObject } from "cloudflare:workers"; -import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "../meta-merger/abstract-sql.js"; -// import { Env } from "./env.js"; -import { ExecSQLResult, FPBackendDurableObject } from "./server.js"; - -export class CFDObjSQLStatement implements SQLStatement { - readonly sql: string; - readonly db: CFDObjSQLDatabase; - constructor(db: CFDObjSQLDatabase, sql: string) { - this.db = db; - this.sql = sql; - } - async run(...params: SQLParams): Promise { - const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; - return res.rawResults[0] as T; - } - async all(...params: SQLParams): Promise { - const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; - return res.rawResults as T[]; - } -} - -export class CFDObjSQLDatabase implements SQLDatabase { - readonly dobj: DurableObjectStub; - constructor(dobj: DurableObjectStub) { - this.dobj = dobj; - } - prepare(sql: string): SQLStatement { - return new CFDObjSQLStatement(this, sql); - } -} diff --git a/src/fp-cloud/backend/cf-hono-server.ts b/src/fp-cloud/backend/cf-hono-server.ts deleted file mode 100644 index c190a7bb..00000000 --- a/src/fp-cloud/backend/cf-hono-server.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { HttpHeader, KeyedResolvOnce, Logger, LoggerImpl, URI } from "@adviser/cement"; -import { Context, Hono } from "hono"; -import { ConnMiddleware, HonoServerFactory, RunTimeParams, HonoServerBase } from "../hono-server.js"; -import { WSContext, WSContextInit, WSEvents } from "hono/ws"; -import { buildErrorMsg, defaultGestalt, EnDeCoder, Gestalt } from "../msg-types.js"; -// import { RequestInfo as CFRequestInfo } from "@cloudflare/workers-types"; -import { defaultMsgParams, jsonEnDe } from "../msger.js"; -import { ensureLogger, ensureSuperThis, SuperThis } from "@fireproof/core"; -import { SQLDatabase } from "../meta-merger/abstract-sql.js"; -import { CFWorkerSQLDatabase } from "../meta-merger/cf-worker-abstract-sql.js"; -import { CFDObjSQLDatabase } from "./cf-dobj-abstract-sql.js"; -import { Env } from "./env.js"; -import { WSRoom } from "../ws-room.js"; -import { FPBackendDurableObject, FPRoomDurableObject } from "./server.js"; - -const startedChs = new KeyedResolvOnce(); - -export function getBackendDurableObject(env: Env) { - // console.log("getDurableObject", env); - const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; - const rany = env as unknown as Record>; - const dObjNs = rany[cfBackendKey]; - const id = dObjNs.idFromName(env.FP_BACKEND_DO_ID ?? cfBackendKey); - return dObjNs.get(id); -} - -export function getRoomDurableObject(env: Env) { - // console.log("getDurableObject", env); - const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_WS_ROOM"; - const rany = env as unknown as Record>; - const dObjNs = rany[cfBackendKey]; - const id = dObjNs.idFromName(cfBackendKey); - return dObjNs.get(id); -} - -class CFWSRoom implements WSRoom { - readonly dobj: DurableObjectStub; - constructor(dobj: DurableObjectStub) { - this.dobj = dobj; - } - async acceptConnection(ws: WebSocket, wse: WSEvents): Promise { - const ret = await this.dobj.acceptWebSocket(ws, wse); - const wsCtx = new WSContext(ws as WSContextInit); - wse.onOpen?.({} as Event, wsCtx); - // return Promise.resolve(); - // ws.accept(); - return ret; - } -} - -export class CFHonoFactory implements HonoServerFactory { - readonly _onClose: () => void; - constructor( - onClose: () => void = () => { - /* */ - } - ) { - this._onClose = onClose; - } - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { - // this._env = c.env - const sthis = ensureSuperThis({ - logger: new LoggerImpl(), - }); - sthis.env.sets(c.env); - const logger = ensureLogger(sthis, `CFHono[${URI.from(c.req.url).pathname}]`); - const ende = jsonEnDe(sthis); - // this.sthis.env. - const fpProtocol = sthis.env.get("FP_PROTOCOL"); - const msgP = defaultMsgParams(sthis, { - hasPersistent: true, - protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], - }); - const gs = defaultGestalt(msgP, { - id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", - }); - - const wsRoom = new CFWSRoom(c.env); - const cfBackendMode = c.env.CF_BACKEND_MODE && c.env.CF_BACKEND_MODE === "DURABLE_OBJECT" ? "DURABLE_OBJECT" : "D1"; - let db: SQLDatabase; - switch (cfBackendMode) { - case "DURABLE_OBJECT": { - db = new CFDObjSQLDatabase(getBackendDurableObject(c.env)); - const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); - // TODO WE NEED TO START THE DURABLE OBJECT - // but then on every request we import the schema - return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs })); - } - // break; - case "D1": - default: { - const cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"; - return startedChs - .get(cfBackendKey) - .once(async () => { - db = new CFWorkerSQLDatabase(c.env[cfBackendKey] as D1Database); - const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); - await chs.start(); - return chs; - }) - .then((chs) => fn({ sthis, logger, ende, impl: chs })); - } - // break; - } - // return ret; // .then((v) => sthis.logger.Flush().then(() => v)) - } - - async start(_app: Hono): Promise { - // const { upgradeWebSocket } = await import("hono/cloudflare-workers"); - // this._upgradeWebSocket = upgradeWebSocket; - } - - async serve(_app: Hono, _port?: number): Promise { - return {} as T; - } - async close(): Promise { - this._onClose(); - return; - } -} - -export class CFHonoServer extends HonoServerBase { - // _upgradeWebSocket?: UpgradeWebSocket - - readonly ende: EnDeCoder; - // readonly env: Env; - // readonly wsConnections = new Map() - constructor( - sthis: SuperThis, - logger: Logger, - ende: EnDeCoder, - gs: Gestalt, - sqlDb: SQLDatabase, - wsRoom: WSRoom, - headers?: HttpHeader - ) { - super(sthis, logger, gs, sqlDb, wsRoom, headers); - this.ende = ende; - // this.env = env; - } - - // getDurableObject(conn: Connection) { - // const id = env.FP_META_GROUPS.idFromName("fireproof"); - // const stub = env.FP_META_GROUPS.get(id); - // } - - upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { - // if (!this._upgradeWebSocket) { - // throw new Error("upgradeWebSocket not implemented"); - // } - return async (conn, c, _next) => { - const upgradeHeader = c.req.header("Upgrade"); - if (!upgradeHeader || upgradeHeader !== "websocket") { - return new Response( - this.ende.encode(buildErrorMsg(this.sthis, this.logger, {}, new Error("expected Upgrade: websocket"))), - { status: 426 } - ); - } - // const env = c.env as Env; - // const id = env.FP_META_GROUPS.idFromName([conn.key.tenant, conn.key.ledger].join(":")); - // const dObj = env.FP_META_GROUPS.get(id); - // c.env.WS_EVENTS = createEvents(c); - // return dObj.fetch(c.req.raw as unknown as CFRequestInfo) as unknown as Promise; - // this._upgradeWebSocket!(createEvents)(c, next); - - const { 0: client, 1: server } = new WebSocketPair(); - conn.attachWSPair({ client, server }); - - const wsEvents = await createEvents(c); - // console.log("upgradeWebSocket", c.req.url); - - // const wsCtx = new WSContext(server as WSContextInit); - - // server.onopen = (ev) => { - // console.log("onopen", ev); - // wsEvents.onOpen?.(ev, wsCtx); - // } - - await this.wsRoom.acceptConnection(server, wsEvents); - - // server.send("Hello from server"); - - // this.wsConnections.set(this.sthis.nextId().str, { client, server }); - // const client = webSocketPair[0], - // server = webSocketPair[1]; - - return new Response(null, { - status: 101, - webSocket: client, - }); - }; - } -} diff --git a/src/fp-cloud/backend/env.d.ts b/src/fp-cloud/backend/env.d.ts deleted file mode 100644 index a3f757e1..00000000 --- a/src/fp-cloud/backend/env.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Generated by Wrangler on Fri Aug 16 2024 13:55:06 GMT+0200 (Central European Summer Time) -// by running `wrangler types` - -import type { DurableObjectNamespace } from "@cloudflare/workers-types"; -// import { WSEvents } from "hono/ws"; -import { FPRoomDurableObject, FPBackendDurableObject } from "./server.ts"; - -export interface Env { - // bucket: R2Bucket; - // kv_store: KVNamespace; - - /** AWS/S3 access key ID for storage backend */ - ACCESS_KEY_ID: string; - ACCOUNT_ID: string; - BUCKET_NAME: string; - CLOUDFLARE_API_TOKEN: string; - EMAIL: string; - FIREPROOF_SERVICE_PRIVATE_KEY: string; - POSTMARK_TOKEN: string; - SECRET_ACCESS_KEY: string; - SERVICE_ID: string; - STORAGE_URL: string; - REGION: string; - VERSION: string; - FP_DEBUG: string; - FP_STACK: string; - FP_FORMAT: string; - FP_PROTOCOL: string; - /** Test date in ISO8601 format (YYYYMMDD'T'HHmmss'Z'). Optional. */ - TEST_DATE?: string; - /** Maximum idle time in seconds before connection timeout. Optional. */ - MAX_IDLE_TIME?: string; - - // default D1 - CF_BACKEND_MODE: "D1" | "DURABLE_OBJECT"; - // default D1 "FP_BACKEND_D1" - // default DURABLE_OBJECT "FP_BACKEND_DO" - CF_BACKEND_KEY?: string; - - FP_BACKEND_D1: D1Database; - - FP_BACKEND_DO: DurableObjectNamespace; - // default CF_BACKEND_KEY - FP_BACKEND_DO_ID: string; - - // default "FP_WS_ROOM" - CF_WS_ROOM_KEY: string; - - FP_WS_ROOM: DurableObjectNamespace; - - // WS_EVENTS: WSEvents; -} - -// declare module "cloudflare:test" { -// // ...or if you have an existing `Env` type... -// interface ProvidedEnv extends Env { -// readonly test: boolean; -// } -// } diff --git a/src/fp-cloud/backend/fp-meta-groups.ts-off b/src/fp-cloud/backend/fp-meta-groups.ts-off deleted file mode 100644 index d18d3aae..00000000 --- a/src/fp-cloud/backend/fp-meta-groups.ts-off +++ /dev/null @@ -1,47 +0,0 @@ -import { DurableObject } from "cloudflare:workers"; -import { Env } from "./env.js"; - -export class FPMetaGroups extends DurableObject { - currentlyConnectedWebSockets: number; - - constructor(ctx: DurableObjectState, env: Env) { - // This is reset whenever the constructor runs because - // regular WebSockets do not survive Durable Object resets. - // - // WebSockets accepted via the Hibernation API can survive - // a certain type of eviction, but we will not cover that here. - super(ctx, env); - this.currentlyConnectedWebSockets = 0; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async fetch(request: Request): Promise { - // Creates two ends of a WebSocket connection. - const webSocketPair = new WebSocketPair(); - const [client, server] = Object.values(webSocketPair); - - // Calling `accept()` tells the runtime that this WebSocket is to begin terminating - // request within the Durable Object. It has the effect of "accepting" the connection, - // and allowing the WebSocket to send and receive messages. - server.accept(); - this.currentlyConnectedWebSockets += 1; - - // Upon receiving a message from the client, the server replies with the same message, - // and the total number of connections with the "[Durable Object]: " prefix - // eslint-disable-next-line @typescript-eslint/no-unused-vars - server.addEventListener("message", (event: MessageEvent) => { - server.send(`[Durable Object] currentlyConnectedWebSockets: ${this.currentlyConnectedWebSockets}`); - }); - - // If the client closes the connection, the runtime will close the connection too. - server.addEventListener("close", (cls: CloseEvent) => { - this.currentlyConnectedWebSockets -= 1; - server.close(cls.code, "Durable Object is closing WebSocket"); - }); - - return new Response(null, { - status: 101, - webSocket: client, - }); - } -} diff --git a/src/fp-cloud/backend/server.ts b/src/fp-cloud/backend/server.ts deleted file mode 100644 index c0f100b6..00000000 --- a/src/fp-cloud/backend/server.ts +++ /dev/null @@ -1,72 +0,0 @@ -// / -// import { Logger } from "@adviser/cement"; -// import { Hono } from "hono"; -import { DurableObject } from "cloudflare:workers"; -import { HonoServer } from "../hono-server.js"; -import { Hono } from "hono"; -import { Env } from "./env.js"; -import { CFHonoFactory } from "./cf-hono-server.js"; -import { WSContext, WSContextInit, WSEvents } from "hono/ws"; - -const app = new Hono(); -const honoServer = new HonoServer(new CFHonoFactory()); - -export default { - fetch: async (req, env, ctx): Promise => { - await honoServer.register(app); - return app.fetch(req, env, ctx); - }, -} satisfies ExportedHandler; -/* - async fetch(req, env, _ctx): Promise { - const id = env.FP_META_GROUPS.idFromName("fireproof"); - const stub = env.FP_META_GROUPS.get(id); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return stub.fetch(req as any) as unknown as Promise; - }, -} satisfies ExportedHandler; -*/ - -export interface ExecSQLResult { - readonly rowsRead: number; - readonly rowsWritten: number; - readonly rawResults: unknown[]; -} - -export class FPBackendDurableObject extends DurableObject { - async execSql(sql: string, params: unknown[]): Promise { - const cursor = await this.ctx.storage.sql.exec(sql, ...params); - const rawResults = cursor.toArray(); - const res = { - rowsRead: cursor.rowsRead, - rowsWritten: cursor.rowsWritten, - rawResults, - }; - // console.log("execSql", sql, params, res); - return res; - } -} - -export class FPRoomDurableObject extends DurableObject { - private wsEvents?: WSEvents; - - async acceptWebSocket(ws: WebSocket, wsEvents: WSEvents): Promise { - this.ctx.acceptWebSocket(ws); - this.wsEvents = wsEvents; - } - - webSocketError(ws: WebSocket, error: unknown): void | Promise { - const wsCtx = new WSContext(ws as WSContextInit); - this.wsEvents?.onError?.(error as Event, wsCtx); - } - - async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer): Promise { - const wsCtx = new WSContext(ws as WSContextInit); - this.wsEvents?.onMessage?.({ data: msg } as MessageEvent, wsCtx); - } - - webSocketClose(ws: WebSocket, code: number, reason: string): void | Promise { - const wsCtx = new WSContext(ws as WSContextInit); - this.wsEvents?.onClose?.({ code, reason } as CloseEvent, wsCtx); - } -} diff --git a/src/fp-cloud/backend/wrangler.toml b/src/fp-cloud/backend/wrangler.toml deleted file mode 100644 index dfdee3a2..00000000 --- a/src/fp-cloud/backend/wrangler.toml +++ /dev/null @@ -1,143 +0,0 @@ -name = "fireproof-cloud" -main = "server.ts" -compatibility_date = "2024-04-19" -compatibility_flags = ["nodejs_compat"] -# upload_source_maps = true - -# [durable_objects] -# bindings = [ -# { name = "FP_DO", class_name = "FPDurableObject"}, -# ] - -# [[d1_databases]] -# binding = "DB" -# database_name = "test-meta-merge" -# database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" - -[durable_objects] -bindings = [ - # { name = "FP_DO", class_name = "FPDurableObject", script_name = "cf-dobj-abstract-sql" } - { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, - { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } -] - -[[migrations]] -tag = "v1" # Should be unique for each entry -new_sqlite_classes = ["FPBackendDurableObject"] - -[observability] -enabled = true -head_sampling_rate = 1 - - -[env.test.vars] -VERSION = "FP-MSG-1.0" -STORAGE_URL = "http://localhost:9000/testbucket" -ACCESS_KEY_ID = "minioadmin" -SECRET_ACCESS_KEY = "minioadmin" -FP_DEBUG = "FPMetaGroups" -#FP_FORMAT = "yaml" -# TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "http" -CF_BACKEND_MODE = "DURABLE_OBJECT" - -# [env.test.services] -# bindings = [ -# { binding = "FP_DO", service = "FP_DO" } -# ] - -[[env.test.d1_databases]] -binding = "FP_BACKEND_D1" -database_name = "test-meta-merge" -database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" - -[[env.test.migrations]] -tag = "v1" # Should be unique for each entry -new_sqlite_classes = ["FPBackendDurableObject"] - -[env.test.durable_objects] -bindings = [ - { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, - { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } -] - - -[env.test-reqRes-D1.vars] -VERSION = "FP-MSG-1.0" -STORAGE_URL = "http://localhost:9000/testbucket" -ACCESS_KEY_ID = "minioadmin" -SECRET_ACCESS_KEY = "minioadmin" -FP_DEBUG = "FPMetaGroups" -#FP_FORMAT = "yaml" -# TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "http" - -[[env.test-reqRes-D1.d1_databases]] -binding = "FP_BACKEND_D1" -database_name = "test-meta-merge" -database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" - -[env.test-reqRes-D1.durable_objects] -bindings = [ - { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, - { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } -] - -[env.test-reqRes-DO.vars] -VERSION = "FP-MSG-1.0" -STORAGE_URL = "http://localhost:9000/testbucket" -ACCESS_KEY_ID = "minioadmin" -SECRET_ACCESS_KEY = "minioadmin" -FP_DEBUG = "FPMetaGroups" -#FP_FORMAT = "yaml" -# TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "http" -CF_BACKEND_MODE = "DURABLE_OBJECT" - -[env.test-reqRes-DO.durable_objects] -bindings = [ - { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, - { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } -] - -[env.test-stream-D1.vars] -VERSION = "FP-MSG-1.0" -STORAGE_URL = "http://localhost:9000/testbucket" -ACCESS_KEY_ID = "minioadmin" -SECRET_ACCESS_KEY = "minioadmin" -FP_DEBUG = "FPMetaGroups" -#FP_FORMAT = "yaml" -# TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "ws" - - -[env.test-stream-D1.durable_objects] -bindings = [ - { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, - { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } -] - -[[env.test-stream-D1.d1_databases]] -binding = "FP_BACKEND_D1" -database_name = "test-meta-merge" -database_id = "b8b452ed-b1d9-478f-b56c-c5e4545f157a" - - -[env.test-stream-DO.vars] -VERSION = "FP-MSG-1.0" -STORAGE_URL = "http://localhost:9000/testbucket" -ACCESS_KEY_ID = "minioadmin" -SECRET_ACCESS_KEY = "minioadmin" -FP_DEBUG = "FPMetaGroups" -#FP_FORMAT = "yaml" -# TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "ws" -CF_BACKEND_MODE = "DURABLE_OBJECT" - -[env.test-stream-DO.durable_objects] -bindings = [ - { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, - { name = "FP_WS_ROOM", class_name = "FPRoomDurableObject" } -] - - diff --git a/src/fp-cloud/client/README.md b/src/fp-cloud/client/README.md deleted file mode 100644 index ec73ad43..00000000 --- a/src/fp-cloud/client/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Fireproof Cloud - -This gateway intended for use with Fireproof Cloud. - -## Usage - -You can call the `connect` function with a database and it will provision a remote UUID for the database, and sync the database to the remote. It will also log a URL to the console that you can open in a browser to connect to the database, as well as try to open the URL in a new tab. Tell us what you think about this workflow! - -```typescript -import { fireproof } from "@fireproof/core"; -import { connect } from "@fireproof/cloud"; - -const database = await fireproof("my-db-name"); -const connection = await connect(database); -``` - -### With React Hooks - -In a React component, you can use the `useFireproof` hook to get the database and then call `connect` (it is safe to call `connect` multiple times, but in this example we're using a state variable to store the dashboard URL). - -```typescript -import { useFireproof } from "use-fireproof"; -import { connect } from "@fireproof/cloud"; - -const { database } = useFireproof("my-db-name"); -const [dashboardUrl, setDashboardUrl] = useState(); - -// there is a useConnection hook coming soon -useEffect(() => { - connect(database).then((connection) => { - setDashboardUrl(connection.dashboardUrl?.toString()); - }); -}, [database]); -``` - -## The Second Argument - -The second argument to `connect` is the remote database name. This will be assigned for you if you don't provide one, and the created name will be persisted locally. - -The most common way to use this is if you want to sync to a remote database. The UUID will have been assigned when on first sync, and now you want to connect a new client to that remote. - -```typescript -const connection = await connect(database, "my-remote-uuid"); -``` - -If you provide a name, it will be used as the remote database name. If you want to control the name, you should use a prefix unique to your app, so no one else uses your endpoint. This is useful if you want the database name to come from your URL slug, like `/my-app/my-db-name`. - -```typescript -const connection = await connect(database, `com.my-app.v1.${database.name}`); -``` - -Note: if your database already has data in it, connecting to a new remote will do nothing. To prevent data lost, you need to rename the local database to an unused name and the connect. - -## No Warranty, For Evaluation Purposes - -This preview of Fireproof Cloud doesn't even have login, so don't expect your data to be persisted, etc. Please give us feedback on the workflow! We'll be adding login and access control soon. - -The source of truth on this stuff is the team. Join us on [Discord](https://discord.gg/cCryrNHePH) if you want to chat! diff --git a/src/fp-cloud/client/cli-pre-signed-url.ts b/src/fp-cloud/client/cli-pre-signed-url.ts deleted file mode 100644 index 8419d631..00000000 --- a/src/fp-cloud/client/cli-pre-signed-url.ts +++ /dev/null @@ -1,119 +0,0 @@ -// small tool to generate pre-signed url for cloud storage -// curl $(npx tsx src/cloud/client/cli-pre-signed-url.ts GET) -// curl -X PUT --data-binary @/etc/protocols $(npx tsx src/cloud/client/cli-pre-signed-url.ts) -import { BuildURI } from "@adviser/cement"; -import { AwsClient } from "aws4fetch"; -import dotenv from "dotenv"; -import { command, run, option, oneOf, string } from "cmd-ts"; -import { ensureSuperThis } from "@fireproof/core"; -// import * as t from 'io-ts'; - -(async () => { - dotenv.config(); - const sthis = ensureSuperThis(); - const cmd = command({ - name: "cli-pre-signed-url", - description: "sign a url for cloud storage", - version: "1.0.0", - args: { - method: option({ - long: "method", - type: oneOf(["GET", "PUT", "POST", "DELETE"]), - defaultValue: () => "PUT", - defaultValueIsSerializable: true, - }), - accessKeyId: option({ - long: "accessKeyId", - type: string, - defaultValue: () => sthis.env.get("CF_ACCESS_KEY_ID") || "accessKeyId", - defaultValueIsSerializable: true, - }), - secretAccessKey: option({ - long: "secretAccessKey", - type: string, - defaultValue: () => sthis.env.get("CF_SECRET_ACCESS_KEY") || "secretAccessKey", - defaultValueIsSerializable: true, - }), - region: option({ - long: "region", - type: string, - defaultValue: () => "us-east-1", - defaultValueIsSerializable: true, - }), - service: option({ - long: "service", - type: string, - defaultValue: () => "s3", - defaultValueIsSerializable: true, - }), - storageURL: option({ - long: "storageURL", - type: string, - defaultValue: () => sthis.env.get("CF_STORAGE_URL") || "https://bucket.example.com/db/main", - defaultValueIsSerializable: true, - }), - path: option({ - long: "path", - type: string, - defaultValue: () => "db/main", - defaultValueIsSerializable: true, - }), - expires: option({ - long: "expires", - type: string, - defaultValue: () => "3600", - defaultValueIsSerializable: true, - }), - now: option({ - long: "now", - type: { - async from(str): Promise { - const decoded = new Date(str); - if (isNaN(decoded.getTime())) { - throw new Error("invalid date"); - } - // 2021-09-01T12:34:56Z - return decoded - .toISOString() - .replace(/[-:]/g, "") - .replace(/\.\d+Z$/, "Z"); - }, - displayName: "WithoutMillis", - description: "without milliseconds", - }, - // 2021-09-01T12:34:56Z - // 2024-11-17T07:21:10.958Z - defaultValue: () => - new Date() - .toISOString() - .replace(/[-:]/g, "") - .replace(/\.\d+Z$/, "Z"), - defaultValueIsSerializable: true, - }), - }, - handler: async (args) => { - const a4f = new AwsClient({ - accessKeyId: args.accessKeyId, - secretAccessKey: args.secretAccessKey, - region: args.region, - service: args.service, - }); - const buildUrl = BuildURI.from(args.storageURL).appendRelative(args.path).setParam("X-Amz-Expires", args.expires); - - // eslint-disable-next-line no-console - console.log( - await a4f - .sign(new Request(buildUrl.toString(), { method: args.method }), { - aws: { - signQuery: true, - datetime: args.now, - }, - }) - .then((res) => res.url) - ); - }, - }); - - await run(cmd, process.argv.slice(2)); - // eslint-disable-next-line no-console -})().catch(console.error); diff --git a/src/fp-cloud/client/cloud-gateway.test.ts b/src/fp-cloud/client/cloud-gateway.test.ts deleted file mode 100644 index b30f07fe..00000000 --- a/src/fp-cloud/client/cloud-gateway.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Hono } from "hono"; -import { HonoServer } from "../hono-server.js"; -import { defaultGestalt } from "../msg-types.js"; -import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle } from "../test-helper.js"; -import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; -import { defaultMsgParams } from "../msger.js"; -import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; -import { BuildURI } from "@adviser/cement"; - -const sthis = ensureSuperThis(); -const msgP = defaultMsgParams(sthis, { hasPersistent: true }); -const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - -describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gateway", ({ factory }) => { - const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); - const style = wsStyle(sthis, port, msgP, my); - - let server: HonoServer; - let gw: bs.Gateway; - let unregister: () => void; - let url: BuildURI; - beforeAll(async () => { - const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.register(app, port)); - unregister = registerFireproofCloudStoreProtocol("fireproof:"); - gw = new FireproofCloudGateway(sthis); - url = BuildURI.from(`fireproof://localhost:${port}/`) - .setParam("protocol", "http") - .setParam("name", "ledger-name") - .setParam("tenant", "tendant"); - }); - afterAll(async () => { - await server.close(); - unregister(); - }); - describe("data", () => { - it("get not found", async () => { - await Promise.all( - Array(20) - .fill(async () => { - url.setParam("store", "data"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); - const res = await gw.get(kurl); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - - it("put - get - del - get", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const resStart = await gw.start(url.URI()); - expect(resStart.isOk()).toBeTruthy(); - - url.setParam("store", "data"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); - - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl); - expect(resDel.isOk()).toBeTruthy(); - - const res = await gw.get(kurl); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - }); - - describe("WAL", () => { - it("get not found", async () => { - await Promise.all( - Array(20) - .fill(async () => { - url.setParam("store", "wal"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); - const res = await gw.get(kurl); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - - it("put - get - del - get", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const resStart = await gw.start(url.URI()); - expect(resStart.isOk()).toBeTruthy(); - - url.setParam("store", "wal"); - const key = `theWALKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); - - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl); - expect(resDel.isOk()).toBeTruthy(); - - const res = await gw.get(kurl); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - }); -}); diff --git a/src/fp-cloud/client/gateway.ts b/src/fp-cloud/client/gateway.ts deleted file mode 100644 index a221fcef..00000000 --- a/src/fp-cloud/client/gateway.ts +++ /dev/null @@ -1,605 +0,0 @@ -// import PartySocket, { PartySocketOptions } from "partysocket"; -import { Result, URI, KeyedResolvOnce, exception2Result, key } from "@adviser/cement"; -import { bs, ensureLogger, Logger, NotFoundError, rt, SuperThis } from "@fireproof/core"; -import { - buildErrorMsg, - buildReqOpen, - FPStoreTypes, - HttpMethods, - MsgBase, - MsgIsError, - ReqSignedUrl, - MsgWithError, - ResSignedUrl, -} from "../msg-types.js"; -import { to_uint8 } from "../../coerce-binary.js"; -import { MsgConnected, Msger } from "../msger.js"; -import { MsgIsResGetData, MsgIsResPutData, ResDelData, ResGetData, ResPutData } from "../msg-types-data.js"; - -const VERSION = "v0.1-fp-cloud"; - -export interface StoreTypeGateway { - get(uri: URI, conn: Promise>): Promise>; - put(uri: URI, body: Uint8Array, conn: Promise>): Promise>; - delete(uri: URI, conn: Promise>): Promise>; -} - -abstract class BaseGateway { - readonly logger: Logger; - readonly sthis: SuperThis; - constructor(sthis: SuperThis, module: string) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, module); - } - - abstract getConn(uri: URI, conn: MsgConnected): Promise>; - async get(uri: URI, prConn: Promise>): Promise> { - const rConn = await prConn; - if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in getConn").ResultError(); - } - const conn = rConn.Ok(); - // this.logger.Debug().Any("conn", conn.key).Msg("get"); - return this.getConn(uri, conn); - } - - abstract putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise>; - async put(uri: URI, body: Uint8Array, prConn: Promise>): Promise> { - const rConn = await prConn; - if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); - } - const conn = rConn.Ok(); - // this.logger.Debug().Any("conn", conn.key).Msg("put"); - return this.putConn(uri, body, conn); - } - - abstract delConn(uri: URI, conn: MsgConnected): Promise>; - async delete(uri: URI, prConn: Promise>): Promise> { - const rConn = await prConn; - if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); - } - const conn = rConn.Ok(); - // this.logger.Debug().Any("conn", conn.key).Msg("del"); - return this.delConn(uri, conn); - } - - // prepareReqSignedUrl(type: string, method: HttpMethods, store: FPStoreTypes, uri: URI, conn: Connection): Result { - - // const sig = { - // conn, - // params: { - // method, - // store, - // key: uri.getParam(" - // } satisfies ReqSignedUrl; - // return Result.Ok(buildReqSignedUrl(this.sthis, type, sig, conn)) - // } - - async getResSignedUrl( - type: string, - method: HttpMethods, - store: FPStoreTypes, - waitForFn: (msg: MsgBase) => boolean, - uri: URI, - conn: MsgConnected - ): Promise> { - const rParams = uri.getParamsResult({ - key: key.REQUIRED, - store: key.REQUIRED, - path: key.OPTIONAL, - tenant: key.REQUIRED, - name: key.REQUIRED, - index: key.OPTIONAL, - }); - if (rParams.isErr()) { - return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, rParams.Err()); - } - const params = rParams.Ok(); - if (store !== params.store) { - return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, new Error("store mismatch")); - } - const rsu = { - tid: this.sthis.nextId().str, - type, - // conn: conn.conn, - tenant: { - tenant: params.tenant, - ledger: params.name, - }, - // tenant: conn.tenant, - params: { - method, - store, - ...params, - key: params.key, - }, - version: VERSION, - } as ReqSignedUrl; - return conn.request(rsu, { waitFor: waitForFn }); - } - - async putObject(uri: URI, uploadUrl: string, body: Uint8Array): Promise> { - this.logger.Debug().Any("url", { uploadUrl, uri }).Msg("put-fetch-url"); - const rUpload = await exception2Result(async () => fetch(uploadUrl, { method: "PUT", body })); - if (rUpload.isErr()) { - return this.logger.Error().Url(uploadUrl, "uploadUrl").Err(rUpload).Msg("Error in put fetch").ResultError(); - } - if (!rUpload.Ok().ok) { - return this.logger.Error().Url(uploadUrl, "uploadUrl").Http(rUpload.Ok()).Msg("Error in put fetch").ResultError(); - } - if (uri.getParam("testMode")) { - trackPuts.add(uri.toString()); - } - return Result.Ok(undefined); - } - - async getObject(uri: URI, downloadUrl: string): Promise> { - this.logger.Debug().Any("url", { downloadUrl, uri }).Msg("get-fetch-url"); - const rDownload = await exception2Result(async () => fetch(downloadUrl.toString(), { method: "GET" })); - if (rDownload.isErr()) { - return this.logger - .Error() - .Url(downloadUrl, "uploadUrl") - .Err(rDownload) - .Msg("Error in get downloadUrl") - .ResultError(); - } - const download = rDownload.Ok(); - if (!download.ok) { - if (download.status === 404) { - return Result.Err(new NotFoundError("Not found")); - } - return this.logger.Error().Url(downloadUrl, "uploadUrl").Err(rDownload).Msg("Error in get fetch").ResultError(); - } - return Result.Ok(to_uint8(await download.arrayBuffer())); - } - - async delObject(uri: URI, deleteUrl: string): Promise> { - this.logger.Debug().Any("url", { deleteUrl, uri }).Msg("get-fetch-url"); - const rDelete = await exception2Result(async () => fetch(deleteUrl.toString(), { method: "DELETE" })); - if (rDelete.isErr()) { - return this.logger.Error().Url(deleteUrl, "deleteUrl").Err(rDelete).Msg("Error in get deleteURL").ResultError(); - } - const download = rDelete.Ok(); - if (!download.ok) { - if (download.status === 404) { - return Result.Err(new NotFoundError("Not found")); - } - return this.logger.Error().Url(deleteUrl, "deleteUrl").Err(rDelete).Msg("Error in del fetch").ResultError(); - } - return Result.Ok(undefined); - } -} - -class DataGateway extends BaseGateway implements StoreTypeGateway { - constructor(sthis: SuperThis) { - super(sthis, "DataGateway"); - } - async getConn(uri: URI, conn: MsgConnected): Promise> { - // type: string, method: HttpMethods, store: FPStoreTypes, waitForFn: - const rResSignedUrl = await this.getResSignedUrl( - "reqGetData", - "GET", - "data", - MsgIsResGetData, - uri, - conn - ); - if (MsgIsError(rResSignedUrl)) { - return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); - } - const { signedUrl: downloadUrl } = rResSignedUrl; - return this.getObject(uri, downloadUrl); - } - async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { - const rResSignedUrl = await this.getResSignedUrl( - "reqPutData", - "PUT", - "data", - MsgIsResPutData, - uri, - conn - ); - if (MsgIsError(rResSignedUrl)) { - return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); - } - const { signedUrl: uploadUrl } = rResSignedUrl; - return this.putObject(uri, uploadUrl, body); - } - async delConn(uri: URI, conn: MsgConnected): Promise> { - const rResSignedUrl = await this.getResSignedUrl( - "reqDelData", - "DELETE", - "data", - MsgIsResPutData, - uri, - conn - ); - if (MsgIsError(rResSignedUrl)) { - return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); - } - const { signedUrl: deleteUrl } = rResSignedUrl; - return this.delObject(uri, deleteUrl); - } -} - -class MetaGateway extends BaseGateway implements StoreTypeGateway { - constructor(sthis: SuperThis) { - super(sthis, "MetaGateway"); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getConn(uri: URI, conn: MsgConnected): Promise> { - // const rkey = uri.getParamResult("key"); - // if (rkey.isErr()) { - // return Result.Err(rkey.Err()); - // } - // const rsu = buildReqGetMeta(this.sthis, conn.key, { - // ...conn.key, - // method: "GET", - // store: "meta", - // key: rkey.Ok(), - // }); - // const rRes = await conn.request(rsu, { - // waitType: "resGetMeta", - // }); - // if (rRes.isErr()) { - // return Result.Err(rRes.Err()); - // } - // const res = rRes.Ok(); - // if (MsgIsError(res)) { - // return Result.Err(res); - // } - // if (res.signedGetUrl) { - // return this.getObject(uri, res.signedGetUrl); - // } - // return Result.Ok(this.sthis.txt.encode(JSON.stringify(res.metas))); - return Result.Ok(new Uint8Array()); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { - // const bodyRes = Result.Ok(body); // await bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body); - // if (bodyRes.isErr()) { - // return this.logger.Error().Err(bodyRes).Msg("Error in addCryptoKeyToGatewayMetaPayload").ResultError(); - // } - // const rsu = this.prepareReqSignedUrl(uri, "PUT", conn.key); - // if (rsu.isErr()) { - // return Result.Err(rsu.Err()); - // } - // const dbMetas = JSON.parse(this.sthis.txt.decode(bodyRes.Ok())) as CRDTEntry[]; - // this.logger.Debug().Any("dbMetas", dbMetas).Msg("putMeta"); - // const req = buildReqPutMeta(this.sthis, conn.key, rsu.Ok().params, dbMetas); - // const res = await conn.request(req, { waitType: "resPutMeta" }); - // if (res.isErr()) { - // return Result.Err(res.Err()); - // } - // // console.log("putMeta", JSON.stringify({dbMetas, res})); - // this.logger.Debug().Any("qs", { req, res: res.Ok() }).Msg("putMeta"); - // this.putObject(uri, res.Ok().signedPutUrl, bodyRes.Ok()); - // return res; - return Result.Ok(undefined); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async delConn(uri: URI, conn: MsgConnected): Promise> { - // const rsu = this.prepareReqSignedUrl(uri, "DELETE", conn.key); - // if (rsu.isErr()) { - // return Result.Err(rsu.Err()); - // } - // const res = await conn.request(buildReqDelMeta(this.sthis, conn.key, rsu.Ok().params), { - // waitType: "resDelMeta", - // }); - // if (res.isErr()) { - // return Result.Err(res.Err()); - // } - // const { signedDelUrl } = res.Ok(); - // if (signedDelUrl) { - // return this.delObject(uri, signedDelUrl); - // } - // return Result.Ok(undefined); - return Result.Ok(undefined); - } -} - -class WALGateway extends BaseGateway implements StoreTypeGateway { - // WAL will not pollute to the cloud - readonly wals = new Map(); - constructor(sthis: SuperThis) { - super(sthis, "WALGateway"); - } - getWalKeyFromUri(uri: URI): Result { - const rKey = uri.getParamsResult({ - key: 0, - name: 0, - }); - if (rKey.isErr()) { - return Result.Err(rKey.Err()); - } - const { name, key } = rKey.Ok(); - return Result.Ok(`${name}:${key}`); - } - async getConn(uri: URI): Promise> { - const rKey = this.getWalKeyFromUri(uri); - if (rKey.isErr()) { - return Result.Err(rKey.Err()); - } - const wal = this.wals.get(rKey.Ok()); - if (!wal) { - return Result.Err(new NotFoundError("Not found")); - } - return Result.Ok(wal); - } - async putConn(uri: URI, body: Uint8Array): Promise> { - const rKey = this.getWalKeyFromUri(uri); - if (rKey.isErr()) { - return Result.Err(rKey.Err()); - } - this.wals.set(rKey.Ok(), body); - return Result.Ok(undefined); - } - async delConn(uri: URI): Promise> { - const rKey = this.getWalKeyFromUri(uri); - if (rKey.isErr()) { - return Result.Err(rKey.Err()); - } - this.wals.delete(rKey.Ok()); - return Result.Ok(undefined); - } -} - -const storeTypedGateways = new KeyedResolvOnce(); -function getStoreTypeGateway(sthis: SuperThis, uri: URI): StoreTypeGateway { - const store = uri.getParam("store"); - switch (store) { - case "data": - return storeTypedGateways.get(store).once(() => new DataGateway(sthis)); - case "meta": - return storeTypedGateways.get(store).once(() => new MetaGateway(sthis)); - case "wal": - return storeTypedGateways.get(store).once(() => new WALGateway(sthis)); - default: - throw ensureLogger(sthis, "getStoreTypeGateway") - .Error() - .Str("store", store) - .Msg("Invalid store type") - .ResultError(); - } -} - -// const keyedConnections = new KeyedResolvOnce(); -interface Subscription { - readonly sid: string; - readonly uri: string; // optimization - readonly callback: (msg: Uint8Array) => void; - readonly unsub: () => void; -} -const subscriptions = new Map(); -// const doServerSubscribe = new KeyedResolvOnce(); -const trackPuts = new Set(); -export class FireproofCloudGateway implements bs.Gateway { - readonly logger: Logger; - readonly sthis: SuperThis; - - constructor(sthis: SuperThis) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "FireproofCloudGateway", { - this: true, - }); - } - - async buildUrl(baseUrl: URI, key: string): Promise> { - return Result.Ok(baseUrl.build().setParam("key", key).URI()); - } - - async start(uri: URI): Promise> { - await this.sthis.start(); - const ret = uri.build().defParam("version", VERSION); - const rName = uri.getParamResult("name"); - if (rName.isErr()) { - return this.logger.Error().Err(rName).Msg("name not found").ResultError(); - } - ret.defParam("protocol", "wss"); - return Result.Ok(ret.URI()); - } - - async get(uri: URI): Promise { - return getStoreTypeGateway(this.sthis, uri).get(uri, this.getCloudConnection(uri)); - } - - async put(uri: URI, body: Uint8Array): Promise> { - const ret = await getStoreTypeGateway(this.sthis, uri).put(uri, body, this.getCloudConnection(uri)); - if (ret.isOk()) { - if (uri.getParam("testMode")) { - trackPuts.add(uri.toString()); - } - } - return ret; - } - - async delete(uri: URI): Promise { - trackPuts.delete(uri.toString()); - return getStoreTypeGateway(this.sthis, uri).delete(uri, this.getCloudConnection(uri)); - } - - async close(uri: URI): Promise { - const uriStr = uri.toString(); - // CAUTION here is my happen a mutation of subscriptions caused by unsub - for (const sub of Array.from(subscriptions.values())) { - for (const s of sub) { - if (s.uri.toString() === uriStr) { - s.unsub(); - } - } - } - const rConn = await this.getCloudConnection(uri); - if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in getCloudConnection").ResultError(); - } - const conn = rConn.Ok(); - await conn.close(); - return Result.Ok(undefined); - } - - // fireproof://localhost:1999/?name=test-public-api&protocol=ws&store=meta - async getCloudConnection(uri: URI): Promise> { - const rParams = uri.getParamsResult({ - name: key.REQUIRED, - protocol: "https", - store: key.REQUIRED, - storekey: key.OPTIONAL, - tenant: key.REQUIRED, - }); - if (rParams.isErr()) { - return this.logger.Error().Url(uri).Err(rParams).Msg("getCloudConnection:err").ResultError(); - } - const params = rParams.Ok(); - // let tenant: string; - // if (params.tenant) { - // tenant = params.tenant; - // } else { - // if (!params.storekey) { - // return this.logger.Error().Url(uri).Msg("no tendant or storekey given").ResultError(); - // } - // const dataKey = params.storekey.replace(/:(meta|wal)@$/, `:data@`); - // const kb = await rt.kb.getKeyBag(this.sthis); - // const rfingerprint = await kb.getNamedKey(dataKey); - // if (rfingerprint.isErr()) { - // return this.logger.Error().Err(rfingerprint).Msg("Error in getNamedKey").ResultError(); - // } - // tenant = rfingerprint.Ok().fingerPrint; - // } - const qOpen = buildReqOpen(this.sthis, {}); - - let cUrl = uri.build().protocol(params.protocol).cleanParams().URI(); - if (cUrl.pathname === "/") { - cUrl = cUrl.build().pathname("/fp").URI(); - } - return Msger.connect(this.sthis, cUrl, qOpen); - // keyedConnections.get(keyTenantLedger(qOpen.conn.key)).once(async () => Msger.open(this.sthis, cUrl, qOpen)); - } - - // private notifySubscribers(data: Uint8Array, callbacks: ((msg: Uint8Array) => void)[] = []): void { - // for (const cb of callbacks) { - // try { - // cb(data); - // } catch (error) { - // this.logger.Error().Err(error).Msg("Error in subscriber callback execution"); - // } - // } - // } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async subscribe(uri: URI, callback: (meta: Uint8Array) => void): Promise { - return Result.Err(new Error("Not implemented")); - // const rParams = uri.getParamsResult({ - // store: 0, - // storekey: 0, - // }); - // if (rParams.isErr()) { - // return this.logger.Error().Err(rParams).Msg("Error in subscribe").ResultError(); - // } - // const { store } = rParams.Ok(); - // if (store !== "meta") { - // return Result.Err(new Error("store must be meta")); - // } - // const rConn = await this.getCloudConnection(uri); - // if (rConn.isErr()) { - // return this.logger.Error().Err(rConn).Msg("Error in subscribe:getCloudConnection").ResultError(); - // } - // const conn = rConn.Ok(); - // const rResSubscribeMeta = await doServerSubscribe.get(pkKey(conn.key)).once(async () => { - // const subId = this.sthis.nextId().str; - // const fn = (subId: string) => (msg: MsgBase) => { - // if (MsgIsUpdateMetaEvent(msg) && subId === msg.subscriberId) { - // // console.log("onMessage", subId, conn.key, msg.metas); - // const s = subscriptions.get(subId); - // if (!s) { - // return; - // } - // console.log("msg", JSON.stringify(msg)); - // this.notifySubscribers( - // this.sthis.txt.encode(JSON.stringify(msg.metas)), - // s.map((s) => s.callback) - // ); - // } - // }; - // conn.onMessage(fn(subId)); - // return conn.request(buildReqSubscriptMeta(this.sthis, conn.key, subId), { - // waitType: "resSubscribeMeta", - // }); - // }); - // if (rResSubscribeMeta.isErr()) { - // return this.logger.Error().Err(rResSubscribeMeta).Msg("Error in subscribe:request").ResultError(); - // } - // const subId = rResSubscribeMeta.Ok().subscriberId; - // let callbacks = subscriptions.get(subId); - // if (!callbacks) { - // callbacks = []; - // subscriptions.set(subId, callbacks); - // } - // const sid = this.sthis.nextId().str; - // const unsub = () => { - // const idx = callbacks.findIndex((c) => c.sid === sid); - // if (idx !== -1) { - // callbacks.splice(idx, 1); - // } - // if (callbacks.length === 0) { - // subscriptions.delete(subId); - // } - // }; - // callbacks.push({ uri: uri.toString(), callback, sid, unsub }); - // return Result.Ok(unsub); - } - - async destroy(_uri: URI): Promise> { - await Promise.all(Array.from(trackPuts).map(async (k) => this.delete(URI.from(k)))); - return Result.Ok(undefined); - } -} - -// function pkKey(set?: ConnectionKey): string { -// const ret = JSON.stringify( -// Object.entries(set || {}) -// .sort(([a], [b]) => a.localeCompare(b)) -// .filter(([k]) => k !== "id") -// .map(([k, v]) => ({ [k]: v })) -// ); -// return ret; -// } - -export class FireproofCloudTestStore implements bs.TestGateway { - readonly logger: Logger; - readonly sthis: SuperThis; - readonly gateway: bs.Gateway; - constructor(gw: bs.Gateway, sthis: SuperThis) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "FireproofCloudTestStore"); - this.gateway = gw; - } - async get(uri: URI, key: string): Promise { - const url = uri.build().setParam("key", key).URI(); - const dbFile = this.sthis.pathOps.join(rt.getPath(url, this.sthis), rt.getFileName(url, this.sthis)); - this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get"); - const buffer = await this.gateway.get(url); - this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got"); - return buffer.Ok(); - } -} - -const onceRegisterFireproofCloudStoreProtocol = new KeyedResolvOnce<() => void>(); -export function registerFireproofCloudStoreProtocol(protocol = "fireproof:", overrideBaseURL?: string) { - return onceRegisterFireproofCloudStoreProtocol.get(protocol).once(() => { - URI.protocolHasHostpart(protocol); - return bs.registerStoreProtocol({ - protocol, - overrideBaseURL, - gateway: async (sthis) => { - return new FireproofCloudGateway(sthis); - }, - test: async (sthis: SuperThis) => { - const gateway = new FireproofCloudGateway(sthis); - return new FireproofCloudTestStore(gateway, sthis); - }, - }); - }); -} diff --git a/src/fp-cloud/client/index.ts b/src/fp-cloud/client/index.ts deleted file mode 100644 index 48804cd3..00000000 --- a/src/fp-cloud/client/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BuildURI, CoerceURI, KeyedResolvOnce, runtimeFn, URI } from "@adviser/cement"; -import { bs, Database, fireproof } from "@fireproof/core"; -import { ConnectFunction, connectionFactory, makeKeyBagUrlExtractable } from "../../connection-from-store.js"; -import { registerFireproofCloudStoreProtocol } from "./gateway.js"; - -interface ConnectData { - readonly remoteName: string; - firstConnect: boolean; - endpoint?: string; -} - -const SYNC_DB_NAME = "fp_sync"; - -// Usage: -// -// import { useFireproof } from 'use-fireproof' -// import { connect } from '@fireproof/cloud' -// -// const { db } = useFireproof('test') -// -// const cx = connect(db); - -// TODO need to set the keybag url automatically - -// if (!process.env.FP_KEYBAG_URL) { -// process.env.FP_KEYBAG_URL = "file://./dist/kb-dir-fireproof?fs=mem"; -// } - -// if (!runtimeFn().isBrowser) { -// const url = BuildURI.from(process.env.FP_KEYBAG_URL || rt.kb.defaultKeyBagUrl()); -// url.setParam("extractKey", "_deprecated_internal_api"); -// process.env.FP_KEYBAG_URL = url.toString(); -// } - -registerFireproofCloudStoreProtocol(); - -const connectionCache = new KeyedResolvOnce(); -export const rawConnect: ConnectFunction = ( - db: Database, - remoteDbName = "", - url = "fireproof://cloud.fireproof.direct" -) => { - const { sthis, blockstore, name: dbName } = db; - if (!dbName) { - throw new Error("dbName is required"); - } - const urlObj = BuildURI.from(url); - urlObj.protocol("fireproof:"); - const existingName = urlObj.getParam("name"); - urlObj.defParam("name", remoteDbName || existingName || dbName); - urlObj.defParam("localName", dbName); - urlObj.defParam("storekey", `@${dbName}:data@`); - urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/"); - // const fpUrl = urlObj - // .toString() - // .replace(/^http:\/\//, "fireproof://") - // .replace(/^https:\/\//, "fireproof://"); - // eslint-disable-next-line no-console - console.log("Config URL: " + urlObj.toString()); - return connectionCache.get(urlObj.toString()).once(() => { - makeKeyBagUrlExtractable(sthis); - const connection = connectionFactory(sthis, urlObj); - connection.connect_X(blockstore); - return connection; - }); -}; - -async function getOrCreateRemoteName(dbName: string, remoteName?: string) { - const syncDb = fireproof(SYNC_DB_NAME); - - const result = await syncDb.query("localName", { key: dbName, includeDocs: true }); - if (result.rows.length === 0) { - const doc = { - remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str, - localName: dbName, - firstConnect: !remoteName, - } as ConnectData; - const { id } = await syncDb.put(doc); - return { ...doc, _id: id }; - } - const doc = result.rows[0].doc; - return doc; -} - -export function connect( - db: Database, - remoteName?: string, - dashboardURI: CoerceURI = "https://dashboard.fireproof.storage/", - remoteURI: CoerceURI = "fireproof://cloud.fireproof.direct" -): Promise { - const dbName = db.name as string; - if (!dbName) { - throw new Error("Database name is required for cloud connection"); - } - - return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => { - if (!doc) { - throw new Error("Failed to get or create remote name"); - } - doc.endpoint = URI.from(remoteURI).toString(); - const connection = rawConnect(db, doc.remoteName, URI.from(doc.endpoint).toString()); - const connectURI = URI.from(dashboardURI).build().pathname("/fp/databases/connect"); - connectURI.defParam("localName", dbName); - connectURI.defParam("remoteName", doc.remoteName); - if (doc.endpoint) { - connectURI.defParam("endpoint", doc.endpoint); - } - // eslint-disable-next-line no-console - console.log("Fireproof Cloud: " + connectURI.toString()); - if ( - doc.firstConnect && - runtimeFn().isBrowser && - window.location.href.indexOf(URI.from(dashboardURI).toString()) === -1 - ) { - // Set firstConnect to false after opening the window, so we don't constantly annoy with the dashboard - const syncDb = fireproof(SYNC_DB_NAME); - doc.firstConnect = false; - await syncDb.put(doc); - - // window.open(connectURI.toString(), "_blank"); - } - connection.dashboardUrl = URI.from(connectURI); - return connection; - }); -} diff --git a/src/fp-cloud/cloud.test.ts-off b/src/fp-cloud/cloud.test.ts-off deleted file mode 100644 index a0811ec5..00000000 --- a/src/fp-cloud/cloud.test.ts-off +++ /dev/null @@ -1,533 +0,0 @@ -// import { env } from "cloudflare:test" -import { BuildURI, Future, URI } from "@adviser/cement"; -import { ReqSignedUrl, ResSignedUrl } from "./msg-types.js"; -import { Env } from "./backend/env.js"; -import { $ } from "zx"; -import fs from "fs/promises"; -import * as toml from "smol-toml"; -import { bs, CRDTEntry, Database, ensureSuperThis, fireproof, isNotFoundError, rt } from "@fireproof/core"; -import { AwsClient } from "aws4fetch"; -import { smokeDB } from "../../tests/helper.js"; -// import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./client/gateway.ts-off"; -import { calculatePreSignedUrl } from "./pre-signed-url.js"; -import { newWebSocket } from "./new-websocket.js"; -import { registerFireproofCloudStoreProtocol } from "./client/gateway.js"; - -// function testReqSignedUrl(tid = "test") { -// return { -// tid: tid, -// type: "reqSignedUrl", -// params: { -// // protocol: "ws", -// path: "/hallo", -// store: "wal", -// key: "main", -// }, -// version: "test", -// } satisfies ReqSignedUrl; -// } - -// async function testResSignedUrl(env: Env, tid?: string, amzDate?: string): Promise { -// const req = testReqSignedUrl(tid); -// const rSignedUrl = await calculatePreSignedUrl(req, env, amzDate); -// if (rSignedUrl.isErr()) { -// throw rSignedUrl.Err(); -// } -// return { -// params: req.params, -// signedUrl: rSignedUrl.Ok().toString(), -// // `http://localhost:8080/tenantId/test-name/wal/main.json?tid=${tid}&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKeyId%2F20241121%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241121T225359Z&X-Amz-Expires=86400&X-Amz-Signature=f52d5ecfbb6be93210dd57cb49ba1e426a8aee24a0738aedb636ae5722fcdded&X-Amz-SignedHeaders=host`, -// tid: tid || "test", -// type: "resSignedUrl", -// version: env.VERSION, -// } satisfies ResSignedUrl; -// } - -// describe("CloudBackendTest", () => { -// const sthis = ensureSuperThis(); -// let env: Env; -// let pid: number; -// const port = +(process.env.FP_WRANGLER_PORT || 0) || ~~(1024 + Math.random() * (0x10000 - 1024)); -// const wrangler = BuildURI.from("http://localhost") -// .port("" + port) -// .URI(); -// async function cfFetch(relative: string, init: RequestInit) { -// return fetch(wrangler.build().appendRelative(relative).asURL(), init); -// } -// beforeAll(async () => { -// const tomlFile = "src/cloud/backend/wrangler.toml"; -// const tomeStr = await fs.readFile(tomlFile, "utf-8"); -// const wranglerFile = toml.parse(tomeStr) as unknown as { -// env: { test: { vars: Env } }; -// }; -// env = wranglerFile.env.test.vars; -// if (process.env.FP_WRANGLER_PORT) { -// return; -// } -// $.verbose = !!process.env.FP_DEBUG; -// const runningWrangler = $` -// wrangler dev -c ${tomlFile} --port ${port} --env test --no-show-interactive-dev-session & -// waitPid=$! -// echo "PID:$waitPid" -// wait $waitPid`; -// const waitReady = new Future(); -// runningWrangler.stdout.on("data", (chunk) => { -// // console.log(">>", chunk.toString()) -// const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; -// if (mightPid) { -// pid = +mightPid; -// } -// if (chunk.includes("Ready on http")) { -// waitReady.resolve(true); -// } -// }); -// runningWrangler.stderr.on("data", (chunk) => { -// // eslint-disable-next-line no-console -// console.error("!!", chunk.toString()); -// }); -// await waitReady.asPromise(); -// // await f.asPromise() -// // wrangler dev -c src/cloud/backend/wrangler.toml --port 4711 --env test -// }); - -// afterAll(async () => { -// // console.log("kill", runningWrangler.pid, runningWrangler) -// // process.kill(runningWrangler.pid) -// // process.stdin.write(Array(4).fill("x\n\r").join("")) -// if (pid) process.kill(pid); -// }); - -// describe("raw tests", () => { -// it("return 404", async () => { -// const res = await cfFetch("/posts", {}); -// expect(res.status).toBe(404); -// expect(await res.json()).toEqual({ -// message: "Notfound:/posts", -// tid: "internal", -// type: "error", -// version: env.VERSION, -// }); -// }); -// it("return 422 invalid json", async () => { -// const res = await cfFetch("/fp", { method: "PUT" }); -// expect(res.status).toBe(422); -// expect(await res.json()).toEqual({ -// message: "Unexpected end of JSON input", -// tid: "internal", -// type: "error", -// version: env.VERSION, -// }); -// }); - -// it("return 422 illegal msg", async () => { -// const res = await cfFetch("/fp", { -// method: "PUT", -// body: JSON.stringify({ -// bucket: "test", -// key: "test", -// }), -// }); -// expect(res.status).toBe(422); -// expect(await res.json()).toEqual({ -// message: "unknown msg.type=undefined", -// tid: "internal", -// type: "error", -// version: env.VERSION, -// }); -// }); - -// it("return 200 msg", async () => { -// const res = await cfFetch("/fp", { -// method: "PUT", -// body: JSON.stringify(testReqSignedUrl()), -// }); -// expect(res.status).toBe(200); -// expect(await res.json()).toEqual(await testResSignedUrl(env)); -// }); - -// // it("reqOpen without websocket", async () => { -// // const conn = await msgOpen(cfURL, { } -// // }); - -// // it("reqOpen with websocket", async () => { -// // }); - -// it("use websockets SignedUrl", async () => { -// await Promise.all( -// Array(100) -// .fill(null) -// .map(async () => { -// const url = wrangler.build().appendRelative("/ws").protocol("ws:"); -// const so = await newWebSocket(url); -// const done = new Future(); -// let total = 10; -// let tid = `${total}-test-${Math.random()}`; -// so.onopen = () => { -// so.send(JSON.stringify(testReqSignedUrl(tid))); -// }; -// so.onmessage = async (msg) => { -// try { -// const res = JSON.parse(msg.data.toString()) as ResSignedUrl; -// expect(res).toEqual(await testResSignedUrl(env, tid, URI.from(res.signedUrl).getParam("X-Amz-Date"))); -// if (--total === 0) { -// done.resolve(true); -// } else { -// tid = `${total}-test-${Math.random()}`; -// so.send(JSON.stringify(testReqSignedUrl(tid))); -// } -// } catch (err) { -// done.reject(err); -// } -// }; -// so.onerror = (ev) => { -// assert.fail(`WebSocket error: ${ev}`); -// }; -// return done.asPromise().then(() => so.close(1000, "done")); -// }) -// ); -// }); -// }); - -describe("FireproofCloudGateway", () => { - let db: Database; - let unregister: () => void; - interface ExtendedGateway extends bs.Gateway { - headerSize: number; - subscribe?: (url: URI, callback: (meta: Uint8Array) => void) => Promise; // Changed VoidResult to UnsubscribeResult - } - - // has to leave - interface ExtendedStore { - gateway: ExtendedGateway; - _url: URI; - name: string; - } - - beforeAll(() => { - unregister = registerFireproofCloudStoreProtocol("fireproof:"); - }); - - beforeEach(() => { - const config = { - store: { - stores: { - base: wrangler.build().protocol("fireproof:").setParam("protocol", "ws").setParam("testMode", "true"), - // process.env.FP_STORAGE_URL, // || "fireproof://localhost:1968", - }, - }, - }; - const name = "fireproof-cloud-test-db-" + sthis.nextId().str; - db = fireproof(name, config); - }); - - afterEach(async () => { - // Clear the database before each test - if (db) { - await db.close(); - await db.destroy(); - } - }); - - afterAll(() => { - unregister(); - }); - - // it("env setup is ok", () => { - // // expect(process.env.FP_STORAGE_URL).toMatch(/fireproof:\/\/localhost:1999/); - // }); - - it("should have loader and options", () => { - const loader = db.blockstore.loader; - expect(loader).toBeDefined(); - if (!loader) { - throw new Error("Loader is not defined"); - } - expect(loader.ebOpts).toBeDefined(); - expect(loader.ebOpts.store).toBeDefined(); - expect(loader.ebOpts.store.stores).toBeDefined(); - if (!loader.ebOpts.store.stores) { - throw new Error("Loader stores is not defined"); - } - if (!loader.ebOpts.store.stores.base) { - throw new Error("Loader stores.base is not defined"); - } - - const baseUrl = URI.from(loader.ebOpts.store.stores.base); - expect(baseUrl.protocol).toBe("fireproof:"); - // expect(baseUrl.hostname).toBe("localhost"); - // expect(baseUrl.port || "").toBe("1999"); - }); - - it("should initialize and perform basic operations", async () => { - const docs = await smokeDB(db); - - // // get a new db instance - // db = new Database(name, config); - - // Test update operation - const updateDoc = await db.get<{ content: string }>(docs[0]._id); - updateDoc.content = "Updated content"; - const updateResult = await db.put(updateDoc); - expect(updateResult.id).toBe(updateDoc._id); - - const updatedDoc = await db.get<{ content: string }>(updateDoc._id); - expect(updatedDoc.content).toBe("Updated content"); - - // Test delete operation - await db.del(updateDoc._id); - try { - await db.get(updateDoc._id); - throw new Error("Document should have been deleted"); - } catch (e) { - const error = e as Error; - expect(error.message).toContain("Not found"); - } - }); - - it("should subscribe to changes", async () => { - // Extract stores from the loader - const metaStore = (await db.blockstore.loader?.metaStore()) as unknown as ExtendedStore; - - const metaGateway = metaStore?.gateway; - - const metaUrl = await metaGateway?.buildUrl(metaStore?._url, "main"); - await metaGateway?.start(metaStore?._url); - - let didCall = false; - - expect(metaGateway.subscribe).toBeTypeOf("function"); - if (metaGateway.subscribe) { - const future = new Future(); - - const metaSubscribeResult = await metaGateway.subscribe(metaUrl?.Ok(), (data: Uint8Array) => { - // console.log("data", data); - const decodedData = sthis.txt.decode(data); - expect(decodedData).toContain("parents"); - didCall = true; - future.resolve(); - }); - expect(metaSubscribeResult.isOk()).toBeTruthy(); - const ok = await db.put({ _id: "key1", hello: "world1" }); - expect(ok).toBeTruthy(); - expect(ok.id).toBe("key1"); - await future.asPromise(); - expect(didCall).toBeTruthy(); - metaSubscribeResult.Ok()(); - } - }); -}); -describe("AwsClient R2", () => { - it("make presigned url", async () => { - const sthis = ensureSuperThis(); - const a4f = new AwsClient({ - accessKeyId: sthis.env.get("CF_ACCESS_KEY_ID") || "accessKeyId", - secretAccessKey: sthis.env.get("CF_SECRET_ACCESS_KEY") || "secretAccessKey", - region: "us-east-1", - service: "s3", - }); - const buildUrl = BuildURI.from(sthis.env.get("CF_STORAGE_URL") || "https://bucket.example.com/db/main") - .appendRelative("db/main") - .setParam("X-Amz-Expires", "22"); - const signedUrl = await a4f - .sign(new Request(buildUrl.toString(), { method: "PUT" }), { - aws: { - signQuery: true, - datetime: "2021-09-01T12:34:56Z", - }, - }) - .then((res) => res.url); - expect(URI.from(signedUrl).asObj()).toEqual( - buildUrl - .setParam("X-Amz-Date", "2021-09-01T12:34:56Z") - .setParam("X-Amz-Algorithm", "AWS4-HMAC-SHA256") - .setParam("X-Amz-Credential", `${a4f.accessKeyId}/2021-09-/${a4f.region}/${a4f.service}/aws4_request`) - .setParam("X-Amz-SignedHeaders", "host") - .setParam( - "X-Amz-Signature", - sthis.env.get("CF_PRESIGNED_SIGNATURE") || "bbae4604fbe51a4ce9972183d8871a8a187ab0f4d2415afd6dc728f8ccc9900f" - ) - .asObj() - ); - }); -}); - -describe(`store=meta`, () => { - const store = "meta"; - let gw: bs.Gateway; - const sthis = ensureSuperThis(); - let uri: URI; - beforeAll(async () => { - gw = new FireproofCloudGateway(sthis); - const id = sthis.nextId().str; - uri = BuildURI.from("fireproof://localhost") - .port("" + port) - .setParam("store", store) - .setParam("name", id) - .setParam("protocol", "ws") - .setParam("storekey", id) - .setParam("testMode", "true") - .URI(); - - const last: Uint8Array[] = []; - const cnt = 4; - Array(cnt) - .fill(null) - .map(async () => { - const rOk = (await gw.subscribe?.(uri, (meta: Uint8Array) => { - last.push(meta); - if (last.length === cnt) { - expect(last[0]).toEqual(last[1]); - expect(last[1]).toEqual(last[2]); - expect(last[2]).toEqual(last[3]); - last.length = 0; - } - })) as bs.VoidResult; - expect(rOk.isOk()).toBeTruthy(); - }); - - const keyBag = await rt.kb.getKeyBag(sthis); - await keyBag.getNamedKey(`@${id}:data@`); - }); - - afterAll(async () => { - const rOk = await gw.close(uri); - expect(rOk.isOk()).toBeTruthy(); - }); - - const subscribeCallbacks: { - connId: string; - uri: URI; - cb: ReturnType void>>; - unsub: bs.UnsubscribeResult; - }[] = []; - beforeEach(async () => { - await Promise.all( - Array(1) - .fill(null) - .map(async () => { - const cb = vitest.fn(); - const connId = sthis.nextId().str; - const uriConnId = uri.build().setParam("connId", connId).URI(); - const unsub = (await gw.subscribe?.(uriConnId, (meta: Uint8Array) => - cb(sthis.txt.decode(meta), connId) - )) as bs.UnsubscribeResult; - subscribeCallbacks.push({ cb, unsub, connId, uri: uriConnId }); - }) - ); - }); - afterEach(() => { - subscribeCallbacks.forEach(({ unsub }) => unsub.Ok()()); - subscribeCallbacks.length = 0; - }); - - function crdtEntry(connId = "default"): Uint8Array { - return sthis.txt.encode( - JSON.stringify([ - { - cid: `${connId}:bafyreidjlylxmmb3yuz7levzzbso3g7ql54zovxl3mkhbbqxmmfnfbkoym`, - data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWdvdHM3dmFzeHhhdmdoY3FjeHo3ZXJibTdtY21ramQybTV0bXpzcGdhbG91d2lpcjYzZnkifV19Z3BhcmVudHOA", - parents: [], - }, - { - cid: `${connId}:bafyreie7izpgpmxd6heoiweoyblgyzoxt74xrp5wcpqo66bmjv2plgmceq`, - data: "MomRkYXRhoWZkYk1ldGFYU3siY2FycyI6W3siLyI6ImJhZzR5dnFhYmNpcWQyZ2l1c2t2YWJoZTZ5ZHdsdXo0aGx4Z3lyNTZ5dmZmbjVpdndqdmhlYXl3cWJ4bHFmeGEifV19Z3BhcmVudHOA", - parents: [], - }, - ] satisfies CRDTEntry[]) - ); - } - - it(`buildUrl`, async () => { - const rOk = await gw.buildUrl(uri, "KEY"); - const url = rOk.Ok(); - expect(url.getParam("store")).toBe(store); - expect(url.getParam("key")).toBe("KEY"); - }); - it(`start`, async () => { - const rOk = await gw.start(uri); - const url = rOk.Ok(); - expect(url.getParam("store")).toBe(store); - expect(url.getParam("version")).toBeTruthy(); - }); - - it(`unsubscribe`, async () => { - subscribeCallbacks.forEach((sub) => sub.unsub.Ok()()); - const rOk = await gw.put(uri.build().setParam("key", "main").URI(), crdtEntry()); - // console.log(rOk); - expect(rOk.isOk()).toBeTruthy(); - subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); - }); - - it(`get-put-delete`, async () => { - async function getNotFound() { - for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { - for (const key of ["KEY1", "KEY2"]) { - const rOk = await gw.get(u.build().setParam("key", key).URI()); - expect(rOk.isErr()).toBeTruthy(); - expect(isNotFoundError(rOk.Err())).toBeTruthy(); - } - } - subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); - } - console.log("getNotFound-pre"); - - subscribeCallbacks.forEach(({ cb }) => { - expect(cb).toHaveBeenCalledTimes(0); - }); - // get not found - await getNotFound(); - // put - async function put() { - for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { - for (const key of ["KEY1", "KEY2"]) { - const rOk = await gw.put( - u.build().setParam("key", key).URI(), - crdtEntry(`${key}:${u.getParam("connId", "default")}`) - ); - expect(rOk.isOk()).toBeTruthy(); - } - } - // console.log('put', subscribeCallbacks.map(({ cb }) => cb.mock.calls)); - subscribeCallbacks.forEach(({ cb, connId }) => { - // expect(cb).toHaveBeenCalledTimes(subscribeCallbacks.length * 2); - for (const key of ["KEY1", "KEY2"]) { - expect(cb).toHaveBeenCalledWith(sthis.txt.decode(crdtEntry(`${key}:${connId}`)), connId); - } - }); - } - // console.log('put-pre') - await put(); - subscribeCallbacks.forEach(({ cb }) => cb.mockClear()); - - async function get() { - for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { - for (const key of ["KEY1", "KEY2"]) { - const rOk = await gw.get(u.build().setParam("key", key).URI()); - const data = JSON.parse(sthis.txt.decode(rOk.Ok())) as CRDTEntry[]; - expect(data).toEqual(subscribeCallbacks.map(({ connId }) => crdtEntry(connId))); - } - } - subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); - } - console.log("get-pre"); - await get(); - async function del() { - for (const u of [...subscribeCallbacks.map(({ uri }) => uri), uri]) { - for (const key of ["KEY1", "KEY2"]) { - const rOk = await gw.delete(u.build().setParam("key", key).URI()); - expect(rOk.isOk()).toBeTruthy(); - } - } - subscribeCallbacks.forEach(({ cb }) => expect(cb).not.toHaveBeenCalled()); - } - console.log("del-pre"); - await del(); - // get not found - console.log("getNotFound-pre"); - await getNotFound(); - }); - it(`close`, async () => { - const rOk = await gw.close(uri); - expect(rOk.isOk()).toBeTruthy(); - }); -}); diff --git a/src/fp-cloud/connection.test.ts b/src/fp-cloud/connection.test.ts deleted file mode 100644 index 43c4fa3a..00000000 --- a/src/fp-cloud/connection.test.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { ensureSuperThis } from "@fireproof/core"; -import { URI } from "@adviser/cement"; -import { - buildReqGestalt, - buildReqOpen, - MsgIsError, - MsgIsResGestalt, - MsgIsResOpen, - defaultGestalt, - ReqSignedUrlParam, - GwCtx, - MsgWithError, - ResOptionalSignedUrl, -} from "./msg-types.js"; -import { - MsgIsResGetData, - MsgIsResPutData, - MsgIsResDelData, - buildReqPutData, - buildReqDelData, - buildReqGetData, -} from "./msg-types-data.js"; -import { - buildReqGetWAL, - buildReqPutWAL, - buildReqDelWAL, - MsgIsResGetWAL, - MsgIsResPutWAL, - MsgIsResDelWAL, -} from "./msg-types-wal.js"; -import { applyStart, defaultMsgParams, MsgConnected, Msger } from "./msger.js"; -import { HonoServer } from "./hono-server.js"; -import { Hono } from "hono"; -import { calculatePreSignedUrl } from "./pre-signed-url.js"; -import { CFHonoServerFactory, httpStyle, NodeHonoServerFactory, resolveToml, wsStyle } from "./test-helper.js"; -import { - buildReqDelMeta, - buildBindGetMeta, - buildReqPutMeta, - MsgIsResDelMeta, - ResDelMeta, - ReqDelMeta, - BindGetMeta, - EventGetMeta, - MsgIsEventGetMeta, - MsgIsResPutMeta, -} from "./msg-type-meta.js"; - -async function refURL(sp: ResOptionalSignedUrl) { - const { env } = await resolveToml("D1"); - return ( - await calculatePreSignedUrl(sp, { - storageUrl: URI.from(env.STORAGE_URL), - aws: { - accessKeyId: env.ACCESS_KEY_ID, - secretAccessKey: env.SECRET_ACCESS_KEY, - region: env.REGION, - }, - test: { - amzDate: URI.from(sp.signedUrl).getParam("X-Amz-Date"), - }, - }) - ) - .Ok() - .asObj(); -} - -describe("Connection", () => { - const sthis = ensureSuperThis(); - const msgP = defaultMsgParams(sthis, { hasPersistent: true }); - - beforeAll(async () => { - sthis.env.sets((await resolveToml("D1")).env as unknown as Record); - }); - - describe.each([NodeHonoServerFactory(), CFHonoServerFactory("DO"), CFHonoServerFactory("D1")])( - "$name - Connection", - (honoServer) => { - const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); - const qOpen = buildReqOpen(sthis, { reqId: "req-open-test" }); - const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - describe.each([httpStyle(sthis, port, msgP, my), wsStyle(sthis, port, msgP, my)])( - `${honoServer.name} - $name`, - (style) => { - let server: HonoServer; - beforeAll(async () => { - const app = new Hono(); - server = await honoServer - .factory(sthis, msgP, style.remoteGestalt, port) - .then((srv) => srv.register(app, port)); - }); - afterAll(async () => { - // console.log("closing server"); - await server.close(); - }); - it(`conn refused`, async () => { - const rC = await applyStart(style.connRefused.open()); - expect(rC.isErr()).toBeTruthy(); - expect(rC.Err().message).toMatch(/ECONNREFUSED/); - }); - - it(`timeout`, async () => { - const rC = await applyStart(style.timeout.open()); - expect(rC.isErr()).toBeTruthy(); - expect(rC.Err().message).toMatch(/Timeout/i); - }); - - describe(`connection`, () => { - let c: MsgConnected; - beforeEach(async () => { - const rC = await style.ok.open().then((r) => MsgConnected.connect(r, { reqId: "req-open-testx" })); - expect(rC.isOk()).toBeTruthy(); - c = rC.Ok(); - expect(c.conn).toEqual({ - reqId: "req-open-testx", - resId: c.conn.resId, - }); - }); - afterEach(async () => { - await c.close(); - }); - - it("kaputt url http", async () => { - const r = await c.raw.request( - { - tid: "test", - type: "kaputt", - version: "FP-MSG-1.0", - }, - { waitFor: () => true } - ); - if (!MsgIsError(r)) { - assert.fail("expected MsgError"); - return; - } - expect(r).toEqual({ - message: "unexpected message", - tid: "test", - type: "error", - version: "FP-MSG-1.0", - src: { - tid: "test", - type: "kaputt", - version: "FP-MSG-1.0", - }, - }); - }); - it("gestalt url http", async () => { - const msgP = defaultMsgParams(sthis, {}); - const req = buildReqGestalt(sthis, defaultGestalt(msgP, { id: "test" })); - const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); - if (!MsgIsResGestalt(r)) { - assert.fail("expected MsgError", JSON.stringify(r)); - } - expect(r.gestalt).toEqual(c.exchangedGestalt?.remote); - }); - - it("openConnection", async () => { - const req = buildReqOpen(sthis, { ...c.conn }); - const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); - if (!MsgIsResOpen(r)) { - assert.fail(JSON.stringify(r)); - } - expect(r).toEqual({ - conn: { ...c.conn, resId: r.conn?.resId }, - tid: req.tid, - type: "resOpen", - version: "FP-MSG-1.0", - }); - }); - }); - - it("open", async () => { - const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, { - reqId: "req-open-testy", - }); - expect(rC.isOk()).toBeTruthy(); - const c = rC.Ok(); - expect(c.conn).toEqual({ - reqId: "req-open-testy", - resId: c.conn.resId, - }); - expect(c.raw).toBeInstanceOf(style.cInstance); - expect(c.exchangedGestalt).toEqual({ - my, - remote: style.remoteGestalt, - }); - await c.close(); - }); - describe(`${honoServer.name} - Msgs`, () => { - let gwCtx: GwCtx; - let conn: MsgConnected; - beforeAll(async () => { - const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); - expect(rC.isOk()).toBeTruthy(); - conn = rC.Ok(); - gwCtx = { - conn: conn.conn, - tenant: { - tenant: "Tenant", - ledger: "Ledger", - }, - }; - }); - afterAll(async () => { - await conn.close(); - }); - it("Open", async () => { - const res = await conn.raw.request(buildReqOpen(sthis, conn.conn), { waitFor: MsgIsResOpen }); - if (!MsgIsResOpen(res)) { - assert.fail("expected MsgResOpen", JSON.stringify(res)); - } - expect(MsgIsResOpen(res)).toBeTruthy(); - expect(res.conn).toEqual({ ...qOpen.conn, resId: res.conn.resId }); - }); - - function sup() { - return { - path: "test/me", - key: "key-test", - } satisfies ReqSignedUrlParam; - } - describe("Data", async () => { - it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildReqGetData(sthis, sp, gwCtx), { waitFor: MsgIsResGetData }); - if (MsgIsResGetData(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResGetData", JSON.stringify(res)); - } - }); - it("Put", async () => { - const sp = sup(); - const res = await conn.request(buildReqPutData(sthis, sp, gwCtx), { waitFor: MsgIsResPutData }); - if (MsgIsResPutData(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResPutData", JSON.stringify(res)); - } - }); - it("Del", async () => { - const sp = sup(); - const res = await conn.request(buildReqDelData(sthis, sp, gwCtx), { waitFor: MsgIsResDelData }); - if (MsgIsResDelData(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResDelData", JSON.stringify(res)); - } - }); - }); - - describe("Meta", async () => { - it("bind stop", async () => { - const sp = sup(); - expect(conn.raw.activeBinds.size).toBe(0); - const streams: ReadableStream>[] = Array(5) - .fill(0) - .map(() => { - return conn.bind(buildBindGetMeta(sthis, sp, gwCtx), { - waitFor: MsgIsEventGetMeta, - }); - }); - for await (const stream of streams) { - const reader = stream.getReader(); - while (true) { - const { done, value: msg } = await reader.read(); - if (done) { - break; - } - if (MsgIsEventGetMeta(msg)) { - // expect(msg.params).toEqual(sp); - expect(URI.from(msg.signedUrl).asObj()).toEqual(await refURL(msg)); - } else { - assert.fail("expected MsgEventGetMeta", JSON.stringify(msg)); - } - await reader.cancel(); - } - } - expect(conn.raw.activeBinds.size).toBe(0); - // await Promise.all(streams.map((s) => s.cancel())); - }); - - it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildBindGetMeta(sthis, sp, gwCtx), { waitFor: MsgIsEventGetMeta }); - if (MsgIsEventGetMeta(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgIsEventGetMeta", JSON.stringify(res)); - } - }); - it("Put", async () => { - const sp = sup(); - const metas = Array(5) - .fill({ cid: "x", parents: [], data: "MomRkYXRho" }) - .map((data) => { - return { ...data, cid: sthis.timeOrderedNextId().str }; - }); - const res = await conn.request(buildReqPutMeta(sthis, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta }); - if (MsgIsResPutMeta(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgIsResPutMeta", JSON.stringify(res)); - } - }); - it("Del", async () => { - const sp = sup(); - const res = await conn.request(buildReqDelMeta(sthis, sp, gwCtx), { - waitFor: MsgIsResDelMeta, - }); - if (MsgIsResDelMeta(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResDelWAL", JSON.stringify(res)); - } - }); - }); - describe("WAL", async () => { - it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildReqGetWAL(sthis, sp, gwCtx), { waitFor: MsgIsResGetWAL }); - if (MsgIsResGetWAL(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResGetWAL", JSON.stringify(res)); - } - }); - it("Put", async () => { - const sp = sup(); - const res = await conn.request(buildReqPutWAL(sthis, sp, gwCtx), { waitFor: MsgIsResPutWAL }); - if (MsgIsResPutWAL(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResPutWAL", JSON.stringify(res)); - } - }); - it("Del", async () => { - const sp = sup(); - const res = await conn.request(buildReqDelWAL(sthis, sp, gwCtx), { waitFor: MsgIsResDelWAL }); - if (MsgIsResDelWAL(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResDelWAL", JSON.stringify(res)); - } - }); - }); - }); - } - ); - } - ); -}); diff --git a/src/fp-cloud/hono-server.ts b/src/fp-cloud/hono-server.ts deleted file mode 100644 index b04fe3b1..00000000 --- a/src/fp-cloud/hono-server.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { exception2Result, HttpHeader, param, ResolveOnce, Result, URI } from "@adviser/cement"; -import { Logger, SuperThis } from "@fireproof/core"; -import { Context, Hono, Next } from "hono"; -import { top_uint8 } from "../coerce-binary.js"; -import { - Gestalt, - buildErrorMsg, - MsgBase, - EnDeCoder, - ErrorMsg, - MsgWithError, - buildRes, - MsgWithConn, - GwCtx, - MsgIsError, -} from "./msg-types.js"; -import { MsgDispatcher, WSConnection } from "./msg-dispatch.js"; -import { WSEvents } from "hono/ws"; -import { calculatePreSignedUrl, PreSignedMsg } from "./pre-signed-url.js"; -import { buildMsgDispatcher } from "./msg-dispatcher-impl.js"; -import { - BindGetMeta, - buildEventGetMeta, - buildResDelMeta, - buildResPutMeta, - EventGetMeta, - ReqDelMeta, - ReqPutMeta, - ResDelMeta, - ResPutMeta, -} from "./msg-type-meta.js"; -import { MetaMerger } from "./meta-merger/meta-merger.js"; -import { SQLDatabase } from "./meta-merger/abstract-sql.js"; -import { WSRoom } from "./ws-room.js"; - -export interface RunTimeParams { - readonly sthis: SuperThis; - readonly logger: Logger; - readonly ende: EnDeCoder; - readonly impl: HonoServerImpl; -} -// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -export type ConnMiddleware = (conn: WSConnection, c: Context, next: Next) => Promise; -export interface HonoServerImpl { - start(): Promise; - gestalt(): Gestalt; - calculatePreSignedUrl(p: PreSignedMsg): Promise>; - upgradeWebSocket: (createEvents: (c: Context) => WSEvents | Promise) => ConnMiddleware; - handleBindGetMeta(sthis: SuperThis, logger: Logger, msg: BindGetMeta): Promise>; - handleReqPutMeta(sthis: SuperThis, logger: Logger, msg: ReqPutMeta): Promise>; - handleReqDelMeta(sthis: SuperThis, logger: Logger, msg: ReqDelMeta): Promise>; - readonly headers: HttpHeader; -} - -export abstract class HonoServerBase implements HonoServerImpl { - readonly _gs: Gestalt; - readonly sthis: SuperThis; - readonly logger: Logger; - readonly metaMerger: MetaMerger; - readonly headers: HttpHeader; - readonly wsRoom: WSRoom; - constructor(sthis: SuperThis, logger: Logger, gs: Gestalt, sqlDb: SQLDatabase, wsRoom: WSRoom, headers?: HttpHeader) { - this.logger = logger; - this._gs = gs; - this.sthis = sthis; - this.wsRoom = wsRoom; - this.metaMerger = new MetaMerger(sqlDb); - this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); - } - - abstract upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware; - - start(drop = false): Promise { - return this.metaMerger.createSchema(drop).then(() => this); - } - - gestalt(): Gestalt { - return this._gs; - } - - async handleReqPutMeta( - sthis: SuperThis, - logger: Logger, - msg: MsgWithConn - ): Promise> { - const rUrl = await buildRes("PUT", "meta", "resPutMeta", sthis, logger, msg, this); - if (MsgIsError(rUrl)) { - return rUrl; - } - await this.metaMerger.addMeta({ - logger, - connection: msg, - metas: msg.metas, - }); - return buildResPutMeta(sthis, logger, msg, { ...rUrl, metas: await this.metaMerger.metaToSend(msg) }); - } - - async handleReqDelMeta( - sthis: SuperThis, - logger: Logger, - msg: MsgWithConn - ): Promise> { - const rUrl = await buildRes("DELETE", "meta", "resDelMeta", sthis, logger, msg, this); - if (MsgIsError(rUrl)) { - return rUrl; - } - await this.metaMerger.delMeta({ - logger, - connection: msg, - }); - return buildResDelMeta(sthis, logger, msg, rUrl.signedUrl); - } - - async handleBindGetMeta( - sthis: SuperThis, - logger: Logger, - msg: MsgWithConn, - gwCtx: GwCtx = msg - ): Promise> { - const rUrl = await buildRes("GET", "meta", "eventGetMeta", sthis, logger, msg, this); - if (MsgIsError(rUrl)) { - return rUrl; - } - return buildEventGetMeta( - sthis, - logger, - msg, - { - ...rUrl, - metas: await this.metaMerger.metaToSend(msg), - }, - gwCtx - ); - } - - calculatePreSignedUrl(p: PreSignedMsg): Promise> { - const rRes = this.sthis.env.gets({ - STORAGE_URL: param.REQUIRED, - ACCESS_KEY_ID: param.REQUIRED, - SECRET_ACCESS_KEY: param.REQUIRED, - REGION: "us-east-1", - }); - if (rRes.isErr()) { - return Promise.resolve(Result.Err(rRes.Err())); - } - const res = rRes.Ok(); - return calculatePreSignedUrl(p, { - storageUrl: URI.from(res.STORAGE_URL), - aws: { - accessKeyId: res.ACCESS_KEY_ID, - secretAccessKey: res.SECRET_ACCESS_KEY, - region: res.REGION, - }, - }); - } -} - -export interface HonoServerFactory { - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise; - - start(app: Hono): Promise; - serve(app: Hono, port?: number): Promise; - close(): Promise; -} - -export const CORS = HttpHeader.from({ - // "Accept": "application/json", - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET,POST,OPTIONS,PUT,DELETE", - "Access-Control-Max-Age": "86400", // Cache pre-flight response for 24 hours -}); - -export class HonoServer { - // readonly sthis: SuperThis; - // readonly msgP: MsgerParams; - // readonly gestalt: Gestalt; - // readonly logger: Logger; - readonly factory: HonoServerFactory; - constructor(/* sthis: SuperThis, msgP: MsgerParams, gestalt: Gestalt, */ factory: HonoServerFactory) { - // this.sthis = sthis; - // this.logger = ensureLogger(sthis, "HonoServer"); - // this.msgP = msgP; - // this.gestalt = gestalt; - this.factory = factory; - } - readonly _register = new ResolveOnce(); - async register(app: Hono, port?: number): Promise { - return this._register.once(async () => { - await this.factory.start(app); - // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) - // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) - app.put("/fp", (c) => - this.factory.inject(c, async ({ sthis, logger, impl }) => { - impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); - const rMsg = await exception2Result(() => c.req.json() as Promise); - if (rMsg.isErr()) { - c.status(400); - return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); - } - const dispatcher = buildMsgDispatcher(sthis, impl.gestalt()); - return dispatcher.dispatch(impl, rMsg.Ok(), (msg) => c.json(msg)); - }) - ); - app.get("/ws", (c, next) => - this.factory.inject(c, async ({ sthis, logger, ende, impl }) => { - return impl.upgradeWebSocket((_c) => { - let dp: MsgDispatcher; - return { - onOpen: (_e, _ws) => { - dp = buildMsgDispatcher(sthis, impl.gestalt()); - }, - onError: (error) => { - logger.Error().Err(error).Msg("WebSocket error"); - }, - onMessage: async (event, ws) => { - const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); - if (rMsg.isErr()) { - ws.send( - ende.encode( - buildErrorMsg( - sthis, - logger, - { - message: event.data, - } as ErrorMsg, - rMsg.Err() - ) - ) - ); - } else { - await dp.dispatch(impl, rMsg.Ok(), (msg) => { - const str = ende.encode(msg); - ws.send(str); - return new Response(str); - }); - } - }, - onClose: () => { - dp = undefined as unknown as MsgDispatcher; - // console.log('Connection closed') - }, - }; - })(new WSConnection(), c, next); - }) - ); - await this.factory.serve(app, port); - return this; - }); - } - async close() { - const ret = await this.factory.close(); - return ret; - } -} - -// export async function honoServer(_sthis: SuperThis, _msgP: MsgerParams, _gestalt: Gestalt) { -// const rt = runtimeFn(); -// if (rt.isNodeIsh) { -// // const { NodeHonoServer } = await import("./node-hono-server.js"); -// // return new HonoServer(sthis, msgP, gestalt, new NodeHonoServer()); -// } -// throw new Error("Not implemented"); -// } diff --git a/src/fp-cloud/http-connection.ts b/src/fp-cloud/http-connection.ts deleted file mode 100644 index 17a07b34..00000000 --- a/src/fp-cloud/http-connection.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { HttpHeader, Logger, Result, URI, exception2Result } from "@adviser/cement"; -import { SuperThis, ensureLogger } from "@fireproof/core"; -import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError } from "./msg-types.js"; -import { - ActiveStream, - ExchangedGestalt, - MsgerParamsWithEnDe, - MsgRawConnection, - OnMsgFn, - selectRandom, - timeout, - UnReg, -} from "./msger.js"; -import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; - -export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnection { - readonly logger: Logger; - readonly msgP: MsgerParamsWithEnDe; - - readonly baseURIs: URI[]; - - readonly #onMsg = new Map(); - - constructor(sthis: SuperThis, uris: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { - super(sthis, exGestalt); - this.logger = ensureLogger(sthis, "HttpConnection"); - // this.msgParam = msgP; - this.baseURIs = uris; - this.msgP = msgP; - } - - async start(): Promise> { - // if (this._qsOpen.req) { - // const sOpen = await this.request(this._qsOpen.req, { waitFor: MsgIsResOpen }); - // if (!MsgIsResOpen(sOpen)) { - // return Result.Err(this.logger.Error().Any("Err", sOpen).Msg("unexpected response").AsError()); - // } - // this._qsOpen.res = sOpen; - // } - return Result.Ok(undefined); - } - - async close(): Promise> { - await Promise.all(Array.from(this.activeBinds.values()).map((state) => state.controller?.close())); - this.#onMsg.clear(); - return Result.Ok(undefined); - } - - toMsg(msg: MsgWithError): MsgWithError { - this.#onMsg.forEach((fn) => fn(msg)); - return msg; - } - - onMsg(fn: OnMsgFn): UnReg { - const key = this.sthis.nextId().str; - this.#onMsg.set(key, fn); - return () => this.#onMsg.delete(key); - } - - #poll(state: ActiveStream): void { - this.request(state.bind.msg, state.bind.opts) - .then((msg) => { - try { - state.controller?.enqueue(msg); - if (MsgIsError(msg)) { - state.controller?.close(); - } else { - state.timeout = setTimeout(() => this.#poll(state), state.bind.opts.pollInterval ?? 1000); - } - } catch (err) { - console.log("poll error", err); - state.controller?.error(err); - state.controller?.close(); - } - }) - .catch((err) => { - console.log("poll catch error", err); - state.controller?.error(err); - // state.controller?.close(); - }); - } - - readonly activeBinds = new Map>(); - bind(req: Q, opts: RequestOpts): ReadableStream> { - const state: ActiveStream = { - id: this.sthis.nextId().str, - bind: { - msg: req, - opts, - }, - } satisfies ActiveStream; - this.activeBinds.set(state.id, state); - return new ReadableStream>({ - cancel: () => { - clearTimeout(state.timeout as number); - this.activeBinds.delete(state.id); - }, - start: (controller) => { - state.controller = controller; - this.#poll(state); - }, - }); - } - - async request(req: Q, _opts: RequestOpts): Promise> { - const headers = HttpHeader.from(); - headers.Set("Content-Type", this.msgP.mime); - headers.Set("Accept", this.msgP.mime); - - const rReqBody = exception2Result(() => this.msgP.ende.encode(req)); - if (rReqBody.isErr()) { - return this.toMsg( - buildErrorMsg( - this.sthis, - this.logger, - req, - this.logger.Error().Err(rReqBody.Err()).Any("req", req).Msg("encode error").AsError() - ) - ); - } - headers.Set("Content-Length", rReqBody.Ok().byteLength.toString()); - const url = selectRandom(this.baseURIs); - this.logger.Debug().Url(url).Any("body", req).Msg("request"); - const rRes = await exception2Result(() => - timeout( - this.msgP.timeout, - fetch(url.toString(), { - method: "PUT", - headers: headers.AsHeaderInit(), - body: rReqBody.Ok(), - }) - ) - ); - this.logger.Debug().Url(url).Any("body", rRes).Msg("response"); - if (rRes.isErr()) { - return this.toMsg( - buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Err(rRes).Msg("fetch error").AsError()) - ); - } - const res = rRes.Ok(); - if (!res.ok) { - return this.toMsg( - buildErrorMsg( - this.sthis, - this.logger, - req, - this.logger - .Error() - .Url(url) - .Str("status", res.status.toString()) - .Str("statusText", res.statusText) - .Msg("HTTP Error") - .AsError(), - await res.text() - ) - ); - } - const data = new Uint8Array(await res.arrayBuffer()); - const ret = await exception2Result(async () => this.msgP.ende.decode(data) as S); - if (ret.isErr()) { - return this.toMsg( - buildErrorMsg( - this.sthis, - this.logger, - req, - this.logger.Error().Err(ret.Err()).Msg("decode error").AsError(), - this.sthis.txt.decode(data) - ) - ); - } - return this.toMsg(ret.Ok()); - } - - // toOnMessage(msg: WithErrorMsg): Result> { - // this.mec.msgFn?.(msg as unknown as MessageEvent); - // return Result.Ok(msg); - // } -} diff --git a/src/fp-cloud/meta-merger/abstract-sql.ts b/src/fp-cloud/meta-merger/abstract-sql.ts deleted file mode 100644 index 445c5d2a..00000000 --- a/src/fp-cloud/meta-merger/abstract-sql.ts +++ /dev/null @@ -1,53 +0,0 @@ -// import { RunResult } from "better-sqlite3"; - -// export function now() { -// return new Date().toISOString(); -// } - -// export interface SqlLiteStmt { -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// bind(...args: any[]): any; -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// run(...args: any[]): Promise; -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// get(...args: any[]): Promise; -// } - -// export interface SqlLite { -// prepare(sql: string): SqlLiteStmt; -// } - -// export interface SqlLiteDBDialect { - -// } - -// export type SQLLiteFlavor = BaseSQLiteDatabase<'async', unknown>; - -export interface SQLDatabase { - prepare(sql: string): SQLStatement; -} - -export type SQLParams = (string | number | Date)[]; - -// export type SQLRow = Record; - -export interface SQLStatement { - run(...params: SQLParams): Promise; - all(...params: SQLParams): Promise; -} - -export function conditionalDrop(drop: boolean, tabName: string, create: string): string[] { - if (!drop) { - return [create]; - } - return [`DROP TABLE IF EXISTS ${tabName}`, create]; -} - -export function sqliteCoerceParams(params: SQLParams): (string | number)[] { - return params.map((i) => { - if (i instanceof Date) { - return i.toISOString(); - } - return i; - }); -} diff --git a/src/fp-cloud/meta-merger/bettersql-abstract-sql.ts b/src/fp-cloud/meta-merger/bettersql-abstract-sql.ts deleted file mode 100644 index e28bf869..00000000 --- a/src/fp-cloud/meta-merger/bettersql-abstract-sql.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "./abstract-sql.js"; - -import Database from "better-sqlite3"; - -export class BetterSQLStatement implements SQLStatement { - readonly stmt: Database.Statement; - constructor(stmt: Database.Statement) { - this.stmt = stmt; - } - - async run(...iparams: SQLParams): Promise { - const res = (await this.stmt.run(...sqliteCoerceParams(iparams))) as T; - // console.log("run", res); - return res; - } - async all(...params: SQLParams): Promise { - const res = (await this.stmt.all(...sqliteCoerceParams(params))) as T[]; - // console.log("all", res); - return res; - } -} - -export class BetterSQLDatabase implements SQLDatabase { - readonly db: Database.Database; - constructor(dbOrPath: Database.Database | string) { - if (typeof dbOrPath === "string") { - this.db = new Database(dbOrPath); - } else { - this.db = dbOrPath; - } - } - - prepare(sql: string): SQLStatement { - return new BetterSQLStatement(this.db.prepare(sql)); - } -} diff --git a/src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts b/src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts deleted file mode 100644 index f1cabf1e..00000000 --- a/src/fp-cloud/meta-merger/cf-worker-abstract-sql.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { SQLDatabase, sqliteCoerceParams, SQLParams, SQLStatement } from "./abstract-sql.js"; - -import type { D1Database } from "@cloudflare/workers-types"; - -export class CFWorkerSQLStatement implements SQLStatement { - readonly stmt: D1PreparedStatement; - constructor(stmt: D1PreparedStatement) { - this.stmt = stmt; - } - - async run(...iparams: SQLParams): Promise { - const bound = this.stmt.bind(...sqliteCoerceParams(iparams)); - // console.log("cf-run", sqliteCoerceParams(iparams), bound); - return bound.run() as T; - } - async all(...params: SQLParams): Promise { - const rows = await this.stmt.bind(...sqliteCoerceParams(params)).run(); - return rows.results as T[]; - } -} - -export class CFWorkerSQLDatabase implements SQLDatabase { - readonly db: D1Database; - constructor(db: D1Database) { - this.db = db; - } - - prepare(sql: string): SQLStatement { - return new CFWorkerSQLStatement(this.db.prepare(sql)); - } -} diff --git a/src/fp-cloud/meta-merger/create-schema-cli.ts b/src/fp-cloud/meta-merger/create-schema-cli.ts deleted file mode 100644 index 7ebd858e..00000000 --- a/src/fp-cloud/meta-merger/create-schema-cli.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MetaSendSql } from "./meta-send.js"; - -async function main() { - // eslint-disable-next-line no-console - console.log(MetaSendSql.schema(true).join(";\n")); -} - -// eslint-disable-next-line no-console -main().catch(console.error); diff --git a/src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts b/src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts deleted file mode 100644 index b8f0e94b..00000000 --- a/src/fp-cloud/meta-merger/meta-by-tenant-ledger.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { ResolveOnce } from "@adviser/cement"; -import { CRDTEntry } from "@fireproof/core"; -import { TenantLedgerSql } from "./tenant-ledger.js"; -import { ByConnection } from "./meta-merger.js"; -import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; - -export interface MetaByTenantLedgerRow { - readonly tenant: string; - readonly ledger: string; - readonly reqId: string; - readonly resId: string; - readonly metaCID: string; - readonly meta: CRDTEntry; - readonly updateAt: Date; -} - -interface SQLMetaByTenantLedgerRow { - readonly tenant: string; - readonly ledger: string; - readonly reqId: string; - readonly resId: string; - readonly metaCID: string; - readonly meta: string; - readonly updateAt: string; -} - -/* -SELECT * FROM Mitarbeiter e1 -WHERE NOT EXISTS -( - SELECT 1 FROM Mitarbeiter e2 - WHERE e1.employee_id=e2.employee_id und e2.employee_name LIKE 'A%' -); - */ - -export class MetaByTenantLedgerSql { - static schema(drop = false) { - return [ - ...TenantLedgerSql.schema(drop), - ...conditionalDrop( - drop, - "MetaByTenantLedger", - ` - CREATE TABLE IF NOT EXISTS MetaByTenantLedger( - tenant TEXT NOT NULL, - ledger TEXT NOT NULL, - reqId TEXT NOT NULL, - resId TEXT NOT NULL, - metaCID TEXT NOT NULL, - meta TEXT NOT NULL, - updatedAt TEXT NOT NULL, - PRIMARY KEY (tenant, ledger, reqId, resId, metaCID), - UNIQUE(metaCID), - FOREIGN KEY (tenant, ledger) REFERENCES TenantLedger(tenant, ledger) - ) - ` - ), - ]; - } - - readonly db: SQLDatabase; - readonly tenantLedgerSql: TenantLedgerSql; - constructor(db: SQLDatabase, tenantLedgerSql: TenantLedgerSql) { - this.db = db; - this.tenantLedgerSql = tenantLedgerSql; - } - - readonly #sqlCreateMetaByTenantLedger = new ResolveOnce(); - sqlCreateMetaByTenantLedger(): SQLStatement[] { - return this.#sqlCreateMetaByTenantLedger.once(() => { - return MetaByTenantLedgerSql.schema().map((i) => this.db.prepare(i)); - }); - } - - readonly #sqlInsertMetaByTenantLedger = new ResolveOnce(); - sqlEnsureMetaByTenantLedger(): SQLStatement { - return this.#sqlInsertMetaByTenantLedger.once(() => { - return this.db.prepare(` - INSERT INTO MetaByTenantLedger(tenant, ledger, reqId, resId, metaCID, meta, updatedAt) - SELECT ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS ( - SELECT 1 FROM MetaByTenantLedger WHERE metaCID = ? - ) - `); - }); - } - - readonly #sqlDeleteByConnection = new ResolveOnce(); - sqlDeleteByConnection(): SQLStatement { - return this.#sqlDeleteByConnection.once(() => { - return this.db.prepare(` - DELETE FROM MetaByTenantLedger - WHERE - tenant = ? - AND - ledger = ? - AND - reqId = ? - AND - resId = ? - AND - metaCID NOT IN (SELECT value FROM json_each(?)) - `); - }); - } - - /* - * select * from MetaByTenantLedger where tenant = 'tenant' and ledger = 'ledger' group by metaCID - */ - - // readonly #sqlSelectByMetaCIDs = new ResolveOnce() - // sqlSelectByMetaCIDs(): Statement { - // return this.#sqlSelectByMetaCIDs.once(() => { - // return this.db.prepare(` - // SELECT tenant, ledger, reqId, resId, metaCID, meta, updatedAt - // FROM MetaByTenantLedger - // WHERE metaCID in ? - // `); - // }) - // } - // async selectByMetaCIDs(metaCIDs: string[]): Promise { - // const stmt = this.sqlSelectByMetaCIDs(); - // const rows = await stmt.all(metaCIDs) - // return rows.map(row => ({ - // ...row, - // meta: JSON.parse(row.meta), - // updateAt: new Date(row.updateAt) - // } satisfies MetaByTenantLedgerRow)) - // } - - async deleteByConnection(t: ByConnection & { metaCIDs: string[] }) { - const stmt = this.sqlDeleteByConnection(); - return stmt.run(t.tenant, t.ledger, t.reqId, t.resId, JSON.stringify(t.metaCIDs)); - } - - async ensure(t: MetaByTenantLedgerRow) { - const stmt = this.sqlEnsureMetaByTenantLedger(); - return stmt.run( - t.tenant, - t.ledger, - t.reqId, - t.resId, - t.metaCID, - JSON.stringify(t.meta), - t.updateAt.toISOString(), - t.metaCID - ); - } - - readonly #sqlSelectByConnection = new ResolveOnce(); - sqlSelectByConnection(): SQLStatement { - return this.#sqlSelectByConnection.once(() => { - return this.db.prepare(` - SELECT tenant, ledger, reqId, resId, metaCID, meta, updatedAt - FROM MetaByTenantLedger - WHERE tenant = ? AND ledger = ? AND reqId = ? AND resId = ? - ORDER BY updatedAt - `); - }); - } - - async selectByConnection(conn: ByConnection): Promise { - const stmt = this.sqlSelectByConnection(); - const rows = await stmt.all(conn.tenant, conn.ledger, conn.reqId, conn.resId); - return rows.map( - (row) => - ({ - ...row, - meta: JSON.parse(row.meta), - updateAt: new Date(row.updateAt), - }) satisfies MetaByTenantLedgerRow - ); - } -} diff --git a/src/fp-cloud/meta-merger/meta-merger.test.ts b/src/fp-cloud/meta-merger/meta-merger.test.ts deleted file mode 100644 index 621715eb..00000000 --- a/src/fp-cloud/meta-merger/meta-merger.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -// import type { Database } from "better-sqlite3"; -import { Connection, MetaMerger } from "./meta-merger.js"; -import { CRDTEntry, ensureSuperThis } from "@fireproof/core"; -import { runtimeFn } from "@adviser/cement"; -import { SQLDatabase } from "./abstract-sql.js"; -import type { Env } from "../backend/env.js"; -import { getBackendDurableObject } from "../backend/cf-hono-server.js"; - -function sortCRDTEntries(rows: CRDTEntry[]) { - return rows.sort((a, b) => a.cid.localeCompare(b.cid)); -} - -interface MetaConnection { - readonly metas: CRDTEntry[]; - readonly connection: Connection; -} - -function toCRDTEntries(rows: MetaConnection[]) { - return rows.reduce((r, i) => [...r, ...i.metas], [] as CRDTEntry[]); -} - -// function filterConnection(ref: MetaConnection[], connection: Connection) { -// return toCRDTEntries(ref.filter((r) => -// (r.connection.tenant.tenant === connection.tenant.tenant && -// r.connection.tenant.ledger === connection.tenant.ledger && -// r.connection.conn.reqId === connection.conn.reqId && -// r.connection.conn.resId === connection.conn.resId))) -// } - -function getSQLFlavours(): { name: string; factory: () => Promise }[] { - if (runtimeFn().isCFWorker) { - return [ - { - name: "cf-worker-d1", - factory: async () => { - const { CFWorkerSQLDatabase } = await import("./cf-worker-abstract-sql.js"); - const { env } = await import("cloudflare:test"); - return new CFWorkerSQLDatabase((env as Env).FP_BACKEND_D1); - }, - }, - { - name: "cf-worker-do", - factory: async () => { - const { CFDObjSQLDatabase } = await import("../backend/cf-dobj-abstract-sql.js"); - const { env } = await import("cloudflare:test"); - return new CFDObjSQLDatabase(getBackendDurableObject(env as Env)); - }, - }, - ]; - } else { - return [ - { - name: "bettersql", - factory: async () => { - const { BetterSQLDatabase } = await import("./bettersql-abstract-sql.js"); - return new BetterSQLDatabase("./dist/test.db"); - }, - }, - ]; - } -} - -describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { - // let db: SQLDatabase; - const sthis = ensureSuperThis(); - const logger = sthis.logger; - let mm: MetaMerger; - beforeAll(async () => { - // db = new Database(':memory:'); - const db = await flavour.factory(); - mm = new MetaMerger(db); - await mm.createSchema(); - }); - - let connection: Connection; - beforeEach(() => { - connection = { - tenant: { - tenant: `tenant${sthis.timeOrderedNextId().str}`, - ledger: "ledger", - }, - conn: { - reqId: "reqId", - resId: `resId-${sthis.timeOrderedNextId().str}`, - }, - } satisfies Connection; - }); - - afterEach(async () => { - await mm.delMeta({ - logger, - connection, - }); - }); - - it("insert nothing", async () => { - await mm.addMeta({ - logger, - connection, - metas: [], - now: new Date(), - }); - const rows = await mm.metaToSend(connection); - expect(rows).toEqual([]); - }); - - it("insert one multiple", async () => { - const cid = sthis.timeOrderedNextId().str; - for (let i = 0; i < 10; i++) { - const metas = Array(i).fill({ - cid: cid, - parents: [], - data: "MomRkYXRho", - }); - await mm.addMeta({ - logger, - connection, - metas, - now: new Date(), - }); - const rows = await mm.metaToSend(connection); - if (i === 1) { - expect(rows).toEqual(metas); - } else { - expect(rows).toEqual([]); - } - } - }); - - it("insert multiple", async () => { - const conns = []; - for (let i = 0; i < 10; i++) { - const metas = Array(i) - .fill({ - cid: "x", - parents: [], - data: "MomRkYXRho", - }) - .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); - const conn = { - ...connection.conn, - reqId: sthis.timeOrderedNextId().str, - }; - conns.push(conn); - await mm.addMeta({ - logger, - connection: { - ...connection, - conn, - } satisfies Connection, - metas, - now: new Date(), - }); - const rows = await mm.metaToSend(connection); - expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(metas)); - } - await Promise.all( - conns.map(async (conn) => - mm.delMeta({ - logger, - connection: { ...connection, conn }, - metas: [], - }) - ) - ); - }); - - it("metaToSend to sink", async () => { - const connections = Array(2) - .fill(connection) - .map((c) => ({ ...c, conn: { ...c.conn, reqId: sthis.timeOrderedNextId().str } })); - const ref: MetaConnection[] = []; - for (const connection of connections) { - const metas = Array(2) - .fill({ - cid: "x", - parents: [], - data: "MomRkYXRho", - }) - .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); - ref.push({ metas, connection }); - await mm.addMeta({ - logger, - connection, - metas, - now: new Date(), - }); - } - // wrote 10 connections with 3 metas each - for (const connection of connections) { - const rows = await mm.metaToSend(connection); - expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(toCRDTEntries(ref))); - const rowsEmpty = await mm.metaToSend(connection); - expect(sortCRDTEntries(rowsEmpty)).toEqual([]); - } - const newConnections = Array(2) - .fill(connection) - .map((c) => ({ ...c, conn: { ...c.conn, reqId: sthis.timeOrderedNextId().str } })); - for (const connection of newConnections) { - const rows = await mm.metaToSend(connection); - expect(sortCRDTEntries(rows)).toEqual(sortCRDTEntries(toCRDTEntries(ref))); - const rowsEmpty = await mm.metaToSend(connection); - expect(sortCRDTEntries(rowsEmpty)).toEqual([]); - } - await Promise.all( - connections.map(async (connection) => - mm.delMeta({ - logger, - connection, - metas: [], - }) - ) - ); - }); - - it("delMeta", async () => { - await mm.addMeta({ - logger, - connection, - metas: [ - { - cid: `del-${sthis.timeOrderedNextId().str}`, - parents: [], - data: "MomRkYXRho", - }, - { - cid: `del-${sthis.timeOrderedNextId().str}`, - parents: [], - data: "MomRkYXRho", - }, - ], - now: new Date(), - }); - const rows = await mm.metaToSend(connection); - expect(rows.length).toBe(2); - await mm.delMeta({ - logger, - connection, - metas: rows, - now: new Date(), - }); - const rowsDel = await mm.metaToSend(connection); - expect(rowsDel.length).toBe(0); - }); -}); diff --git a/src/fp-cloud/meta-merger/meta-merger.ts b/src/fp-cloud/meta-merger/meta-merger.ts deleted file mode 100644 index 5b7a62d5..00000000 --- a/src/fp-cloud/meta-merger/meta-merger.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { CRDTEntry, Logger } from "@fireproof/core"; -import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; -import { MetaSendSql } from "./meta-send.js"; -import { TenantLedgerSql } from "./tenant-ledger.js"; -import { TenantSql } from "./tenant.js"; -import { SQLDatabase } from "./abstract-sql.js"; -import { QSId, TenantLedger } from "../msg-types.js"; - -export interface Connection { - readonly tenant: TenantLedger; - readonly conn: QSId; -} - -export interface MetaMerge { - readonly logger: Logger; - readonly connection: Connection; - readonly metas: CRDTEntry[]; - readonly now?: Date; -} - -export interface ByConnection { - readonly tenant: string; - readonly ledger: string; - readonly reqId: string; - readonly resId: string; -} - -function toByConnection(connection: Connection): ByConnection { - return { - ...connection.conn, - ...connection.tenant, - }; -} - -export class MetaMerger { - readonly db: SQLDatabase; - // readonly sthis: SuperThis; - readonly sql: { - readonly tenant: TenantSql; - readonly tenantLedger: TenantLedgerSql; - readonly metaByTenantLedger: MetaByTenantLedgerSql; - readonly metaSend: MetaSendSql; - }; - - constructor(db: SQLDatabase) { - this.db = db; - // this.sthis = sthis; - const tenant = new TenantSql(db); - const tenantLedger = new TenantLedgerSql(db, tenant); - this.sql = { - tenant, - tenantLedger, - metaByTenantLedger: new MetaByTenantLedgerSql(db, tenantLedger), - metaSend: new MetaSendSql(db), - }; - } - - async createSchema(drop = false) { - for (const i of this.sql.metaSend.sqlCreateMetaSend(drop)) { - await i.run(); - } - } - - async delMeta( - mm: Omit & { readonly metas?: CRDTEntry[] } - ): Promise<{ now: Date; byConnection: ByConnection }> { - const now = mm.now || new Date(); - const byConnection = toByConnection(mm.connection); - const metaCIDs = (mm.metas ?? []).map((meta) => meta.cid); - const connCIDs = { - ...byConnection, - // needs something with is not empty to delete - metaCIDs: metaCIDs.length ? metaCIDs : [new Date().toISOString()], - }; - await this.sql.metaSend.deleteByConnection(connCIDs); - await this.sql.metaByTenantLedger.deleteByConnection(connCIDs); - return { now, byConnection }; - } - - async addMeta(mm: MetaMerge) { - if (!mm.metas.length) { - return; - } - const { now, byConnection } = await this.delMeta(mm); - await this.sql.tenantLedger.ensure({ - ...mm.connection.tenant, - createdAt: now, - }); - for (const meta of mm.metas) { - try { - await this.sql.metaByTenantLedger.ensure({ - ...byConnection, - metaCID: meta.cid, - meta: meta, - updateAt: now, - }); - } catch (e) { - mm.logger.Warn().Err(e).Str("metaCID", meta.cid).Msg("addMeta"); - } - } - } - - async metaToSend(sink: Connection, now = new Date()): Promise { - const bySink = toByConnection(sink); - const rows = await this.sql.metaSend.selectToAddSend({ ...bySink, now }); - await this.sql.metaSend.insert( - rows.map((row) => ({ - metaCID: row.metaCID, - reqId: row.reqId, - resId: row.resId, - sendAt: row.sendAt, - })) - ); - return rows.map((row) => row.meta); - } -} diff --git a/src/fp-cloud/meta-merger/meta-send.ts b/src/fp-cloud/meta-merger/meta-send.ts deleted file mode 100644 index 2debc00a..00000000 --- a/src/fp-cloud/meta-merger/meta-send.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ResolveOnce } from "@adviser/cement"; -import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; -import { ByConnection } from "./meta-merger.js"; -import { CRDTEntry } from "@fireproof/core"; -import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; - -export interface MetaSendRow { - readonly metaCID: string; - readonly reqId: string; - readonly resId: string; - readonly sendAt: Date; -} - -type SQLMetaSendRowWithMeta = MetaSendRow & { meta: string }; -export type MetaSendRowWithMeta = MetaSendRow & { meta: CRDTEntry }; - -export class MetaSendSql { - static schema(drop = false) { - return [ - ...MetaByTenantLedgerSql.schema(drop), - ...conditionalDrop( - drop, - "MetaSend", - ` - CREATE TABLE IF NOT EXISTS MetaSend ( - metaCID TEXT NOT NULL, - reqId TEXT NOT NULL, - resId TEXT NOT NULL, - sendAt TEXT NOT NULL, - PRIMARY KEY(metaCID,reqId,resId), - FOREIGN KEY(metaCID) REFERENCES MetaByTenantLedger(metaCID) - ); - ` - ), - ]; - } - - readonly db: SQLDatabase; - constructor(db: SQLDatabase) { - this.db = db; - } - - readonly #sqlCreateMetaSend = new ResolveOnce(); - sqlCreateMetaSend(drop: boolean): SQLStatement[] { - return this.#sqlCreateMetaSend.once(() => { - return MetaSendSql.schema(drop).map((i) => this.db.prepare(i)); - }); - } - - readonly #sqlInsertMetaSend = new ResolveOnce(); - sqlInsertMetaSend(): SQLStatement { - return this.#sqlInsertMetaSend.once(() => { - return this.db.prepare(` - INSERT INTO MetaSend(metaCID, reqId, resId, sendAt) VALUES(?, ?, ?, ?) - `); - }); - } - - readonly #sqlSelectToAddSend = new ResolveOnce(); - sqlSelectToAddSend(): SQLStatement { - return this.#sqlSelectToAddSend.once(() => { - return this.db.prepare(` - SELECT t.metaCID, ? as reqId, ? as resId, ? as sendAt, t.meta FROM MetaByTenantLedger as t - WHERE - t.tenant = ? - AND - t.ledger = ? - AND - NOT EXISTS (SELECT 1 FROM MetaSend AS s WHERE t.metaCID = s.metaCID and s.reqId = ? and s.resId = ?) - `); - }); - } - - async selectToAddSend(conn: ByConnection & { now: Date }): Promise { - const stmt = this.sqlSelectToAddSend(); - const rows = await stmt.all( - conn.reqId, - conn.resId, - conn.now, - conn.tenant, - conn.ledger, - conn.reqId, - conn.resId - ); - return rows.map( - (i) => - ({ - metaCID: i.metaCID, - reqId: i.reqId, - resId: i.resId, - sendAt: new Date(i.sendAt), - meta: JSON.parse(i.meta) as CRDTEntry, - }) satisfies MetaSendRowWithMeta - ); - } - - async insert(t: MetaSendRow[]) { - const stmt = this.sqlInsertMetaSend(); - for (const i of t) { - await stmt.run(i.metaCID, i.reqId, i.resId, i.sendAt.toISOString()); - } - } - - readonly #sqlDeleteByConnection = new ResolveOnce(); - sqlDeleteByMetaCID(): SQLStatement { - return this.#sqlDeleteByConnection.once(() => { - return this.db.prepare(` - DELETE FROM MetaSend - WHERE metaCID in (SELECT metaCID FROM MetaByTenantLedger - WHERE - tenant = ? - AND - ledger = ? - AND - reqId = ? - AND - resId = ? - AND - metaCID NOT IN (SELECT value FROM json_each(?))) - `); - }); - } - - async deleteByConnection(dmi: ByConnection & { metaCIDs: string[] }) { - const stmt = this.sqlDeleteByMetaCID(); - return stmt.run(dmi.tenant, dmi.ledger, dmi.reqId, dmi.resId, JSON.stringify(dmi.metaCIDs)); - } -} diff --git a/src/fp-cloud/meta-merger/tenant-ledger.ts b/src/fp-cloud/meta-merger/tenant-ledger.ts deleted file mode 100644 index b5dc5fa7..00000000 --- a/src/fp-cloud/meta-merger/tenant-ledger.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ResolveOnce } from "@adviser/cement"; -import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; -import { TenantSql } from "./tenant.js"; - -export interface TenantLedgerRow { - readonly tenant: string; - readonly ledger: string; - readonly createdAt: Date; -} - -export class TenantLedgerSql { - static schema(drop = false) { - return [ - ...TenantSql.schema(drop), - ...conditionalDrop( - drop, - "TenantLedger", - ` - CREATE TABLE IF NOT EXISTS TenantLedger( - tenant TEXT NOT NULL, - ledger TEXT NOT NULL, - createdAt TEXT NOT NULL, - PRIMARY KEY(tenant, ledger), - FOREIGN KEY(tenant) REFERENCES Tenant(tenant) - ) - ` - ), - ]; - } - - readonly db: SQLDatabase; - readonly tenantSql: TenantSql; - constructor(db: SQLDatabase, tenantSql: TenantSql) { - this.db = db; - this.tenantSql = tenantSql; - } - - readonly #sqlCreateTenantLedger = new ResolveOnce(); - sqlCreateTenantLedger(): SQLStatement[] { - return this.#sqlCreateTenantLedger.once(() => { - return TenantLedgerSql.schema().map((i) => this.db.prepare(i)); - }); - } - - readonly #sqlInsertTenantLedger = new ResolveOnce(); - sqlEnsureTenantLedger(): SQLStatement { - return this.#sqlInsertTenantLedger.once(() => { - return this.db.prepare(` - INSERT INTO TenantLedger(tenant, ledger, createdAt) - SELECT ?, ?, ? WHERE - NOT EXISTS(SELECT 1 FROM TenantLedger WHERE tenant = ? and ledger = ?) - `); - }); - } - - async ensure(t: TenantLedgerRow) { - await this.tenantSql.ensure({ tenant: t.tenant, createdAt: t.createdAt }); - const stmt = this.sqlEnsureTenantLedger(); - const ret = stmt.run(t.tenant, t.ledger, t.createdAt, t.tenant, t.ledger); - return ret; - } -} diff --git a/src/fp-cloud/meta-merger/tenant.ts b/src/fp-cloud/meta-merger/tenant.ts deleted file mode 100644 index 3ee67001..00000000 --- a/src/fp-cloud/meta-merger/tenant.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ResolveOnce } from "@adviser/cement"; -import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; - -export interface TenantRow { - readonly tenant: string; - readonly createdAt: Date; -} - -export class TenantSql { - static schema(drop = false): string[] { - return [ - ...conditionalDrop( - drop, - "Tenant", - ` - CREATE TABLE IF NOT EXISTS Tenant( - tenant TEXT NOT NULL PRIMARY KEY, - createdAt TEXT NOT NULL - ) - ` - ), - ]; - } - - readonly db: SQLDatabase; - constructor(db: SQLDatabase) { - this.db = db; - } - - readonly #sqlCreateTenant = new ResolveOnce(); - sqlCreateTenant(): SQLStatement[] { - return this.#sqlCreateTenant.once(() => { - return TenantSql.schema().map((i) => this.db.prepare(i)); - }); - } - - readonly #sqlInsertTenant = new ResolveOnce(); - sqlEnsureTenant(): SQLStatement { - return this.#sqlInsertTenant.once(() => { - return this.db.prepare(` - INSERT INTO Tenant(tenant, createdAt) - SELECT ?, ? WHERE NOT EXISTS(SELECT 1 FROM Tenant WHERE tenant = ?) - `); - }); - } - - async ensure(t: TenantRow) { - const stmt = this.sqlEnsureTenant(); - return stmt.run(t.tenant, t.createdAt, t.tenant); - } -} diff --git a/src/fp-cloud/msg-dispatch.ts b/src/fp-cloud/msg-dispatch.ts deleted file mode 100644 index dc39b54a..00000000 --- a/src/fp-cloud/msg-dispatch.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Logger } from "@adviser/cement"; -import { SuperThis, ensureLogger } from "@fireproof/core"; -import { Gestalt, MsgBase, buildErrorMsg, MsgWithError, MsgIsWithConn, MsgWithConn, QSId } from "./msg-types.js"; - -import { PreSignedMsg } from "./pre-signed-url.js"; -import { HonoServerImpl } from "./hono-server.js"; -import { UnReg } from "./msger.js"; - -export interface MsgContext { - calculatePreSignedUrl(p: PreSignedMsg): Promise; -} - -export interface WSPair { - readonly client: WebSocket; - readonly server: WebSocket; -} - -export class WSConnection { - wspair?: WSPair; - - attachWSPair(wsp: WSPair) { - if (!this.wspair) { - this.wspair = wsp; - } else { - throw new Error("wspair already set"); - } - } -} - -type Promisable = T | Promise; - -// function WithValidConn(msg: T, rri?: ResOpen): msg is MsgWithConn { -// return MsgIsWithConn(msg) && !!rri && rri.conn.resId === msg.conn.resId && rri.conn.reqId === msg.conn.reqId; -// } - -interface ConnItem { - conn: QSId; - touched: Date; -} - -class ConnectionManager { - readonly conns = new Map(); - readonly maxItems: number; - - constructor(maxItems?: number) { - this.maxItems = maxItems || 100; - } - - addConn(conn: QSId): QSId { - if (this.conns.size >= this.maxItems) { - const oldest = Array.from(this.conns.values()); - const oneHourAgo = new Date(new Date().getTime() - 60 * 60 * 1000).getTime(); - oldest - .filter((item) => item.touched.getTime() < oneHourAgo) - .forEach((item) => this.conns.delete(item.conn.resId)); - } - this.conns.set(`${conn.reqId}:${conn.resId}`, { conn, touched: new Date() }); - return conn; - } - - isConnected(msg: MsgBase): msg is MsgWithConn { - if (!MsgIsWithConn(msg)) { - return false; - } - return this.conns.has(`${msg.conn.reqId}:${msg.conn.resId}`); - } -} -const connManager = new ConnectionManager(); - -export interface MsgDispatcherCtx { - readonly impl: HonoServerImpl; -} -export interface MsgDispatchItem { - readonly match: (msg: MsgBase) => boolean; - readonly isNotConn?: boolean; - fn(sthis: SuperThis, logger: Logger, ctx: MsgDispatcherCtx, msg: Q): Promisable>; -} - -export class MsgDispatcher { - readonly sthis: SuperThis; - readonly logger: Logger; - // wsConn?: WSConnection; - readonly gestalt: Gestalt; - readonly id: string; - - readonly connManager = connManager; - - static new(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { - return new MsgDispatcher(sthis, gestalt); - } - - private constructor(sthis: SuperThis, gestalt: Gestalt) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "Dispatcher"); - this.gestalt = gestalt; - this.id = sthis.nextId().str; - } - - // addConn(msg: MsgBase): Result { - // if (!MsgIsReqOpenWithConn(msg)) { - // return this.logger.Error().Msg("msg missing reqId").ResultError(); - // } - // return Result.Ok(connManager.addConn(msg.conn)); - // } - - readonly items = new Map>(); - registerMsg(...iItems: MsgDispatchItem[]): UnReg { - const items = iItems.flat(); - const ids: string[] = items.map((item) => { - const id = this.sthis.nextId(12).str; - this.items.set(id, item); - return id; - }); - return () => ids.forEach((id) => this.items.delete(id)); - } - - async dispatch(ctx: HonoServerImpl, msg: MsgBase, send: (msg: MsgBase) => Promisable): Promise { - const validateConn = async ( - msg: T, - fn: (msg: MsgWithConn) => Promisable> - ): Promise => { - if (!connManager.isConnected(msg)) { - return send(buildErrorMsg(this.sthis, this.logger, { ...msg }, new Error("dispatch missing connection"))); - // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); - } - // if (WithValidConn(msg, this.myOpen)) { - const r = await fn(msg); - return Promise.resolve(send(r)); - }; - const found = Array.from(this.items.values()).find((item) => item.match(msg)); - if (!found) { - return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); - } - if (!found.isNotConn) { - return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, { impl: ctx }, msg)); - } - return send(await found.fn(this.sthis, this.logger, { impl: ctx }, msg)); - } -} diff --git a/src/fp-cloud/msg-dispatcher-impl.ts b/src/fp-cloud/msg-dispatcher-impl.ts deleted file mode 100644 index 1645fc23..00000000 --- a/src/fp-cloud/msg-dispatcher-impl.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { SuperThis } from "@fireproof/core"; -import { MsgDispatcher } from "./msg-dispatch.js"; -import { - MsgIsReqGetData, - buildResGetData, - MsgIsReqPutData, - MsgIsReqDelData, - buildResDelData, - buildResPutData, - ReqGetData, - ReqPutData, - ReqDelData, -} from "./msg-types-data.js"; -import { - MsgIsReqDelWAL, - MsgIsReqGetWAL, - MsgIsReqPutWAL, - ReqDelWAL, - ReqGetWAL, - ReqPutWAL, - buildResDelWAL, - buildResGetWAL, - buildResPutWAL, -} from "./msg-types-wal.js"; -import { - MsgIsReqGestalt, - buildResGestalt, - MsgIsReqOpen, - buildErrorMsg, - buildResOpen, - MsgIsReqOpenWithConn, - MsgWithConn, - ReqGestalt, - Gestalt, -} from "./msg-types.js"; -import { - BindGetMeta, - MsgIsBindGetMeta, - MsgIsReqDelMeta, - MsgIsReqPutMeta, - ReqDelMeta, - ReqPutMeta, -} from "./msg-type-meta.js"; - -export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { - const dp = MsgDispatcher.new(sthis, gestalt); - dp.registerMsg( - { - match: MsgIsReqGestalt, - isNotConn: true, - fn: (_sthis, _logger, _ctx, msg: ReqGestalt) => { - return buildResGestalt(msg, dp.gestalt); - }, - }, - { - match: MsgIsReqOpen, - isNotConn: true, - fn: (sthis, logger, _ctx, msg) => { - if (!MsgIsReqOpenWithConn(msg)) { - return buildErrorMsg(sthis, logger, msg, new Error("missing connection")); - } - if (dp.connManager.isConnected(msg)) { - return buildResOpen(sthis, msg, msg.conn.resId); - } - const resId = sthis.nextId(12).str; - const resOpen = buildResOpen(sthis, msg, resId); - dp.connManager.addConn(resOpen.conn); - return resOpen; - }, - }, - { - match: MsgIsReqGetData, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResGetData(sthis, logger, msg, ctx.impl); - }, - }, - { - match: MsgIsReqPutData, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResPutData(sthis, logger, msg, ctx.impl); - }, - }, - { - match: MsgIsReqDelData, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResDelData(sthis, logger, msg, ctx.impl); - }, - }, - { - match: MsgIsReqGetWAL, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResGetWAL(sthis, logger, msg, ctx.impl); - }, - }, - { - match: MsgIsReqPutWAL, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResPutWAL(sthis, logger, msg, ctx.impl); - }, - }, - { - match: MsgIsReqDelWAL, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResDelWAL(sthis, logger, msg, ctx.impl); - }, - }, - { - match: MsgIsBindGetMeta, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return ctx.impl.handleBindGetMeta(sthis, logger, msg); - }, - }, - { - match: MsgIsReqPutMeta, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return ctx.impl.handleReqPutMeta(sthis, logger, msg); - }, - }, - { - match: MsgIsReqDelMeta, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return ctx.impl.handleReqDelMeta(sthis, logger, msg); - }, - } - ); - return dp; -} diff --git a/src/fp-cloud/msg-processor.ts-off b/src/fp-cloud/msg-processor.ts-off deleted file mode 100644 index 788e3884..00000000 --- a/src/fp-cloud/msg-processor.ts-off +++ /dev/null @@ -1,261 +0,0 @@ -import { exception2Result, Logger, } from "@adviser/cement"; -import { - buildErrorMsg, - buildResDelMeta, - buildResGestalt, - buildResGetMeta, - buildResPutMeta, - defaultGestalt, - ErrorMsg, - getStoreFromType, - MsgBase, - MsgIsReqDelData, - MsgIsReqDelMeta, - MsgIsReqDelWAL, - MsgIsReqGestalt, - MsgIsReqGetData, - MsgIsReqGetMeta, - MsgIsReqGetWAL, - MsgIsReqPutData, - MsgIsReqPutMeta, - MsgIsReqPutWAL, - MsgIsReqSubscribeMeta, - ReqDelMeta, - ReqGetMeta, - ReqOptRes, - ReqPutMeta, - ReqRes, - ResDelMeta, - ResGetMeta, - ResPutMeta, -} from "./msg-types.js"; -import { calculatePreSignedUrl } from "./pre-signed-url.js"; -import { SuperThis } from "@fireproof/core"; - -export type WithErrorMsg = T | ErrorMsg; - -export interface CtxBase { - readonly logger: Logger; -} - -export interface ReqOptResCtx extends ReqOptRes { - readonly ctx?: C; -} - -export interface ReqResCtx extends ReqRes { - readonly ctx: C; -} - -export interface MsgProcessor { - dispatch( - decodeFn: () => Promise - ): Promise, O>>; - - // signedUrl(req: ReqSignedUrl, ctx: CtxBase): Promise>; - // subscribeMeta(req: ReqSubscribeMeta, ctx: CtxBase): Promise>; - - // delMeta(req: ReqDelMeta, ctx: CtxBase): Promise>; - // putMeta(req: ReqPutMeta, ctx: CtxBase): Promise>; - // getMeta(req: ReqGetMeta, ctx: CtxBase): Promise>; -} - -export interface RequestOpts { - readonly waitFor: (msg: MsgBase) => boolean; - readonly timeout?: number; // ms -} -// export interface Connection { -// readonly ws: WebSocket; -// readonly key: ConnectionKey; -// request(msg: MsgBase, opts: RequestOpts): Promise>; -// onMessage(msgFn: (msg: MsgBase) => void): () => void; -// close(): Promise; -// } - -export abstract class MsgProcessorBase implements MsgProcessor { - readonly logger: Logger; - readonly serverId: string; - readonly ctx: O; - readonly sthis: SuperThis; - constructor(sthis: SuperThis, logger: Logger, ctx: O, serverId: string) { - this.serverId = serverId; - this.logger = logger; - this.ctx = ctx; - this.sthis = sthis; - } - - async dispatch( - decodeFn: () => Promise, - reqFn: (msg: Q, ctx: O) => Promise> = async (req) => ({ req }) - ): Promise> { - const rReqMsg = await exception2Result(async () => (await decodeFn()) as Q); - if (rReqMsg.isErr()) { - const errMsg = buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, rReqMsg.Err()); - return { - req: errMsg as unknown as Q, - res: errMsg, - ctx: this.ctx, - }; - } - const { req, ctx: optCtx } = await reqFn(rReqMsg.Ok() as Q, this.ctx); - const ctx = { ...(optCtx || this.ctx) }; - switch (true) { - case MsgIsReqGestalt(req): - return { - req, - res: buildResGestalt(req, defaultGestalt(this.serverId, true)) as S | ErrorMsg, - ctx, - }; - - case MsgIsReqGetData(req): - case MsgIsReqGetWAL(req): - return { - req, - res: (await this.signedUrl( - { - ...req, - params: { - ...req.params, - method: "GET", - store: getStoreFromType(req).store, - }, - }, - ctx - )) as S | ErrorMsg, - ctx, - }; - - case MsgIsReqPutData(req): - case MsgIsReqPutWAL(req): - if (req.payload) { - return { - req, - res: buildErrorMsg(this.logger, req, new Error("inband payload not implemented")) as S | ErrorMsg, - ctx, - }; - } - return { - req, - res: (await this.signedUrl( - { - ...req, - params: { - ...req.params, - method: "PUT", - store: getStoreFromType(req).store, - }, - }, - ctx - )) as S | ErrorMsg, - ctx, - }; - - case MsgIsReqDelData(req): - case MsgIsReqDelWAL(req): - return { - req, - res: (await this.signedUrl( - { - ...req, - params: { - ...req.params, - method: "DELETE", - store: getStoreFromType(req).store, - }, - }, - ctx - )) as S | ErrorMsg, - ctx, - }; - - // case MsgIsReqSignedUrl(req): - // return { - // req, - // res: (await this.signedUrl(req, ctx)) as S | ErrorMsg, - // ctx, - // }; - case MsgIsReqSubscribeMeta(req): - return { - req, - res: (await this.subscribeMeta(req, ctx)) as S | ErrorMsg, - ctx, - }; - case MsgIsReqPutMeta(req): - return { - req, - res: (await this.putMeta(req, ctx)) as S | ErrorMsg, - ctx, - }; - case MsgIsReqGetMeta(req): - return { - req, - res: (await this.getMeta(req, ctx)) as S | ErrorMsg, - ctx, - }; - case MsgIsReqDelMeta(req): - return { - req, - res: (await this.delMeta(req, ctx)) as S | ErrorMsg, - ctx, - }; - } - return { - req: req, - res: buildErrorMsg(this.logger, req, new Error(`unknown msg.type=${req.type}`)) as S | ErrorMsg, - ctx, - }; - } - - async delMeta(req: ReqDelMeta, ctx: CFCtxWithGroup): Promise { - // delete meta does nothing in this implementation - // if you delete meta basically you are deleting the whole ledger - return buildResDelMeta(req, { - params: req.params, - status: "unsupported", - connId: ctx.group.connId, - }); - } - - async getMeta(req: ReqGetMeta, ctx: CF): Promise { - const rSignedUrl = await calculatePreSignedUrl( - { - tid: req.tid, - type: "reqSignedUrl", - version: req.version, - params: { ...req.params, method: "GET" }, - }, - ctx.env - ); - if (rSignedUrl.isErr()) { - return buildErrorMsg(this.logger, req, rSignedUrl.Err()); - } - return buildResGetMeta(req, { - signedGetUrl: rSignedUrl.Ok().toString(), - status: "found", - metas: [], - connId: "", - }); - } - - async putMeta(req: ReqPutMeta, ctx: CtxHasGroup): Promise { - const rSignedUrl = await calculatePreSignedUrl( - { - tid: req.tid, - type: "reqSignedUrl", - version: req.version, - params: { ...req.params, method: "PUT" }, - }, - ctx.env - ); - if (rSignedUrl.isErr()) { - return buildErrorMsg(this.logger, req, rSignedUrl.Err()); - } - // roughly time ordered - return buildResPutMeta(req, { - // metaId should be a hash of metas. - metaId: new Date().getTime().toString(), - metas: req.metas, - signedPutUrl: rSignedUrl.Ok().toString(), - connId: ctx.group.connId, - }); - } -} diff --git a/src/fp-cloud/msg-raw-connection-base.ts b/src/fp-cloud/msg-raw-connection-base.ts deleted file mode 100644 index 66bf75e1..00000000 --- a/src/fp-cloud/msg-raw-connection-base.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Logger } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; -import { MsgBase, ErrorMsg, buildErrorMsg } from "./msg-types.js"; -import { ExchangedGestalt, OnErrorFn, UnReg } from "./msger.js"; - -export class MsgRawConnectionBase { - readonly sthis: SuperThis; - readonly exchangedGestalt: ExchangedGestalt; - - constructor(sthis: SuperThis, exGestalt: ExchangedGestalt) { - this.sthis = sthis; - this.exchangedGestalt = exGestalt; - } - - readonly onErrorFns = new Map(); - onError(fn: OnErrorFn): UnReg { - const key = this.sthis.nextId().str; - this.onErrorFns.set(key, fn); - return () => this.onErrorFns.delete(key); - } - - buildErrorMsg(logger: Logger, msg: Partial, err: Error): ErrorMsg { - // const logLine = this.sthis.logger.Error().Err(err).Any("msg", msg); - const rmsg = Array.from(this.onErrorFns.values()).reduce((msg, fn) => { - return fn(msg, err); - }, msg); - const emsg = buildErrorMsg(this.sthis, logger, rmsg, err); - logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); - return emsg; - } -} diff --git a/src/fp-cloud/msg-request.ts b/src/fp-cloud/msg-request.ts deleted file mode 100644 index 51facafe..00000000 --- a/src/fp-cloud/msg-request.ts +++ /dev/null @@ -1,220 +0,0 @@ -// import { Future, Logger, exception2Result, Result, CoerceURI, KeyedResolvOnce, URI } from "@adviser/cement"; -// import { SuperThis, } from "@fireproof/core"; -// import { RequestOpts, } from "./msg-processor.js"; -// import { MsgBase, AuthType, Gestalt, defaultGestalt, ResGestalt, ReqGestalt, MsgIsResGestalt, MsgIsError, } from "./msg-types.js"; - -// import * as json from 'multiformats/codecs/json'; -// import * as cborg from '@fireproof/vendor/cborg'; -// import { MsgConnection } from "./msger.js"; - -// export interface EnDeCoder { -// encode(node: T): Uint8Array; -// decode(data: Uint8Array): T; -// } - -// export interface WaitForTid { -// readonly tid: string; -// readonly future: Future; -// // undefined match all -// readonly waitFor: (msg: MsgBase) => boolean; -// } - -// export interface FetchGestaltParams { -// readonly auth?: AuthType; -// readonly sthis: SuperThis; -// readonly gestaltURL: URI; -// readonly uniqServerId?: string; -// readonly getConn: () => Promise; -// } - -// export interface HttpConnectionParams { -// readonly gestaltURL: CoerceURI; -// readonly fetchConnection?: MsgConnection; -// readonly ende?: EnDeCoder; -// readonly uniqServerId?: string; -// } - -// export type RequestFN = (req: Q, opts: RequestOpts) => Promise> - -// const serverId = "FP-Universal-Client" - -// export function encoded(logger: Logger, g: "JSON" | "CBOR") { -// let ende: EnDeCoder -// let mime: string -// switch (g) { -// case "JSON": -// ende = json -// mime = "application/json" -// break; -// case "CBOR": -// ende = cborg -// mime = "application/cbor" -// break; -// default: -// throw logger.Error().Str("typ", g).Msg(`Unknown encoding: ${g}`).AsError() -// } -// return { ende, mime } -// } - -// const getGestalts = new KeyedResolvOnce(); - -// async function fetchGestalt(fgp: FetchGestaltParams): Promise { -// return getGestalts.get(fgp.gestaltURL.toString()).once(async () => { -// const conn = await fgp.getConn(); -// const rGestalt = await conn.request({ -// type: "reqGestalt", -// tid: fgp.sthis.nextId().str, -// version: serverId, -// gestalt: defaultGestalt({ id: fgp.uniqServerId || serverId }), -// }, { waitFor: MsgIsResGestalt }); -// if (MsgIsError(rGestalt)) { -// throw Error(rGestalt.message) -// } -// const gestalt = rGestalt.gestalt -// const ende = encoded(fgp.sthis.logger, gestalt.encodings[0]) -// return { -// ende: ende.ende, -// mime: ende.mime, -// auth: gestalt.auth, -// gestalt: gestalt -// } -// }) -// } - -// export function selectRandom(arr: T[]): T { -// return arr[Math.floor(Math.random() * arr.length)]; -// } - -// export interface MsgErrorClose { -// readonly msgFn: (msg: MessageEvent) => void; -// readonly errFn: (err: Event) => void; -// readonly closeFn: () => void; -// readonly openFn: () => void; -// } - -// export interface GestaltParams { -// readonly auth?: AuthType; -// readonly sthis: SuperThis; -// readonly gestaltURL: URI; -// readonly uniqServerId?: string; -// } - -// const keyedHttpConnection = new KeyedResolvOnce(); -// function httpFactory(sthis: SuperThis, uniqServerId: string, auth?: AuthType): (() => Promise) { -// return () => keyedHttpConnection.get(uniqServerId || serverId).once(async () => { -// return new HttpConnection(sthis, { -// ende: json, -// mime: "application/json", -// auth: auth, -// params: defaultGestalt(uniqServerId || serverId, false), -// }) -// }) -// } - -// const keyedWSConnection = new KeyedResolvOnce(); - -// export interface Attachable { -// attach(t: T): Promise -// } - -// export class WSAttachable implements Attachable { -// readonly gestalt: MsgerParams -// readonly sthis: SuperThis -// readonly waitForTid = new Map(); -// constructor(sthis: SuperThis, gestalt: MsgerParams) { -// this.gestalt = gestalt -// this.sthis = sthis -// } -// attach(t: WebSocket): Promise { -// return keyedWSConnection.get(this.gestalt.params.id).once(async () => { -// const c = new WSAttachConnection(this.sthis, t, this.waitForTid, { -// openFn: () => this.open(t), -// errFn: (err) => this.error(t, err), -// msgFn: (msg) => this.msg(t, msg), -// closeFn: () => this.close(t) -// }) -// return c -// }) -// } - -// open(ws: WebSocket) { -// this.sthis.logger.Info().Msg("open") -// } - -// error(ws: WebSocket, err: Event) { -// this.sthis.logger.Error().Msg("error") - -// } -// msg(ws: WebSocket, msg: MessageEvent) { -// this.sthis.logger.Info().Any("msg", msg).Msg("msg") -// ws.onmessage = async (event) => { -// const rMsg = await exception2Result(() => JSON.parse(event.data) as MsgBase); -// if (rMsg.isErr()) { -// this.logger.Error().Err(rMsg).Any(event.data).Msg("Invalid message"); -// return; -// } -// const msg = rMsg.Ok(); -// const waitFor = this.waitForTid.get(msg.tid); -// if (waitFor) { -// if (MsgIsError(msg)) { -// this.msgCallbacks.forEach((cb) => cb(msg)); -// this.waitForTid.delete(msg.tid); -// waitFor.future.resolve(msg); -// } else if (waitFor.type) { -// // what for a specific type -// if (waitFor.type === msg.type) { -// this.msgCallbacks.forEach((cb) => cb(msg)); -// this.waitForTid.delete(msg.tid); -// waitFor.future.resolve(msg); -// } else { -// this.msgCallbacks.forEach((cb) => cb(msg)); -// } -// } else { -// // wild-card -// this.msgCallbacks.forEach((cb) => cb(msg)); -// this.waitForTid.delete(msg.tid); -// waitFor.future.resolve(msg); -// } -// } else { -// this.msgCallbacks.forEach((cb) => cb(msg)); -// } -// }; -// } - -// close(ws: WebSocket) { -// this.sthis.logger.Info().Msg("close") -// } - -// // this.params = params; - -// } - -// export async function getAttachable(p: FetchGestaltParams): Promise> { -// const g = await fetchGestalt({ -// gestaltURL: p.gestaltURL, -// sthis: p.sthis, -// getConn: httpFactory(p.sthis, p.uniqServerId || serverId, p.auth), -// }) -// if (g.params.wsEndpoints.length > 0) { -// return new WSAttachable(p.sthis, g) as Attachable -// } -// return { -// attach: async () => new HttpConnection(p.sthis, g) -// } -// } - -// export class ConnectionImpl implements Connection { -// readonly sthis: SuperThis; -// constructor(sthis: SuperThis) { -// this.sthis = sthis; -// } - -// async request(req: Q, opts: RequestOpts): Promise> { - -// } - -// } - -// export class ConnectionImpl implements Connection { - -// } diff --git a/src/fp-cloud/msg-type-meta.ts b/src/fp-cloud/msg-type-meta.ts deleted file mode 100644 index 80c2201d..00000000 --- a/src/fp-cloud/msg-type-meta.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Logger, VERSION } from "@adviser/cement"; -import { CRDTEntry } from "@fireproof/core"; -import { - GwCtx, - MsgBase, - MsgWithConn, - MsgWithOptionalConn, - MsgWithTenantLedger, - NextId, - ReqSignedUrlParam, - ResOptionalSignedUrl, -} from "./msg-types.js"; - -/* Put Meta */ -export interface ReqPutMeta extends MsgWithTenantLedger { - readonly type: "reqPutMeta"; - readonly params: ReqSignedUrlParam; - readonly metas: CRDTEntry[]; -} - -export interface ResPutMeta extends MsgWithTenantLedger, QSMeta { - readonly type: "resPutMeta"; -} - -export function buildReqPutMeta( - sthis: NextId, - signedUrlParams: ReqSignedUrlParam, - metas: CRDTEntry[], - gwCtx: GwCtx -): ReqPutMeta { - return { - tid: sthis.nextId().str, - type: "reqPutMeta", - ...gwCtx, - version: VERSION, - params: signedUrlParams, - metas, - }; -} - -export function MsgIsReqPutMeta(msg: MsgBase): msg is ReqPutMeta { - return msg.type === "reqPutMeta"; -} - -export function buildResPutMeta( - _sthis: NextId, - _logger: Logger, - req: MsgWithTenantLedger>, - meta: QSMeta -): ResPutMeta { - return { - ...meta, - tid: req.tid, - conn: req.conn, - tenant: req.tenant, - type: "resPutMeta", - // key: req.key, - version: VERSION, - }; -} - -export function MsgIsResPutMeta(qs: MsgBase): qs is ResPutMeta { - return qs.type === "resPutMeta"; -} - -/* Bind Meta */ -export interface BindGetMeta extends MsgWithTenantLedger { - readonly type: "bindGetMeta"; - readonly params: ReqSignedUrlParam; -} - -export function MsgIsBindGetMeta(msg: MsgBase): msg is BindGetMeta { - return msg.type === "bindGetMeta"; -} - -export interface QSMeta extends ResOptionalSignedUrl { - readonly metas: CRDTEntry[]; - readonly keys?: string[]; -} - -export interface EventGetMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { - readonly type: "eventGetMeta"; -} - -export function buildBindGetMeta(sthis: NextId, params: ReqSignedUrlParam, gwCtx: GwCtx): BindGetMeta { - return { - tid: sthis.nextId().str, - ...gwCtx, - type: "bindGetMeta", - version: VERSION, - params, - }; -} - -export function buildEventGetMeta( - _sthis: NextId, - _logger: Logger, - req: MsgWithTenantLedger>, - metaParam: QSMeta, - gwCtx: GwCtx -): EventGetMeta { - return { - ...metaParam, - ...gwCtx, - tid: req.tid, - type: "eventGetMeta", - params: { ...req.params, method: "GET", store: "meta" }, - version: VERSION, - }; -} - -export function MsgIsEventGetMeta(qs: MsgBase): qs is EventGetMeta { - return qs.type === "eventGetMeta"; -} - -/* Del Meta */ -export interface ReqDelMeta extends MsgWithTenantLedger { - readonly type: "reqDelMeta"; - readonly params: ReqSignedUrlParam; -} - -export function buildReqDelMeta(sthis: NextId, signedUrlParams: ReqSignedUrlParam, gwCtx: GwCtx): ReqDelMeta { - return { - tid: sthis.nextId().str, - ...gwCtx, - type: "reqDelMeta", - version: VERSION, - params: signedUrlParams, - }; -} - -export function MsgIsReqDelMeta(msg: MsgBase): msg is ReqDelMeta { - return msg.type === "reqDelMeta"; -} - -export interface ResDelMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { - readonly type: "resDelMeta"; -} - -export function buildResDelMeta( - _sthis: NextId, - _logger: Logger, - req: MsgWithTenantLedger>, - signedUrl?: string -): ResDelMeta { - return { - params: { ...req.params, method: "DELETE", store: "meta" }, - signedUrl, - tid: req.tid, - conn: req.conn, - tenant: req.tenant, - type: "resDelMeta", - // key: req.key, - version: VERSION, - }; -} - -export function MsgIsResDelMeta(qs: MsgBase): qs is ResDelMeta { - return qs.type === "resDelMeta"; -} diff --git a/src/fp-cloud/msg-types-data.ts b/src/fp-cloud/msg-types-data.ts deleted file mode 100644 index 12ceeb77..00000000 --- a/src/fp-cloud/msg-types-data.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Logger, Result, URI } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; -import { - ReqSignedUrl, - NextId, - MsgBase, - ResSignedUrl, - MsgWithError, - buildRes, - ReqSignedUrlParam, - buildReqSignedUrl, - GwCtx, - MsgIsTenantLedger, - MsgWithConn, -} from "./msg-types.js"; -import { PreSignedMsg } from "./pre-signed-url.js"; - -export interface ReqGetData extends ReqSignedUrl { - readonly type: "reqGetData"; -} - -export function buildReqGetData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqGetData { - return buildReqSignedUrl(sthis, "reqGetData", sup, ctx); -} - -export function MsgIsReqGetData(msg: MsgBase): msg is ReqGetData { - return msg.type === "reqGetData"; -} - -export interface ResGetData extends ResSignedUrl { - readonly type: "resGetData"; - // readonly payload: Uint8Array; // transfered via JSON base64 -} - -export function MsgIsResGetData(msg: MsgBase): msg is ResGetData { - return msg.type === "resGetData" && MsgIsTenantLedger(msg); -} - -export interface CalculatePreSignedUrl { - calculatePreSignedUrl(p: PreSignedMsg): Promise>; -} - -export function buildResGetData( - sthis: SuperThis, - logger: Logger, - req: MsgWithConn, - ctx: CalculatePreSignedUrl -): Promise> { - return buildRes, ResGetData>("GET", "data", "resGetData", sthis, logger, req, ctx); -} - -export interface ReqPutData extends ReqSignedUrl { - readonly type: "reqPutData"; - // readonly payload: Uint8Array; // transfered via JSON base64 -} - -export function MsgIsReqPutData(msg: MsgBase): msg is ReqPutData { - return msg.type === "reqPutData"; -} - -export function buildReqPutData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqPutData { - return buildReqSignedUrl(sthis, "reqPutData", sup, ctx); -} - -export interface ResPutData extends ResSignedUrl { - readonly type: "resPutData"; -} - -export function MsgIsResPutData(msg: MsgBase): msg is ResPutData { - return msg.type === "resPutData"; -} - -export function buildResPutData( - sthis: SuperThis, - logger: Logger, - req: MsgWithConn, - ctx: CalculatePreSignedUrl -): Promise> { - return buildRes, ResPutData>("PUT", "data", "resPutData", sthis, logger, req, ctx); -} - -export interface ReqDelData extends ReqSignedUrl { - readonly type: "reqDelData"; -} - -export function MsgIsReqDelData(msg: MsgBase): msg is ReqDelData { - return msg.type === "reqDelData"; -} - -export function buildReqDelData(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqDelData { - return buildReqSignedUrl(sthis, "reqDelData", sup, ctx); -} - -export interface ResDelData extends ResSignedUrl { - readonly type: "resDelData"; -} - -export function MsgIsResDelData(msg: MsgBase): msg is ResDelData { - return msg.type === "resDelData"; -} - -export function buildResDelData( - sthis: SuperThis, - logger: Logger, - req: MsgWithConn, - ctx: CalculatePreSignedUrl -): Promise> { - return buildRes, ResDelData>("DELETE", "data", "resDelData", sthis, logger, req, ctx); -} diff --git a/src/fp-cloud/msg-types-wal.ts b/src/fp-cloud/msg-types-wal.ts deleted file mode 100644 index 3363cd43..00000000 --- a/src/fp-cloud/msg-types-wal.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Logger } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; -import { - MsgBase, - MsgWithError, - buildRes, - NextId, - ReqSignedUrl, - ResSignedUrl, - ReqSignedUrlParam, - buildReqSignedUrl, - GwCtx, - MsgIsTenantLedger, - MsgWithTenantLedger, - MsgWithConn, -} from "./msg-types.js"; -import { CalculatePreSignedUrl } from "./msg-types-data.js"; - -export interface ReqGetWAL extends ReqSignedUrl { - readonly type: "reqGetWAL"; -} - -export function MsgIsReqGetWAL(msg: MsgBase): msg is ReqGetWAL { - return msg.type === "reqGetWAL"; -} - -export function buildReqGetWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqGetWAL { - return buildReqSignedUrl(sthis, "reqGetWAL", sup, ctx); -} - -export interface ResGetWAL extends ResSignedUrl { - readonly type: "resGetWAL"; - // readonly payload: Uint8Array; // transfered via JSON base64 -} - -export function MsgIsResGetWAL(msg: MsgBase): msg is ResGetWAL { - return msg.type === "resGetWAL"; -} - -export function buildResGetWAL( - sthis: SuperThis, - logger: Logger, - req: MsgWithTenantLedger>, - ctx: CalculatePreSignedUrl -): Promise> { - return buildRes>, ResGetWAL>( - "GET", - "wal", - "resGetWAL", - sthis, - logger, - req, - ctx - ); -} - -export interface ReqPutWAL extends Omit { - readonly type: "reqPutWAL"; - // readonly payload: Uint8Array; // transfered via JSON base64 -} - -export function MsgIsReqPutWAL(msg: MsgBase): msg is ReqPutWAL { - return msg.type === "reqPutWAL"; -} - -export function buildReqPutWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqPutWAL { - return buildReqSignedUrl(sthis, "reqPutWAL", sup, ctx); -} - -export interface ResPutWAL extends Omit { - readonly type: "resPutWAL"; -} - -export function MsgIsResPutWAL(msg: MsgBase): msg is ResPutWAL { - return msg.type === "resPutWAL"; -} - -export function buildResPutWAL( - sthis: SuperThis, - logger: Logger, - req: MsgWithTenantLedger>, - ctx: CalculatePreSignedUrl -): Promise> { - return buildRes>, ResPutWAL>( - "PUT", - "wal", - "resPutWAL", - sthis, - logger, - req, - ctx - ); -} - -export interface ReqDelWAL extends Omit { - readonly type: "reqDelWAL"; -} - -export function MsgIsReqDelWAL(msg: MsgBase): msg is ReqDelWAL { - return msg.type === "reqDelWAL"; -} - -export function buildReqDelWAL(sthis: NextId, sup: ReqSignedUrlParam, ctx: GwCtx): ReqDelWAL { - return buildReqSignedUrl(sthis, "reqDelWAL", sup, ctx); -} - -export interface ResDelWAL extends Omit { - readonly type: "resDelWAL"; -} - -export function MsgIsResDelWAL(msg: MsgBase): msg is ResDelWAL { - return msg.type === "resDelWAL" && MsgIsTenantLedger(msg); -} - -export function buildResDelWAL( - sthis: SuperThis, - logger: Logger, - req: MsgWithTenantLedger>, - ctx: CalculatePreSignedUrl -): Promise> { - return buildRes>, ResDelWAL>( - "DELETE", - "wal", - "resDelWAL", - sthis, - logger, - req, - ctx - ); -} diff --git a/src/fp-cloud/msg-types.ts b/src/fp-cloud/msg-types.ts deleted file mode 100644 index 43664e83..00000000 --- a/src/fp-cloud/msg-types.ts +++ /dev/null @@ -1,567 +0,0 @@ -import { Future } from "@adviser/cement"; -import { Logger, SuperThis } from "@fireproof/core"; -import { CalculatePreSignedUrl } from "./msg-types-data.js"; -import { PreSignedMsg } from "./pre-signed-url.js"; - -export const VERSION = "FP-MSG-1.0"; - -export type MsgWithError = T | ErrorMsg; - -export interface RequestOpts { - readonly waitFor: (msg: MsgBase) => boolean; - readonly pollInterval?: number; // 1000ms - readonly timeout?: number; // ms -} - -export interface EnDeCoder { - encode(node: T): Uint8Array; - decode(data: Uint8Array): T; -} - -export interface WaitForTid { - readonly tid: string; - readonly future: Future; - readonly timeout?: number; - // undefined match all - readonly waitFor: (msg: MsgBase) => boolean; -} - -// export interface ConnId { -// readonly connId: string; -// } -// type AddConnId = Omit & ConnId & { readonly type: N }; -export interface NextId { - readonly nextId: SuperThis["nextId"]; -} - -export interface AuthType { - readonly type: "ucan"; -} - -export interface UCanAuth { - readonly type: "ucan"; - readonly params: { - readonly tbd: string; - }; -} - -export interface TenantLedger { - readonly tenant: string; - readonly ledger: string; -} - -export function keyTenantLedger(t: TenantLedger): string { - return `${t.tenant}:${t.ledger}`; -} - -export interface QSId { - readonly reqId: string; - readonly resId: string; -} - -// export interface Connection extends ReqResId{ -// readonly key: TenantLedger; -// } - -// export interface Connected { -// readonly conn: Connection; -// } - -export interface MsgBase { - readonly tid: string; - readonly type: string; - readonly version: string; - readonly auth?: AuthType; -} - -export function MsgIsTid(msg: MsgBase, tid: string): boolean { - return msg.tid === tid; -} - -export type MsgWithConn = T & { readonly conn: QSId }; - -export type MsgWithOptionalConn = T & { readonly conn?: QSId }; - -export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; - -export interface ErrorMsg extends MsgBase { - readonly type: "error"; - readonly src: unknown; - readonly message: string; - readonly body?: string; - readonly stack?: string[]; -} - -export function MsgIsError(rq: MsgBase): rq is ErrorMsg { - return rq.type === "error"; -} - -export function MsgIsQSError(rq: ReqRes): rq is ReqRes { - return rq.res.type === "error" || rq.req.type === "error"; -} - -export type HttpMethods = "GET" | "PUT" | "DELETE"; -export type FPStoreTypes = "meta" | "data" | "wal"; - -// reqRes is http -// stream is WebSocket -export type ProtocolCapabilities = "reqRes" | "stream"; - -export interface Gestalt { - /** - * Describes StoreTypes which are handled - */ - readonly storeTypes: FPStoreTypes[]; - /** - * A unique identifier - */ - readonly id: string; - /** - * protocol capabilities - * defaults "stream" - */ - readonly protocolCapabilities: ProtocolCapabilities[]; - /** - * HttpEndpoints (URL) required atleast one - * could be absolute or relative - */ - readonly httpEndpoints: string[]; - /** - * WebsocketEndpoints (URL) required atleast one - * could be absolute or relative - */ - readonly wsEndpoints: string[]; - /** - * Encodings supported - * JSON, CBOR - */ - readonly encodings: ("JSON" | "CBOR")[]; - /** - * Authentication methods supported - */ - readonly auth: AuthType[]; - /** - * Requires Authentication - */ - readonly requiresAuth: boolean; - /** - * In|Outband Data | Meta | WAL Support - * Inband Means that the Payload is part of the message - * Outband Means that the Payload is PUT/GET to a different URL - * A Clien implementation usally not support reading or writing - * support - */ - readonly data?: { - readonly inband: boolean; - readonly outband: boolean; - }; - readonly meta?: { - readonly inband: true; // meta inband is mandatory - readonly outband: boolean; - }; - readonly wal?: { - readonly inband: boolean; - readonly outband: boolean; - }; - /** - * Request Types supported - * reqGestalt, reqSubscribeMeta, reqPutMeta, reqGetMeta, reqDelMeta, reqUpdateMeta - */ - readonly reqTypes: string[]; - /** - * Response Types supported - * resGestalt, resSubscribeMeta, resPutMeta, resGetMeta, resDelMeta, updateMeta - */ - readonly resTypes: string[]; - /** - * Event Types supported - * updateMeta - */ - readonly eventTypes: string[]; -} - -export interface MsgerParams { - readonly mime: string; - readonly auth?: AuthType; - readonly hasPersistent?: boolean; - readonly protocolCapabilities?: ProtocolCapabilities[]; - // readonly protocol: "http" | "ws"; - readonly timeout: number; // msec -} - -// force the server id -export type GestaltParam = Partial & { readonly id: string }; - -export function defaultGestalt(msgP: MsgerParams, gestalt: GestaltParam): Gestalt { - return { - storeTypes: ["meta", "data", "wal"], - httpEndpoints: ["/fp"], - wsEndpoints: ["/ws"], - encodings: ["JSON"], - protocolCapabilities: msgP.protocolCapabilities || ["reqRes", "stream"], - auth: [], - requiresAuth: false, - data: msgP.hasPersistent - ? { - inband: true, - outband: true, - } - : undefined, - meta: msgP.hasPersistent - ? { - inband: true, - outband: true, - } - : undefined, - wal: msgP.hasPersistent - ? { - inband: true, - outband: true, - } - : undefined, - reqTypes: [ - "reqOpen", - "reqGestalt", - // "reqSignedUrl", - "reqSubscribeMeta", - "reqPutMeta", - "reqGetMeta", - "reqDelMeta", - "reqPutData", - "reqGetData", - "reqDelData", - "reqPutWAL", - "reqGetWAL", - "reqDelWAL", - "reqUpdateMeta", - ], - resTypes: [ - "resOpen", - "resGestalt", - // "resSignedUrl", - "resSubscribeMeta", - "resPutMeta", - "resGetMeta", - "resDelMeta", - "resPutData", - "resGetData", - "resDelData", - "resPutWAL", - "resGetWAL", - "resDelWAL", - "updateMeta", - ], - eventTypes: ["updateMeta"], - ...gestalt, - }; -} - -/** - * The ReqGestalt message is used to request the - * features of the Responder. - */ -export interface ReqGestalt extends MsgBase { - readonly type: "reqGestalt"; - readonly gestalt: Gestalt; -} - -export function MsgIsReqGestalt(msg: MsgBase): msg is ReqGestalt { - return msg.type === "reqGestalt"; -} - -export function buildReqGestalt(sthis: NextId, gestalt: Gestalt): ReqGestalt { - return { - tid: sthis.nextId().str, - type: "reqGestalt", - version: VERSION, - gestalt, - }; -} - -/** - * The ResGestalt message is used to respond with - * the features of the Responder. - */ -export interface ResGestalt extends MsgBase { - readonly type: "resGestalt"; - readonly gestalt: Gestalt; -} - -export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt): ResGestalt | ErrorMsg { - return { - tid: req.tid, - type: "resGestalt", - version: VERSION, - gestalt, - }; -} - -export function MsgIsResGestalt(msg: MsgBase): msg is ResGestalt { - return msg.type === "resGestalt"; -} - -export interface ReqOpenConnection { - // readonly key: TenantLedger; - readonly reqId?: string; - readonly resId?: string; // for double open -} - -export interface ReqOpenConn { - readonly reqId: string; - readonly resId?: string; -} - -export interface ReqOpen extends MsgBase { - readonly type: "reqOpen"; - readonly conn: ReqOpenConn; -} - -export function buildReqOpen(sthis: NextId, conn: ReqOpenConnection): ReqOpen { - return { - tid: sthis.nextId().str, - type: "reqOpen", - version: VERSION, - conn: { - ...conn, - reqId: conn.reqId || sthis.nextId().str, - }, - }; -} - -export function MsgIsReqOpenWithConn(imsg: MsgBase): imsg is MsgWithConn { - const msg = imsg as MsgWithConn; - return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; -} - -export function MsgIsReqOpen(imsg: MsgBase): imsg is MsgWithConn { - const msg = imsg as MsgWithConn; - return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; -} - -export interface ResOpen extends MsgBase { - readonly type: "resOpen"; - readonly conn: QSId; -} - -export function MsgIsWithConn(msg: T): msg is MsgWithConn { - const mwc = (msg as MsgWithConn).conn; - return mwc && !!(mwc as QSId).reqId && !!(mwc as QSId).resId; -} - -export function MsgIsConnected(msg: T, qsid: QSId): msg is MsgWithConn { - return MsgIsWithConn(msg) && msg.conn.reqId === qsid.reqId && msg.conn.resId === qsid.resId; -} - -export function buildResOpen(sthis: NextId, req: ReqOpen, resStreamId?: string): ResOpen { - if (!(req.conn && req.conn.reqId)) { - throw new Error("req.conn.reqId is required"); - } - return { - ...req, - type: "resOpen", - conn: { - ...req.conn, - resId: req.conn.resId || resStreamId || sthis.nextId().str, - }, - }; -} - -export function MsgIsResOpen(msg: MsgBase): msg is ResOpen { - return msg.type === "resOpen"; -} - -export interface ReqClose extends Omit { - readonly type: "reqClose"; -} - -export function MsgIsReqClose(msg: MsgBase): msg is ReqClose { - return msg.type === "reqClose" && MsgIsWithConn(msg); -} - -export interface ResClose extends Omit { - readonly type: "resClose"; -} - -export function MsgIsResClose(msg: MsgBase): msg is ResClose { - return msg.type === "resClose" && MsgIsWithConn(msg); -} - -export interface SignedUrlParam { - readonly method: HttpMethods; - readonly store: FPStoreTypes; - // base path - readonly path?: string; - // name of the file - readonly key: string; - readonly expires?: number; // seconds - readonly index?: string; -} - -export type ReqSignedUrlParam = Omit; - -export interface UpdateReqRes { - req: Q; - res: S; -} - -export type ReqRes = Readonly>; - -// export interface ReqOptRes { -// readonly req: Q; -// readonly res?: S; -// } - -// /* Signed URL */ -// export function buildReqSignedUrl(req: ReqSignedUrlParam): ReqSignedUrlParam { -// return { -// tid: req.tid, -// params: { -// // protocol: "wss", -// ...req.params, -// }, -// }; -// } - -// export function MsgIsReqSignedUrl(msg: MsgBase): msg is ReqSignedUrl { -// return msg.type === "reqSignedUrl"; -// } - -// interface StoreAndType { -// readonly store: FPStoreTypes; -// readonly resType: string; -// } -// const reqToRes: Record = { -// reqGetData: { store: "data", resType: "resGetData" }, -// reqPutData: { store: "data", resType: "resPutData" }, -// reqDelData: { store: "data", resType: "resDelData" }, -// reqGetWAL: { store: "wal", resType: "resGetWAL" }, -// reqPutWAL: { store: "wal", resType: "resPutWAL" }, -// reqDelWAL: { store: "wal", resType: "resDelWAL" }, -// }; - -// export function getStoreFromType(req: MsgBase): StoreAndType { -// return ( -// reqToRes[req.type] || -// (() => { -// throw new Error(`unknown req.type=${req.type}`); -// })() -// ); -// } - -// export function buildResSignedUrl(req: ReqSignedUrl, signedUrl: string): ResSignedUrl { -// return { -// tid: req.tid, -// type: getStoreFromType(req).resType, -// version: VERSION, -// params: req.params, -// signedUrl, -// }; -// } - -export function buildErrorMsg( - sthis: SuperThis, - logger: Logger, - base: Partial, - error: Error, - body?: string, - stack?: string[] -): ErrorMsg { - if (!stack && sthis.env.get("FP_STACK")) { - stack = error.stack?.split("\n"); - } - const msg = { - src: base, - type: "error", - tid: base.tid || "internal", - message: error.message, - version: VERSION, - body, - stack, - } satisfies ErrorMsg; - logger.Any("ErrorMsg", msg); - return msg; -} - -// export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; - -export function MsgIsTenantLedger(msg: T): msg is MsgWithTenantLedger { - const t = (msg as MsgWithTenantLedger).tenant; - return !!t && !!t.tenant && !!t.ledger; -} - -export interface ReqSignedUrl extends MsgWithTenantLedger { - // readonly type: "reqSignedUrl"; - readonly params: ReqSignedUrlParam; -} - -export interface GwCtx { - readonly tid?: string; - readonly conn?: QSId; - readonly tenant: TenantLedger; -} - -export interface GwCtxConn { - readonly tid?: string; - readonly conn: QSId; - readonly tenant: TenantLedger; -} - -export function buildReqSignedUrl( - sthis: NextId, - type: string, - params: ReqSignedUrlParam, - gwCtx: GwCtx -): T { - return { - tid: sthis.nextId().str, - type, - version: VERSION, - ...gwCtx, - params, - } as T; -} - -export interface ResSignedUrl extends MsgWithTenantLedger { - // readonly type: "resSignedUrl"; - readonly params: SignedUrlParam; - readonly signedUrl: string; -} - -export interface ResOptionalSignedUrl extends MsgWithTenantLedger { - // readonly type: "resSignedUrl"; - readonly params: SignedUrlParam; - readonly signedUrl?: string; -} - -export async function buildRes>, S extends ResSignedUrl>( - method: SignedUrlParam["method"], - store: FPStoreTypes, - type: string, - sthis: SuperThis, - logger: Logger, - req: Q, - ctx: CalculatePreSignedUrl -): Promise> { - const psm = { - type: "reqSignedUrl", - version: req.version, - params: { - ...req.params, - method, - store, - }, - conn: req.conn, - tenant: req.tenant, - tid: req.tid, - } satisfies PreSignedMsg; - const rSignedUrl = await ctx.calculatePreSignedUrl(psm); - if (rSignedUrl.isErr()) { - return buildErrorMsg(sthis, logger, req, rSignedUrl.Err()); - } - return { - ...req, - params: psm.params, - type, - signedUrl: rSignedUrl.Ok().toString(), - } as unknown as MsgWithError; -} diff --git a/src/fp-cloud/msger.ts b/src/fp-cloud/msger.ts deleted file mode 100644 index d6ea4da2..00000000 --- a/src/fp-cloud/msger.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { BuildURI, CoerceURI, Result, runtimeFn, URI } from "@adviser/cement"; -import { - buildReqGestalt, - defaultGestalt, - EnDeCoder, - Gestalt, - MsgBase, - MsgerParams, - MsgIsResGestalt, - RequestOpts, - ResGestalt, - MsgWithError, - MsgWithConn, - buildReqOpen, - MsgIsConnected, - MsgIsError, - MsgIsResOpen, - MsgWithOptionalConn, - QSId, - MsgIsTid, - ReqGestalt, -} from "./msg-types.js"; -import { SuperThis } from "@fireproof/core"; -import { HttpConnection } from "./http-connection.js"; -import { WSConnection } from "./ws-connection.js"; - -// const headers = { -// "Content-Type": "application/json", -// "Accept": "application/json", -// }; - -export function selectRandom(arr: T[]): T { - return arr[Math.floor(Math.random() * arr.length)]; -} - -export function timeout(ms: number, promise: Promise): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error(`TIMEOUT after ${ms}ms`)); - }, ms); - promise - .then(resolve) - .catch(reject) - .finally(() => clearTimeout(timer)); - }); -} - -export type OnMsgFn = (msg: MsgWithError) => void; -export type UnReg = () => void; - -export interface ExchangedGestalt { - readonly my: Gestalt; - readonly remote: Gestalt; -} - -export type OnErrorFn = (msg: Partial, err: Error) => Partial; - -export interface ActiveStream { - readonly id: string; - readonly bind: { - readonly msg: Q; - readonly opts: RequestOpts; - }; - timeout?: unknown; - controller?: ReadableStreamDefaultController>; -} - -export interface MsgRawConnection { - // readonly ws: WebSocket; - // readonly params: ConnectionKey; - // qsOpen: ReqRes; - readonly sthis: SuperThis; - readonly exchangedGestalt: ExchangedGestalt; - readonly activeBinds: Map>; - bind(req: Q, opts: RequestOpts): ReadableStream>; - request(req: Q, opts: RequestOpts): Promise>; - start(): Promise>; - close(): Promise>; - onMsg(msg: OnMsgFn): UnReg; -} - -export function jsonEnDe(sthis: SuperThis): EnDeCoder { - return { - encode: (node: unknown) => sthis.txt.encode(JSON.stringify(node)), - decode: (data: Uint8Array) => JSON.parse(sthis.txt.decode(data)), - }; -} - -export type MsgerParamsWithEnDe = MsgerParams & { readonly ende: EnDeCoder }; - -export function defaultMsgParams(sthis: SuperThis, igs: Partial): MsgerParamsWithEnDe { - return { - mime: "application/json", - ende: jsonEnDe(sthis), - timeout: 3000, - protocolCapabilities: ["reqRes", "stream"], - ...igs, - } satisfies MsgerParamsWithEnDe; -} - -export interface OpenParams { - readonly timeout: number; -} - -export async function applyStart(prC: Promise>): Promise> { - const rC = await prC; - if (rC.isErr()) { - return rC; - } - const c = rC.Ok(); - const r = await c.start(); - if (r.isErr()) { - return Result.Err(r.Err()); - } - return rC; -} - -export class MsgConnected implements MsgRawConnection { - static async connect( - mrc: Result | MsgRawConnection, - conn: Partial = {} - ): Promise> { - if (Result.Is(mrc)) { - if (mrc.isErr()) { - return Result.Err(mrc.Err()); - } - mrc = mrc.Ok(); - } - const res = await mrc.request(buildReqOpen(mrc.sthis, conn), { waitFor: MsgIsResOpen }); - if (MsgIsError(res) || !MsgIsResOpen(res)) { - return mrc.sthis.logger.Error().Err(res).Msg("unexpected response").ResultError(); - } - return Result.Ok(new MsgConnected(mrc, res.conn)); - } - - readonly sthis: SuperThis; - readonly conn: QSId; - readonly raw: MsgRawConnection; - readonly exchangedGestalt: ExchangedGestalt; - readonly activeBinds: Map>; - private constructor(raw: MsgRawConnection, conn: QSId) { - this.sthis = raw.sthis; - this.raw = raw; - this.exchangedGestalt = raw.exchangedGestalt; - this.conn = conn; - this.activeBinds = raw.activeBinds; - } - - bind( - req: Q, - opts: RequestOpts - ): ReadableStream> { - const stream = this.raw.bind({ ...req, conn: req.conn || this.conn }, opts); - const ts = new TransformStream, MsgWithError>({ - transform: (chunk, controller) => { - if (!MsgIsTid(chunk, req.tid)) { - return; - } - if (MsgIsConnected(chunk, this.conn)) { - if ((opts.waitFor && opts.waitFor(chunk)) || MsgIsError(chunk)) { - controller.enqueue(chunk); - } - } - }, - }); - // eslint-disable-next-line no-console - // why the hell pipeTo sends an error that is undefined? - stream.pipeThrough(ts); - // stream.pipeTo(ts.writable).catch((err) => err && err.message && console.error("bind error", err)); - return ts.readable; - } - - request(req: Q, opts: RequestOpts): Promise> { - return this.raw.request({ ...req, conn: req.conn || this.conn }, opts); - } - start(): Promise> { - return this.raw.start(); - } - close(): Promise> { - return this.raw.close(); - } - onMsg(msgFn: OnMsgFn): UnReg { - return this.raw.onMsg((msg) => { - if (MsgIsConnected(msg, this.conn)) { - msgFn(msg); - } - }); - } -} - -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class Msger { - static async openHttp( - sthis: SuperThis, - // reqOpen: ReqOpen | undefined, - urls: URI[], - msgP: MsgerParamsWithEnDe, - exGestalt: ExchangedGestalt - ): Promise> { - return Result.Ok(new HttpConnection(sthis, urls, msgP, exGestalt)); - } - static async openWS( - sthis: SuperThis, - // qOpen: ReqOpen, - url: URI, - msgP: MsgerParamsWithEnDe, - exGestalt: ExchangedGestalt - ): Promise> { - let ws: WebSocket; - // const { encode } = jsonEnDe(sthis); - url = url.build().URI(); - // .setParam("reqOpen", sthis.txt.decode(encode(qOpen))) - if (runtimeFn().isNodeIsh) { - const { WebSocket } = await import("ws"); - ws = new WebSocket(url.toString()) as unknown as WebSocket; - } else { - ws = new WebSocket(url.toString()); - } - return Result.Ok(new WSConnection(sthis, ws, msgP, exGestalt)); - } - static async open( - sthis: SuperThis, - curl: CoerceURI, - imsgP: Partial = {} - ): Promise> { - // initial exchange with JSON encoding - const jsMsgP = defaultMsgParams(sthis, { ...imsgP, mime: "application/json", ende: jsonEnDe(sthis) }); - const url = URI.from(curl); - const gs = defaultGestalt(defaultMsgParams(sthis, imsgP), { id: "FP-Universal-Client" }); - /* - * request Gestalt with Http - */ - const rHC = await Msger.openHttp(sthis, [url], jsMsgP, { my: gs, remote: gs }); - if (rHC.isErr()) { - return rHC; - } - const hc = rHC.Ok(); - const resGestalt = await hc.request(buildReqGestalt(sthis, gs), { - waitFor: MsgIsResGestalt, - }); - if (!MsgIsResGestalt(resGestalt)) { - return Result.Err(new Error("Invalid Gestalt")); - } - await hc.close(); - const exGt = { my: gs, remote: resGestalt.gestalt } satisfies ExchangedGestalt; - const msgP = defaultMsgParams(sthis, imsgP); - if (exGt.remote.protocolCapabilities.includes("reqRes") && !exGt.remote.protocolCapabilities.includes("stream")) { - return applyStart( - Msger.openHttp( - sthis, - exGt.remote.httpEndpoints.map((i) => BuildURI.from(url).resolve(i).URI()), - msgP, - exGt - ) - ); - } - return applyStart( - Msger.openWS(sthis, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) - ); - } - - static connect( - sthis: SuperThis, - curl: CoerceURI, - imsgP: Partial = {}, - conn: Partial = {} - ): Promise> { - return Msger.open(sthis, curl, imsgP).then((srv) => MsgConnected.connect(srv, conn)); - } - - private constructor() { - /* */ - } -} diff --git a/src/fp-cloud/new-websocket.ts b/src/fp-cloud/new-websocket.ts deleted file mode 100644 index 1c8ef6fe..00000000 --- a/src/fp-cloud/new-websocket.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CoerceURI, runtimeFn, URI } from "@adviser/cement"; - -export async function newWebSocket(url: CoerceURI): Promise { - const wsUrl = URI.from(url).toString(); - if (runtimeFn().isNodeIsh) { - const { WebSocket: MyWS } = await import("ws"); - return new MyWS(wsUrl) as unknown as WebSocket; - } else { - return new WebSocket(wsUrl); - } -} diff --git a/src/fp-cloud/node-hono-server.ts b/src/fp-cloud/node-hono-server.ts deleted file mode 100644 index 53f75a1f..00000000 --- a/src/fp-cloud/node-hono-server.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { UpgradeWebSocket, WSContext, WSContextInit, WSEvents } from "hono/ws"; -import { ConnMiddleware, HonoServerBase, HonoServerFactory, HonoServerImpl, RunTimeParams } from "./hono-server.js"; -import { HttpHeader, URI } from "@adviser/cement"; -import { Context, Hono } from "hono"; -import { ensureLogger, SuperThis } from "@fireproof/core"; -import { defaultMsgParams, jsonEnDe } from "./msger.js"; -import { defaultGestalt, Gestalt, MsgerParams } from "./msg-types.js"; -import { SQLDatabase } from "./meta-merger/abstract-sql.js"; -import { WSRoom } from "./ws-room.js"; - -interface ServerType { - close(fn: () => void): void; -} - -type serveFn = (options: unknown, listeningListener?: ((info: unknown) => void) | undefined) => ServerType; - -export interface NodeHonoFactoryParams { - readonly msgP?: MsgerParams; - readonly gs?: Gestalt; - readonly sql: SQLDatabase; -} - -const wsConnections = new Map(); -class NodeWSRoom implements WSRoom { - readonly sthis: SuperThis; - constructor(sthis: SuperThis) { - this.sthis = sthis; - } - acceptConnection(ws: WebSocket, wse: WSEvents): Promise { - const id = this.sthis.nextId(12).str; - wsConnections.set(id, ws); - - const wsCtx = new WSContext(ws as WSContextInit); - - ws.onerror = (err) => { - // console.log("onerror", err); - wse.onError?.(err, wsCtx); - }; - ws.onclose = (ev) => { - // console.log("onclose", ev); - wse.onClose?.(ev, wsCtx); - }; - ws.onmessage = (evt) => { - // console.log("onmessage", evt); - // wsCtx.send("Hellox from server"); - wse.onMessage?.(evt, wsCtx); - }; - - ws.accept(); - return Promise.resolve(); - } -} - -export class NodeHonoFactory implements HonoServerFactory { - _upgradeWebSocket!: UpgradeWebSocket; - _injectWebSocket!: (t: unknown) => void; - _serve!: serveFn; - _server!: ServerType; - // _env!: Env; - - readonly sthis: SuperThis; - readonly params: NodeHonoFactoryParams; - constructor(sthis: SuperThis, params: NodeHonoFactoryParams) { - this.sthis = sthis; - this.params = params; - } - - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { - // this._env = c.env; - // const sthis = ensureSuperThis(); - const sthis = this.sthis; - const logger = ensureLogger(sthis, `NodeHono[${URI.from(c.req.url).pathname}]`); - const ende = jsonEnDe(sthis); - - const fpProtocol = sthis.env.get("FP_PROTOCOL"); - const msgP = - this.params.msgP ?? - defaultMsgParams(sthis, { - hasPersistent: true, - protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], - }); - const gs = - this.params.gs ?? - defaultGestalt(msgP, { - id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", - }); - const wsRoom = new NodeWSRoom(sthis); - const nhs = new NodeHonoServer(sthis, this, gs, this.params.sql, wsRoom); - return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs })); - } - - async start(app: Hono): Promise { - try { - const { createNodeWebSocket } = await import("@hono/node-ws"); - const { serve } = await import("@hono/node-server"); - this._serve = serve as serveFn; - const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app }); - this._upgradeWebSocket = upgradeWebSocket; - this._injectWebSocket = injectWebSocket as (t: unknown) => void; - } catch (e) { - throw this.sthis.logger.Error().Err(e).Msg("Failed to start NodeHonoFactory").AsError(); - } - } - - async serve(app: Hono, port: number): Promise { - await new Promise((resolve) => { - this._server = this._serve({ fetch: app.fetch, port }, () => { - this._injectWebSocket(this._server); - resolve(); - }); - }); - } - async close(): Promise { - this._server.close(() => { - /* */ - }); - // return new Promise((res) => this._server.close(() => res())); - } -} - -export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { - readonly _upgradeWebSocket: UpgradeWebSocket; - constructor( - sthis: SuperThis, - factory: NodeHonoFactory, - gs: Gestalt, - sqldb: SQLDatabase, - wsRoom: WSRoom, - headers?: HttpHeader - ) { - super(sthis, sthis.logger, gs, sqldb, wsRoom, headers); - this._upgradeWebSocket = factory._upgradeWebSocket; - } - - override upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { - return async (_conn, c, next) => { - // conn.attachWSPair({ client: c.req, server: c.res }); - return this._upgradeWebSocket(createEvents)(c, next); - }; - } -} diff --git a/src/fp-cloud/pre-signed-url.ts b/src/fp-cloud/pre-signed-url.ts deleted file mode 100644 index a5f25de7..00000000 --- a/src/fp-cloud/pre-signed-url.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Result, URI } from "@adviser/cement"; -import { AwsClient } from "aws4fetch"; -import { MsgWithConn, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; - -export interface PreSignedMsg extends MsgWithTenantLedger { - readonly params: SignedUrlParam; -} - -// export interface PreSignedConnMsg { -// readonly params: SignedUrlParam; -// readonly tid: string; -// readonly conn: QSId; -// } - -export interface PreSignedEnv { - readonly storageUrl: URI; - readonly aws: { - readonly accessKeyId: string; - readonly secretAccessKey: string; - readonly region?: string; - }; - readonly test?: { - readonly amzDate?: string; - }; -} - -export async function calculatePreSignedUrl(psm: PreSignedMsg, env: PreSignedEnv): Promise> { - // if (!ipsm.conn) { - // return Result.Err(new Error("Connection is not supported")); - // } - // const psm = ipsm as PreSignedConnMsg; - - // verify if you are not overriding - let store: string = psm.params.store; - if (psm.params.index?.length) { - store = `${store}-${psm.params.index}`; - } - const expiresInSeconds = psm.params.expires || 60 * 60; - - const suffix = ""; - // switch (psm.params.store) { - // case "wal": - // case "meta": - // suffix = ".json"; - // break; - // default: - // break; - // } - - const opUrl = env.storageUrl - .build() - // .protocol(vals.protocuol === "ws" ? "http:" : "https:") - .setParam("X-Amz-Expires", expiresInSeconds.toString()) - .setParam("tid", psm.tid) - .appendRelative(psm.tenant.tenant) - .appendRelative(psm.tenant.ledger) - .appendRelative(store) - .appendRelative(`${psm.params.key}${suffix}`) - .URI(); - const a4f = new AwsClient({ - ...env.aws, - region: env.aws.region || "us-east-1", - service: "s3", - }); - const signedUrl = await a4f - .sign( - new Request(opUrl.toString(), { - method: psm.params.method, - }), - { - aws: { - signQuery: true, - datetime: env.test?.amzDate, - // datetime: env.TEST_DATE, - }, - } - ) - .then((res) => res.url); - return Result.Ok(URI.from(signedUrl)); -} diff --git a/src/fp-cloud/test-helper.ts b/src/fp-cloud/test-helper.ts deleted file mode 100644 index 144ff493..00000000 --- a/src/fp-cloud/test-helper.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { Future, Result, URI } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; -import { $, fs } from "zx"; -import { HttpConnection } from "./http-connection.js"; -import { - MsgerParams, - Gestalt, - defaultGestalt, - buildReqGestalt, - MsgIsResGestalt, - MsgIsError, - MsgBase, -} from "./msg-types.js"; -import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection } from "./msger.js"; -import { WSConnection } from "./ws-connection.js"; -import * as toml from "smol-toml"; -import { Env } from "./backend/env.js"; -import { HonoServer } from "./hono-server.js"; -import { NodeHonoFactory } from "./node-hono-server.js"; -import { CFHonoFactory } from "./backend/cf-hono-server.js"; -import { BetterSQLDatabase } from "./meta-merger/bettersql-abstract-sql.js"; - -export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { - const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["reqRes"] }), { - id: "HTTP-server", - }); - const exGt = { my, remote }; - return { - name: "HTTP", - remoteGestalt: remote, - cInstance: HttpConnection, - ok: { - url: () => URI.from(`http://127.0.0.1:${port}/fp`), - open: () => - applyStart( - Msger.openHttp( - sthis, - [URI.from(`http://localhost:${port}/fp`)], - { - ...msgP, - // protocol: "http", - timeout: 1000, - }, - exGt - ) - ), - }, - connRefused: { - url: () => URI.from(`http://127.0.0.1:${port - 1}/fp`), - open: async (): Promise>> => { - const ret = await Msger.openHttp( - sthis, - [URI.from(`http://localhost:${port - 1}/fp`)], - { - ...msgP, - // protocol: "http", - timeout: 1000, - }, - exGt - ); - if (ret.isErr()) { - return ret; - } - // should fail - const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); - if (MsgIsError(res)) { - return Result.Err(res.message); - } - return ret; - }, - }, - timeout: { - url: () => URI.from(`http://4.7.1.1:${port}/fp`), - open: async (): Promise>> => { - const ret = await Msger.openHttp( - sthis, - [URI.from(`http://4.7.1.1:${port}/fp`)], - { - ...msgP, - // protocol: "http", - timeout: 500, - }, - exGt - ); - // should fail - const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); - if (MsgIsError(res)) { - return Result.Err(res.message); - } - return ret; - }, - }, - }; -} - -export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { - const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["stream"] }), { - id: "WS-server", - }); - const exGt = { my, remote }; - return { - name: "WS", - remoteGestalt: remote, - cInstance: WSConnection, - ok: { - url: () => URI.from(`http://127.0.0.1:${port}/ws`), - open: () => - applyStart( - Msger.openWS( - sthis, - URI.from(`http://localhost:${port}/ws`), - { - ...msgP, - // protocol: "ws", - timeout: 1000, - }, - exGt - ) - ), - }, - connRefused: { - url: () => URI.from(`http://127.0.0.1:${port - 1}/ws`), - open: () => - Msger.openWS( - sthis, - URI.from(`http://localhost:${port - 1}/ws`), - { - ...msgP, - // protocol: "ws", - timeout: 1000, - }, - exGt - ), - }, - timeout: { - url: () => URI.from(`http://4.7.1.1:${port - 1}/ws`), - open: () => - Msger.openWS( - sthis, - URI.from(`http://4.7.1.1:${port - 1}/ws`), - { - ...msgP, - // protocol: "ws", - timeout: 500, - }, - exGt - ), - }, - }; -} - -export async function resolveToml(backend: "D1" | "DO") { - const tomlFile = "src/cloud/backend/wrangler.toml"; - const tomeStr = await fs.readFile(tomlFile, "utf-8"); - const wranglerFile = toml.parse(tomeStr) as unknown as { - env: Record; - }; - return { - tomlFile, - env: wranglerFile.env[`test-reqRes-${backend}`].vars, - }; -} - -export function NodeHonoServerFactory() { - return { - name: "NodeHonoServer", - factory: async (sthis: SuperThis, msgP: MsgerParams, remoteGestalt: Gestalt, _port: number) => { - const { env } = await resolveToml("D1"); - sthis.env.sets(env as unknown as Record); - const nhf = new NodeHonoFactory(sthis, { - msgP, - gs: remoteGestalt, - sql: new BetterSQLDatabase("./dist/node-meta.sqlite"), - }); - return new HonoServer(nhf); - }, - }; -} -export function CFHonoServerFactory(backend: "D1" | "DO") { - return { - name: `CFHonoServer(${backend})`, - factory: async (_sthis: SuperThis, _msgP: MsgerParams, remoteGestalt: Gestalt, port: number) => { - if (process.env.FP_WRANGLER_PORT) { - return new HonoServer(new CFHonoFactory()); - } - const { tomlFile } = await resolveToml(backend); - $.verbose = !!process.env.FP_DEBUG; - const runningWrangler = $` - wrangler dev -c ${tomlFile} --port ${port} --env test-${remoteGestalt.protocolCapabilities[0]}-${backend} --no-show-interactive-dev-session & - waitPid=$! - echo "PID:$waitPid" - wait $waitPid`; - const waitReady = new Future(); - let pid: number | undefined; - runningWrangler.stdout.on("data", (chunk) => { - // console.log(">>", chunk.toString()) - const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; - if (mightPid) { - pid = +mightPid; - } - if (chunk.includes("Ready on http")) { - waitReady.resolve(true); - } - }); - runningWrangler.stderr.on("data", (chunk) => { - // eslint-disable-next-line no-console - console.error("!!", chunk.toString()); - }); - await waitReady.asPromise(); - return new HonoServer( - new CFHonoFactory(() => { - if (pid) process.kill(pid); - }) - ); - }, - }; -} diff --git a/src/fp-cloud/ws-connection.ts b/src/fp-cloud/ws-connection.ts deleted file mode 100644 index 5c29cfd7..00000000 --- a/src/fp-cloud/ws-connection.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { exception2Result, Future, Logger, Result } from "@adviser/cement"; -import { SuperThis, ensureLogger } from "@fireproof/core"; -import { - MsgBase, - MsgIsError, - buildErrorMsg, - ReqOpen, - WaitForTid, - MsgWithError, - RequestOpts, - MsgIsTid, -} from "./msg-types.js"; -import { ActiveStream, ExchangedGestalt, MsgerParamsWithEnDe, MsgRawConnection, OnMsgFn, UnReg } from "./msger.js"; -import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; - -export interface WSReqOpen { - readonly reqOpen: ReqOpen; - readonly ws: WebSocket; // this WS is opened with a specific URL-Param -} - -export class WSConnection extends MsgRawConnectionBase implements MsgRawConnection { - readonly logger: Logger; - readonly msgP: MsgerParamsWithEnDe; - readonly ws: WebSocket; - // readonly baseURI: URI; - - readonly #onMsg = new Map(); - readonly #onClose = new Map(); - - readonly waitForTid = new Map(); - - opened = false; - - readonly id: string; - - constructor(sthis: SuperThis, ws: WebSocket, msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { - super(sthis, exGestalt); - this.id = sthis.nextId().str; - this.logger = ensureLogger(sthis, "WSConnection"); - this.msgP = msgP; - this.ws = ws; - // this.wqs = { ...wsq }; - } - - async start(): Promise> { - const onOpenFuture: Future> = new Future>(); - const timer = setTimeout(() => { - const err = this.logger.Error().Dur("timeout", this.msgP.timeout).Msg("Timeout").AsError(); - this.toMsg(buildErrorMsg(this.sthis, this.logger, {} as MsgBase, err)); - onOpenFuture.resolve(Result.Err(err)); - }, this.msgP.timeout); - this.ws.onopen = () => { - onOpenFuture.resolve(Result.Ok(undefined)); - this.opened = true; - }; - this.ws.onerror = (ierr) => { - const err = this.logger.Error().Err(ierr).Msg("WS Error").AsError(); - onOpenFuture.resolve(Result.Err(err)); - const res = this.buildErrorMsg(this.logger.Error(), {}, err); - this.toMsg(res); - }; - this.ws.onmessage = (evt) => { - if (!this.opened) { - this.toMsg( - buildErrorMsg( - this.sthis, - this.logger, - {} as MsgBase, - this.logger.Error().Msg("Received message before onOpen").AsError() - ) - ); - } - this.#wsOnMessage(evt); - }; - this.ws.onclose = () => { - this.opened = false; - this.close().catch((ierr) => { - const err = this.logger.Error().Err(ierr).Msg("close error").AsError(); - onOpenFuture.resolve(Result.Err(err)); - this.toMsg(buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, err)); - }); - }; - /* wait for onOpen */ - const rOpen = await onOpenFuture.asPromise().finally(() => { - clearTimeout(timer); - }); - if (rOpen.isErr()) { - return rOpen; - } - // const resOpen = await this.request(this.wqs.reqOpen, { waitFor: MsgIsResOpen }); - // if (!MsgIsResOpen(resOpen)) { - // return Result.Err(this.logger.Error().Any("ErrMsg", resOpen).Msg("Invalid response").AsError()); - // } - // this.wqs.resOpen = resOpen; - return Result.Ok(undefined); - } - - readonly #wsOnMessage = async (event: MessageEvent) => { - const rMsg = await exception2Result(() => this.msgP.ende.decode(event.data) as MsgBase); - if (rMsg.isErr()) { - this.logger.Error().Err(rMsg).Any(event.data).Msg("Invalid message"); - return; - } - const msg = rMsg.Ok(); - const waitFor = this.waitForTid.get(msg.tid); - this.#onMsg.forEach((cb) => cb(msg)); - if (waitFor) { - if (MsgIsError(msg)) { - this.waitForTid.delete(msg.tid); - waitFor.future.resolve(msg); - } else if (waitFor.waitFor(msg)) { - // what for a specific type - this.waitForTid.delete(msg.tid); - waitFor.future.resolve(msg); - } else { - // wild-card - this.waitForTid.delete(msg.tid); - waitFor.future.resolve(msg); - } - } - }; - - async close(): Promise> { - this.#onClose.forEach((fn) => fn()); - this.#onClose.clear(); - this.#onMsg.clear(); - this.ws.close(); - return Result.Ok(undefined); - } - - toMsg(msg: MsgWithError): MsgWithError { - this.#onMsg.forEach((fn) => fn(msg)); - return msg; - } - - sendMsg(msg: MsgBase): Promise { - this.ws.send(this.msgP.ende.encode(msg)); - return Promise.resolve(); - } - - onMsg(fn: OnMsgFn): UnReg { - const key = this.sthis.nextId().str; - this.#onMsg.set(key, fn as OnMsgFn); - return () => this.#onMsg.delete(key); - } - - onClose(fn: UnReg): UnReg { - const key = this.sthis.nextId().str; - this.#onClose.set(key, fn); - return () => this.#onClose.delete(key); - } - - readonly activeBinds = new Map>(); - bind(req: Q, opts: RequestOpts): ReadableStream> { - const state: ActiveStream = { - id: this.sthis.nextId().str, - bind: { - msg: req, - opts, - }, - // timeout: undefined, - // controller: undefined, - } satisfies ActiveStream; - this.activeBinds.set(state.id, state); - return new ReadableStream>({ - cancel: () => { - // clearTimeout(state.timeout as number); - this.activeBinds.delete(state.id); - }, - start: (controller) => { - this.onMsg((msg) => { - if (MsgIsError(msg)) { - controller.enqueue(msg); - return; - } - if (!MsgIsTid(msg, req.tid)) { - return; - } - if (opts.waitFor && opts.waitFor(msg)) { - controller.enqueue(msg); - } - }); - this.sendMsg(req); - const future = new Future(); - this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); - future.asPromise().then((msg) => { - if (MsgIsError(msg)) { - // double err emitting - controller.enqueue(msg); - controller.close(); - } - }); - }, - }); - } - - async request(req: Q, opts: RequestOpts): Promise> { - if (!this.opened) { - return buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Msg("Connection not open").AsError()); - } - const future = new Future(); - this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); - await this.sendMsg(req); - return future.asPromise(); - } - - // toOnMessage(msg: WithErrorMsg): Result> { - // this.mec.msgFn?.(msg as unknown as MessageEvent); - // return Result.Ok(msg); - // } -} diff --git a/src/fp-cloud/ws-room.ts b/src/fp-cloud/ws-room.ts deleted file mode 100644 index ebe8e33b..00000000 --- a/src/fp-cloud/ws-room.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WSEvents } from "hono/ws"; - -export interface WSRoom { - acceptConnection(ws: WebSocket, wse: WSEvents): Promise; -} diff --git a/src/netlify/server.ts b/src/netlify/backend/server.ts similarity index 87% rename from src/netlify/server.ts rename to src/netlify/backend/server.ts index 754c1ede..33405f59 100644 --- a/src/netlify/server.ts +++ b/src/netlify/backend/server.ts @@ -1,14 +1,14 @@ import { getStore } from "@netlify/blobs"; -import { to_blob } from "../coerce-binary.js"; +import { to_blob } from "../../coerce-binary.js"; // eslint-disable-next-line no-console console.log("fireproof edge function loaded netlify"); -interface CRDTEntry { - readonly data: string; - readonly cid: string; - readonly parents: string[]; -} +// interface CRDTEntry { +// readonly data: string; +// readonly cid: string; +// readonly parents: string[]; +// } export default async (req: Request) => { // eslint-disable-next-line no-restricted-globals @@ -16,6 +16,10 @@ export default async (req: Request) => { const carId = url.searchParams.get("car"); const metaDb = url.searchParams.get("meta"); + if (url.searchParams.get("health")) { + return new Response(JSON.stringify({ ok: true }), { status: 200 }); + } + if (req.method === "PUT") { if (carId) { const carFiles = getStore("cars"); @@ -24,7 +28,10 @@ export default async (req: Request) => { return new Response(JSON.stringify({ ok: true }), { status: 201 }); } else if (metaDb) { const meta = getStore("meta"); - const x = (await req.json()) as CRDTEntry[]; + const bytes = new Uint8Array(await req.arrayBuffer()); + // eslint-disable-next-line no-restricted-globals + const str = new TextDecoder().decode(bytes); + const x = JSON.parse(str); // fixme, marty changed to [0] as it is a slice of the structure we expected const { data, cid, parents } = x[0]; await meta.setJSON(`${metaDb}/${cid}`, { data, parents }); diff --git a/src/netlify/gateway.ts b/src/netlify/gateway.ts index c1410571..85c9c0eb 100644 --- a/src/netlify/gateway.ts +++ b/src/netlify/gateway.ts @@ -32,7 +32,6 @@ export class NetlifyGateway implements bs.Gateway { if (index) { name += `-${index}`; } - name += ".fp"; const remoteBaseUrl = url.getParam("remoteBaseUrl"); if (!remoteBaseUrl) { return Result.Err(new Error("Remote base URL not found in the URI")); @@ -80,7 +79,6 @@ export class NetlifyGateway implements bs.Gateway { if (index) { name += `-${index}`; } - name += ".fp"; const remoteBaseUrl = url.getParam("remoteBaseUrl"); if (!remoteBaseUrl) { return Result.Err(new Error("Remote base URL not found in the URI")); @@ -94,6 +92,7 @@ export class NetlifyGateway implements bs.Gateway { fetchUrl.setParam("car", key); break; } + // console.log("put", pathPart, key, body.length, fetchUrl.URI().toString()); // if (store === "meta") { // const bodyRes = await bs.addCryptoKeyToGatewayMetaPayload(url, this.sthis, body); // if (bodyRes.isErr()) { @@ -102,7 +101,11 @@ export class NetlifyGateway implements bs.Gateway { // body = bodyRes.Ok(); // } - const done = await fetch(fetchUrl.asURL(), { method: "PUT", body }); + const done = await fetch(fetchUrl.asURL(), { + method: "PUT", + body, + headers: { "Content-Type": "application/octet-stream" }, + }); if (!done.ok) { return this.logger .Error() @@ -127,7 +130,6 @@ export class NetlifyGateway implements bs.Gateway { if (index) { name += `-${index}`; } - name += ".fp"; const fetchUrl = BuildURI.from(remoteBaseUrl); switch (pathPart) { case "meta": @@ -173,7 +175,6 @@ export class NetlifyGateway implements bs.Gateway { if (index) { name += `-${index}`; } - name += ".fp"; const fetchUrl = BuildURI.from(remoteBaseUrl); switch (pathPart) { case "meta": diff --git a/src/sql/gateway-sql.ts b/src/sql/gateway-sql.ts index ca4f5ef7..575b6c3e 100644 --- a/src/sql/gateway-sql.ts +++ b/src/sql/gateway-sql.ts @@ -300,7 +300,7 @@ export function registerSqliteStoreProtocol() { return _register.once(() => { return bs.registerStoreProtocol({ protocol: "sqlite:", - defaultURI: () => URI.from("sqlite://localhost"), + defaultURI: () => URI.from("sqlite://"), gateway: async (sthis) => { return new SQLStoreGateway(sthis); }, diff --git a/src/sql/v0.19/sqlite_factory.ts b/src/sql/v0.19/sqlite_factory.ts index 0f7900b4..e0bcd4a2 100644 --- a/src/sql/v0.19/sqlite_factory.ts +++ b/src/sql/v0.19/sqlite_factory.ts @@ -69,7 +69,7 @@ export async function v0_19sqliteConnectionFactory( switch (upUrl.getParam("taste")) { case "libsql": { const { V0_19LS3Connection } = await import("./sqlite/libsql/sqlite-connection.js"); - sthis.logger.Debug().Str("databaseURL", upUrl.toString()).Msg("connecting to better-sqlite3"); + sthis.logger.Debug().Str("databaseURL", upUrl.toString()).Msg("connecting to libsql"); return { dbConn: new V0_19LS3Connection(sthis, upUrl, opts), url: upUrl.build().setParam("taste", "libsql").URI(), diff --git a/src/ucan/client.ts b/src/ucan/client.ts index 382a2d3a..78361feb 100644 --- a/src/ucan/client.ts +++ b/src/ucan/client.ts @@ -10,7 +10,6 @@ import { sha256 } from "multiformats/hashes/sha2"; import * as ClockCaps from "./clock/capabilities.js"; import * as StoreCaps from "./store/capabilities.js"; import { Server, type Clock, type Service } from "./types.js"; -import { to_uint8 } from "../coerce-binary.js"; //////////////////////////////////////// // CLOCK @@ -141,13 +140,6 @@ export async function registerClock({ //////////////////////////////////////// // CONNECTION //////////////////////////////////////// -export function coerceHeaders(headers: Record | Headers): Record { - if ("entries" in headers) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return Object.fromEntries((headers as any).entries()); - } - return headers as Record; -} export function service(server: Server): ConnectionView { const url = server.uri.toString(); @@ -161,11 +153,16 @@ export function service(server: Server): ConnectionView { }); if (!response.ok) throw new Error(`HTTP Request failed. ${"POST"} ${url} → ${response.status}`); - const buffer = response.ok ? await response.arrayBuffer() : new Uint8Array(); + const buffer = response.ok ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array(); + // console.log("response", response.headers); return { - headers: coerceHeaders(response.headers), - body: to_uint8(buffer), + headers: + "entries" in response.headers + ? Object.fromEntries((response.headers as unknown as Map).entries()) + : {}, + // headers: {}, + body: buffer, }; }, }; diff --git a/src/ucan/common.ts b/src/ucan/common.ts index baa1fa0d..1f0a1e83 100644 --- a/src/ucan/common.ts +++ b/src/ucan/common.ts @@ -3,35 +3,9 @@ import { Delegation } from "@ucanto/interface"; import type { Agent, AgentDataExport, DelegationMeta } from "@web3-storage/access/types"; import { Block } from "multiformats/block"; import { CID } from "multiformats"; -import { to_arraybuf } from "../coerce-binary.js"; - -import type { Service } from "./types.js"; - -export function agentProofs( - agent: Agent, - mailtoDID?: `did:mailto:${string}:${string}` -): { attestations: Delegation[]; delegations: Delegation[] } { - const proofs = agent.proofs([{ with: /did:mailto:.*/, can: "*" }]); - const delegations = proofs.filter( - (p) => p.capabilities[0].can === "*" && (mailtoDID ? p.issuer.did() === mailtoDID : true) - ); - - const delegationCids = delegations.map((d) => d.cid.toString()); - const attestations = proofs.filter((p) => { - const cap = p.capabilities[0]; - return ( - cap.can === "ucan/attest" && - delegationCids.includes((cap.nb as { proof: { toString(): string } }).proof.toString()) - ); - }); - - return { - delegations, - attestations, - }; -} import type { Service } from "./types.js"; +import { to_arraybuf } from "../coerce-binary.js"; export function agentProofs( agent: Agent, diff --git a/src/ucan/ucan-gateway.test.ts b/src/ucan/ucan-gateway.test.ts index ec2f6fb8..913a0154 100644 --- a/src/ucan/ucan-gateway.test.ts +++ b/src/ucan/ucan-gateway.test.ts @@ -1,6 +1,6 @@ import { URI } from "@adviser/cement"; import { fireproof, Database, ConfigOpts } from "@fireproof/core"; -import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest"; import { registerUCANStoreProtocol } from "./ucan-gateway.js"; import { smokeDB } from "../../tests/helper.js"; @@ -11,16 +11,16 @@ import { smokeDB } from "../../tests/helper.js"; describe("UCANGateway", () => { let db: Database; - let unregister: () => void; + // let unregister: () => void; let uri: URI; beforeAll(() => { - unregister = registerUCANStoreProtocol("ucan:"); + registerUCANStoreProtocol("ucan:"); }); - afterAll(() => { - unregister(); - }); + // afterAll(() => { + // unregister(); + // }); beforeEach(async () => { uri = URI.from(process.env.FP_STORAGE_URL); diff --git a/src/ucan/ucan-gateway.ts b/src/ucan/ucan-gateway.ts index 2030143d..a9192fab 100644 --- a/src/ucan/ucan-gateway.ts +++ b/src/ucan/ucan-gateway.ts @@ -371,7 +371,7 @@ export function registerUCANStoreProtocol(protocol = "ucan:", overrideBaseURL?: URI.protocolHasHostpart(protocol); return bs.registerStoreProtocol({ protocol, - defaultURI: () => URI.from(overrideBaseURL || `${protocol}://localhost`), + defaultURI: () => URI.from(overrideBaseURL ?? `${protocol}://localhost`), serdegateway: async (sthis) => { return new AddKeyToDbMetaGateway(new UCANGateway(sthis), "v1"); }, diff --git a/src/v2-cloud/backend/cf-hono-server.ts b/src/v2-cloud/backend/cf-hono-server.ts index c190a7bb..4e3b7008 100644 --- a/src/v2-cloud/backend/cf-hono-server.ts +++ b/src/v2-cloud/backend/cf-hono-server.ts @@ -1,8 +1,25 @@ -import { HttpHeader, KeyedResolvOnce, Logger, LoggerImpl, URI } from "@adviser/cement"; +import { BuildURI, HttpHeader, KeyedResolvOnce, Logger, LoggerImpl, URI } from "@adviser/cement"; import { Context, Hono } from "hono"; -import { ConnMiddleware, HonoServerFactory, RunTimeParams, HonoServerBase } from "../hono-server.js"; -import { WSContext, WSContextInit, WSEvents } from "hono/ws"; -import { buildErrorMsg, defaultGestalt, EnDeCoder, Gestalt } from "../msg-types.js"; +import { + ConnMiddleware, + HonoServerFactory, + RunTimeParams, + HonoServerBase, + WSEventsConnId, + WSContextWithId, +} from "../hono-server.js"; +import { SendOptions, WSContextInit, WSMessageReceive, WSReadyState } from "hono/ws"; +import { + buildErrorMsg, + defaultGestalt, + EnDeCoder, + Gestalt, + MsgBase, + MsgIsWithConn, + MsgWithConn, + QSId, + qsidEqual, +} from "../msg-types.js"; // import { RequestInfo as CFRequestInfo } from "@cloudflare/workers-types"; import { defaultMsgParams, jsonEnDe } from "../msger.js"; import { ensureLogger, ensureSuperThis, SuperThis } from "@fireproof/core"; @@ -12,6 +29,7 @@ import { CFDObjSQLDatabase } from "./cf-dobj-abstract-sql.js"; import { Env } from "./env.js"; import { WSRoom } from "../ws-room.js"; import { FPBackendDurableObject, FPRoomDurableObject } from "./server.js"; +import { ConnItem } from "../msg-dispatch.js"; const startedChs = new KeyedResolvOnce(); @@ -28,23 +46,218 @@ export function getRoomDurableObject(env: Env) { // console.log("getDurableObject", env); const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_WS_ROOM"; const rany = env as unknown as Record>; + // console.log("getRoomDurableObject", cfBackendKey); const dObjNs = rany[cfBackendKey]; const id = dObjNs.idFromName(cfBackendKey); return dObjNs.get(id); } +function webSocket2WSContextInit(ws: WebSocket): WSContextInit { + return { + send: (data: string | ArrayBuffer, _options: SendOptions): void => { + ws.send(data); + }, + close: (code?: number, reason?: string): void => ws.close(code, reason), + raw: ws, + readyState: ws.readyState as WSReadyState, + url: ws.url, + protocol: ws.protocol, + }; +} + +const eventsWithConnId = new Map< + string, + { + getWebSockets?: () => WebSocket[]; + events?: WSEventsConnId; + } +>(); class CFWSRoom implements WSRoom { - readonly dobj: DurableObjectStub; - constructor(dobj: DurableObjectStub) { - this.dobj = dobj; - } - async acceptConnection(ws: WebSocket, wse: WSEvents): Promise { - const ret = await this.dobj.acceptWebSocket(ws, wse); - const wsCtx = new WSContext(ws as WSContextInit); - wse.onOpen?.({} as Event, wsCtx); - // return Promise.resolve(); - // ws.accept(); - return ret; + readonly sthis: SuperThis; + readonly id: string; + + readonly eventsWithConnId = eventsWithConnId; + + constructor(sthis: SuperThis) { + this.sthis = sthis; + this.id = sthis.nextId(12).str; + } + + // private _getWebSocketsCtx = (): WebSocket[] => { + // throw new Error("Method not ready"); + // } + applyGetWebSockets(id: string, fn: () => WebSocket[]): void { + // console.log("applyGetWebSockets", this.id, fn); + let val = this.eventsWithConnId.get(id); + if (!val) { + val = {}; + this.eventsWithConnId.set(id, val); + } + val.getWebSockets = fn; + } + + getConns(conn: QSId): ConnItem[] { + if (!this.eventsWithConnId.has(conn.resId)) { + // eslint-disable-next-line no-console + console.error("getConns:missing", conn); + return []; + } + const getWebSockets = this.eventsWithConnId.get(conn.resId)?.getWebSockets; + if (!getWebSockets) { + // eslint-disable-next-line no-console + console.error("getConns:missing-getWebSockets", conn); + return []; + } + // console.log("getConns-enter:", this.id); + try { + const res = getWebSockets() + .map((i) => { + const o = i.deserializeAttachment(); + if (!o.conn) { + return; + } + + // console.log("getConns", o); + return { + conn: o.conn, + touched: new Date(), + ws: new WSContextWithId(o.id, webSocket2WSContextInit(i)), + } satisfies ConnItem; + }) + .filter((i) => !!i); + // console.log("getConns", this.id, res); + return res ?? []; + } catch (e) { + // eslint-disable-next-line no-console + console.error("getConns", e); + return []; + } + // throw new Error("Method not implemented."); + } + removeConn(conn: QSId): void { + const found = this.getConns(conn).find((i) => qsidEqual(i.conn, conn)); + if (!found) { + return; + } + // console.log("removeConn", this.id, conn); + const s = found.ws.raw?.deserializeAttachment(); + delete s.conn; + found.ws.raw?.serializeAttachment(s); + + // throw new Error("Method not implemented."); + } + addConn(ws: WSContextWithId, conn: QSId): QSId { + const x = ws.raw?.deserializeAttachment(); + ws.raw?.serializeAttachment({ ...x, conn }); + // console.log("addConn", this.id, conn); + // throw new Error("Method not implemented."); + return conn; + } + isConnected(msg: MsgBase): msg is MsgWithConn { + if (!MsgIsWithConn(msg)) { + return false; + } + return !!this.getConns(msg.conn).find((i) => qsidEqual(i.conn, msg.conn)); + // // eslint-disable-next-line no-console + // console.log("isConnected", this.id, this.getWebSockets().length); + // // throw new Error("Method not implemented."); + // return true; + } + // readonly dobj: DurableObjectStub; + // constructor(dobj: DurableObjectStub) { + // this.dobj = dobj; + // } + + applyEvents(id: string, events: WSEventsConnId): void { + // if (this.eventsWithConnId.has(id)) { + // throw new Error("applyEvents:already set"); + // } + let val = this.eventsWithConnId.get(id); + if (!val) { + val = {}; + this.eventsWithConnId.set(id, val); + } + val.events = events; + // console.log("applyEvents", this.id, id); + } + + readonly events = { + onOpen: (id: string, evt: Event, ws: WebSocket) => { + if (!this.eventsWithConnId.has(id)) { + throw new Error(`applyEvents:onOpen missing not ${id} => ${Array.from(this.eventsWithConnId.keys())}`); + } + // const o = ws.deserializeAttachment(); + this.eventsWithConnId.get(id)?.events?.onOpen(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); + }, + onMessage: (id: string, evt: MessageEvent, ws: WebSocket) => { + if (!this.eventsWithConnId.has(id)) { + // console.log("onMessaged:Error", this.id); + throw new Error(`applyEvents:onMessagee missing not ${id}`); + } + // const o = ws.deserializeAttachment(); + const wci = new WSContextWithId(id, webSocket2WSContextInit(ws)); + this.eventsWithConnId.get(id)?.events?.onMessage(evt, wci); + // console.log("onMessaged", this.id); + }, + onClose: (id: string, evt: CloseEvent, ws: WebSocket) => { + // console.log("onClosing", ws); + if (!this.eventsWithConnId.has(id)) { + throw new Error(`applyEvents:onClose missing not ${id}`); + } + // const o = ws.deserializeAttachment(); + this.eventsWithConnId.get(id)?.events?.onClose(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); + // console.log("onClosed", this.id); + }, + onError: (id: string, evt: Event, ws: WebSocket) => { + // console.log("onError", ws); + if (!this.eventsWithConnId.has(id)) { + throw new Error(`applyEvents:onError missing not ${id}`); + } + // const o = ws.deserializeAttachment(); + this.eventsWithConnId.get(id)?.events?.onError(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); + }, + }; // satisfies CFWSEvents; + + // async acceptConnection(ws: WebSocket, wse: WSEvents, ctx: Env): Promise { + // throw new Error("Method not implemented."); + // // const dobj = getRoomDurableObject(ctx); + // // console.log("acceptConnection", dobj); + // // // const ret = dobj.acceptWebSocket(ws, wse); + // // const wsCtx = new WSContext(ws as WSContextInit); + // // wse.onOpen?.({} as Event, wsCtx); + // // // return Promise.resolve(); + // // // ws.accept(); + // // return Promise.resolve(); + // } + + // getEvents(): CFWSEvents { + // return this.events; + // } + + // getWebSockets = (): WebSocket[] => { + // // console.log("getWebSockets", this.id); + // throw new Error("Method not ready"); + // } + // applyExposeCtx(ctx: { getWebSockets: () => WebSocket[] }): void { + // this.getWebSockets = ctx.getWebSockets; + // } +} + +export class CFExposeCtx { + readonly sthis: SuperThis; + readonly wsRoom: CFWSRoom; + readonly logger: Logger; + readonly ende: EnDeCoder; + readonly gs: Gestalt; + readonly db: SQLDatabase; + + constructor(sthis: SuperThis, logger: Logger, ende: EnDeCoder, gs: Gestalt, db: SQLDatabase, wsRoom: CFWSRoom) { + this.sthis = sthis; + this.logger = logger; + this.ende = ende; + this.gs = gs; + this.db = db; + this.wsRoom = wsRoom; } } @@ -58,15 +271,15 @@ export class CFHonoFactory implements HonoServerFactory { this._onClose = onClose; } // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { + async inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { // this._env = c.env const sthis = ensureSuperThis({ logger: new LoggerImpl(), }); sthis.env.sets(c.env); + const logger = ensureLogger(sthis, `CFHono[${URI.from(c.req.url).pathname}]`); const ende = jsonEnDe(sthis); - // this.sthis.env. const fpProtocol = sthis.env.get("FP_PROTOCOL"); const msgP = defaultMsgParams(sthis, { hasPersistent: true, @@ -76,33 +289,53 @@ export class CFHonoFactory implements HonoServerFactory { id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", }); - const wsRoom = new CFWSRoom(c.env); const cfBackendMode = c.env.CF_BACKEND_MODE && c.env.CF_BACKEND_MODE === "DURABLE_OBJECT" ? "DURABLE_OBJECT" : "D1"; let db: SQLDatabase; + let cfBackendKey: string; switch (cfBackendMode) { - case "DURABLE_OBJECT": { - db = new CFDObjSQLDatabase(getBackendDurableObject(c.env)); - const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); - // TODO WE NEED TO START THE DURABLE OBJECT - // but then on every request we import the schema - return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs })); - } - // break; + case "DURABLE_OBJECT": + { + cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; + // console.log("DO-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); + db = new CFDObjSQLDatabase(getBackendDurableObject(c.env)); + } + break; + case "D1": - default: { - const cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"; - return startedChs - .get(cfBackendKey) - .once(async () => { - db = new CFWorkerSQLDatabase(c.env[cfBackendKey] as D1Database); - const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); - await chs.start(); - return chs; - }) - .then((chs) => fn({ sthis, logger, ende, impl: chs })); - } + default: + { + cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"; + // console.log("D1-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); + db = new CFWorkerSQLDatabase(c.env[cfBackendKey] as D1Database); + } + break; + // return startedChs + // .get(cfBackendKey) + // .once(async () => { + // const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + // await chs.start(); + // return chs; + // }) + // .then((chs) => fn({ sthis, logger, ende, impl: chs })); // break; } + + const wsRoom = new CFWSRoom(sthis); + c.env.FP_EXPOSE_CTX = new CFExposeCtx(sthis, logger, ende, gs, db, wsRoom); + // wsRoom.applyGetWebSockets(c.env.FP_EXPOSE_CTX.getWebSockets); + + // TODO WE NEED TO START THE DURABLE OBJECT + // but then on every request we import the schema + // return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs })); + return startedChs + .get(cfBackendKey) + .once(async () => { + const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + await chs.start(); + return chs; + }) + .then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); + // return ret; // .then((v) => sthis.logger.Flush().then(() => v)) } @@ -145,11 +378,14 @@ export class CFHonoServer extends HonoServerBase { // const stub = env.FP_META_GROUPS.get(id); // } - upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { + upgradeWebSocket( + createEvents: (c: Context) => WSEventsConnId | Promise> + ): ConnMiddleware { + // throw new Error("upgradeWebSocket Method not implemented."); // if (!this._upgradeWebSocket) { // throw new Error("upgradeWebSocket not implemented"); // } - return async (conn, c, _next) => { + return async (_conn, c, _next) => { const upgradeHeader = c.req.header("Upgrade"); if (!upgradeHeader || upgradeHeader !== "websocket") { return new Response( @@ -157,38 +393,63 @@ export class CFHonoServer extends HonoServerBase { { status: 426 } ); } - // const env = c.env as Env; - // const id = env.FP_META_GROUPS.idFromName([conn.key.tenant, conn.key.ledger].join(":")); - // const dObj = env.FP_META_GROUPS.get(id); - // c.env.WS_EVENTS = createEvents(c); - // return dObj.fetch(c.req.raw as unknown as CFRequestInfo) as unknown as Promise; - // this._upgradeWebSocket!(createEvents)(c, next); - const { 0: client, 1: server } = new WebSocketPair(); - conn.attachWSPair({ client, server }); + // console.log("upgradeWebSocket", Object.keys(_conn)); + + //wsRoom.getEvents(); + //wsRoom.applyExposeCtx(c.env.EXPOSE_CTX); + + const id = c.env.FP_EXPOSE_CTX.sthis.nextId().str; + // console.log("upgradeWebSocket:createEvents: ", id); + c.env.FP_EXPOSE_CTX.wsRoom.applyEvents(id, await createEvents(c)); + + // const { sthis, logger, ende, wsRoom, gs, db } = c.env.EXPOSE_CTX; + // const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + // await chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); + + const url = BuildURI.from(c.req.url).setParam("ctxId", id).toString(); + + const dobjRoom = getRoomDurableObject(c.env); + const ret = dobjRoom.fetch(url, c.req.raw); + return ret; + + // // const env = c.env as Env; + // // const id = env.FP_META_GROUPS.idFromName([conn.key.tenant, conn.key.ledger].join(":")); + // // const dObj = env.FP_META_GROUPS.get(id); + // // c.env.WS_EVENTS = createEvents(c); + // // return dObj.fetch(c.req.raw as unknown as CFRequestInfo) as unknown as Promise; + // // this._upgradeWebSocket!(createEvents)(c, next); + + // const { 0: client, 1: server } = new WebSocketPair(); + // conn.attachWSPair({ client, server }); + + // const wsEvents = await createEvents(c); + // (this.wsRoom as CFWSRoom).applyEvents(wsEvents); + + // // console.log("applyEvents", c.env.WS_EVENTS); - const wsEvents = await createEvents(c); - // console.log("upgradeWebSocket", c.req.url); + // // const wsEvents = await createEvents(c); + // // console.log("upgradeWebSocket", c.req.url); - // const wsCtx = new WSContext(server as WSContextInit); + // // const wsCtx = new WSContext(server as WSContextInit); - // server.onopen = (ev) => { - // console.log("onopen", ev); - // wsEvents.onOpen?.(ev, wsCtx); - // } + // // server.onopen = (ev) => { + // // console.log("onopen", ev); + // // wsEvents.onOpen?.(ev, wsCtx); + // // } - await this.wsRoom.acceptConnection(server, wsEvents); + // // await this.wsRoom.acceptConnection(server, wsEvents , c.env); - // server.send("Hello from server"); + // // server.send("Hello from server"); - // this.wsConnections.set(this.sthis.nextId().str, { client, server }); - // const client = webSocketPair[0], - // server = webSocketPair[1]; + // // this.wsConnections.set(this.sthis.nextId().str, { client, server }); + // // const client = webSocketPair[0], + // // server = webSocketPair[1]; - return new Response(null, { - status: 101, - webSocket: client, - }); + // return new Response(null, { + // status: 101, + // webSocket: client, + // }); }; } } diff --git a/src/v2-cloud/backend/env.d.ts b/src/v2-cloud/backend/env.d.ts index a3f757e1..a1b2016b 100644 --- a/src/v2-cloud/backend/env.d.ts +++ b/src/v2-cloud/backend/env.d.ts @@ -4,6 +4,7 @@ import type { DurableObjectNamespace } from "@cloudflare/workers-types"; // import { WSEvents } from "hono/ws"; import { FPRoomDurableObject, FPBackendDurableObject } from "./server.ts"; +import { CFExposeCtx } from "./cf-hono-server.ts"; export interface Env { // bucket: R2Bucket; @@ -48,6 +49,8 @@ export interface Env { FP_WS_ROOM: DurableObjectNamespace; + FP_EXPOSE_CTX: CFExposeCtx; + // WS_EVENTS: WSEvents; } diff --git a/src/v2-cloud/backend/server.ts b/src/v2-cloud/backend/server.ts index c0f100b6..383da984 100644 --- a/src/v2-cloud/backend/server.ts +++ b/src/v2-cloud/backend/server.ts @@ -6,7 +6,8 @@ import { HonoServer } from "../hono-server.js"; import { Hono } from "hono"; import { Env } from "./env.js"; import { CFHonoFactory } from "./cf-hono-server.js"; -import { WSContext, WSContextInit, WSEvents } from "hono/ws"; +import { WSMessageReceive } from "hono/ws"; +import { URI } from "@adviser/cement"; const app = new Hono(); const honoServer = new HonoServer(new CFHonoFactory()); @@ -47,26 +48,93 @@ export class FPBackendDurableObject extends DurableObject { } } +export interface CFWSEvents { + readonly onOpen: (evt: Event, ws: WebSocket) => void; + readonly onMessage: (evt: MessageEvent, ws: WebSocket) => void; + readonly onClose: (evt: CloseEvent, ws: WebSocket) => void; + readonly onError: (evt: Event, ws: WebSocket) => void; +} + export class FPRoomDurableObject extends DurableObject { - private wsEvents?: WSEvents; + // wsEvents?: CFWSEvents; + + readonly id = Math.random().toString(36).slice(2); + + // _id!: string; + + async fetch(request: Request): Promise { + // console.log("DO-fetch", request.url, request.method, request.headers); + // Creates two ends of a WebSocket connection. + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating + // request within the Durable Object. It has the effect of "accepting" the connection, + // and allowing the WebSocket to send and receive messages. + // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket + // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while + // the connection is open. During periods of inactivity, the Durable Object can be evicted + // from memory, but the WebSocket connection will remain open. If at some later point the + // WebSocket receives a message, the runtime will recreate the Durable Object + // (run the `constructor`) and deliver the message to the appropriate handler. + this.ctx.acceptWebSocket(server); + + // server.onopen = () => { + // console.log("client onopen"); + // } + // server.onmessage = (event) => { + // console.log("client onmessage", event.data); + // } + // server.onclose = (event) => { + // console.log("client onclose", event.code, event.reason); + // } + // server.onerror = (event) => { + // console.log("client onerror", event); + // } + // const wss = this.ctx.getWebSockets(); + + const id = URI.from(request.url).getParam("ctxId", "none"); + + // console.log("DO-ids:", id, this.id); + + this.env.FP_EXPOSE_CTX.wsRoom.applyGetWebSockets(id, () => this.ctx.getWebSockets()); + server.serializeAttachment({ id }); + + this.env.FP_EXPOSE_CTX.wsRoom.events.onOpen(id, {} as Event, server); + + // for (const ws of wss) { + // ws.setnd(`New WebSocket connection established: ${wss.length}`); + // } + + return new Response(null, { + status: 101, + webSocket: client, + }); + } + + // acceptWebSocket(ws: WebSocket, wsEvents: WSEvents): void { + // this.ctx.acceptWebSocket(ws); + // this.wsEvents = wsEvents; + // } - async acceptWebSocket(ws: WebSocket, wsEvents: WSEvents): Promise { - this.ctx.acceptWebSocket(ws); - this.wsEvents = wsEvents; + webSocketOpen(ws: WebSocket): void | Promise { + const { id } = ws.deserializeAttachment(); + this.env.FP_EXPOSE_CTX.wsRoom.events.onOpen(id, {} as Event, ws); } webSocketError(ws: WebSocket, error: unknown): void | Promise { - const wsCtx = new WSContext(ws as WSContextInit); - this.wsEvents?.onError?.(error as Event, wsCtx); + const { id } = ws.deserializeAttachment(); + this.env.FP_EXPOSE_CTX.wsRoom.events.onError(id, error as Event, ws); } async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer): Promise { - const wsCtx = new WSContext(ws as WSContextInit); - this.wsEvents?.onMessage?.({ data: msg } as MessageEvent, wsCtx); + const { id } = ws.deserializeAttachment(); + // console.log("webSocketMessage", msg); + this.env.FP_EXPOSE_CTX.wsRoom.events.onMessage(id, { data: msg } as MessageEvent, ws); } webSocketClose(ws: WebSocket, code: number, reason: string): void | Promise { - const wsCtx = new WSContext(ws as WSContextInit); - this.wsEvents?.onClose?.({ code, reason } as CloseEvent, wsCtx); + const { id } = ws.deserializeAttachment(); + this.env.FP_EXPOSE_CTX.wsRoom.events.onClose(id, { code, reason } as CloseEvent, ws); } } diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index b30f07fe..2b5056d5 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -40,8 +40,8 @@ describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gat .fill(async () => { url.setParam("store", "data"); const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); - const res = await gw.get(kurl); + const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); expect(res.isErr()).toBeTruthy(); expect(res.Err()).toBeInstanceOf(NotFoundError); }) @@ -53,22 +53,22 @@ describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gat await Promise.all( Array(20) .fill(async () => { - const resStart = await gw.start(url.URI()); + const resStart = await gw.start(url.URI(), sthis); expect(resStart.isOk()).toBeTruthy(); url.setParam("store", "data"); const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl); + const resGet = await gw.get(kurl, sthis); expect(resGet.isOk()).toBeTruthy(); expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl); + const resDel = await gw.delete(kurl, sthis); expect(resDel.isOk()).toBeTruthy(); - const res = await gw.get(kurl); + const res = await gw.get(kurl, sthis); expect(res.isErr()).toBeTruthy(); expect(res.Err()).toBeInstanceOf(NotFoundError); }) @@ -84,8 +84,8 @@ describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gat .fill(async () => { url.setParam("store", "wal"); const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); - const res = await gw.get(kurl); + const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); expect(res.isErr()).toBeTruthy(); expect(res.Err()).toBeInstanceOf(NotFoundError); }) @@ -97,22 +97,22 @@ describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gat await Promise.all( Array(20) .fill(async () => { - const resStart = await gw.start(url.URI()); + const resStart = await gw.start(url.URI(), sthis); expect(resStart.isOk()).toBeTruthy(); url.setParam("store", "wal"); const key = `theWALKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key)).Ok(); + const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!")); + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl); + const resGet = await gw.get(kurl, sthis); expect(resGet.isOk()).toBeTruthy(); expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl); + const resDel = await gw.delete(kurl, sthis); expect(resDel.isOk()).toBeTruthy(); - const res = await gw.get(kurl); + const res = await gw.get(kurl, sthis); expect(res.isErr()).toBeTruthy(); expect(res.Err()).toBeInstanceOf(NotFoundError); }) diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index a221fcef..bfc778d0 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -1,6 +1,6 @@ // import PartySocket, { PartySocketOptions } from "partysocket"; -import { Result, URI, KeyedResolvOnce, exception2Result, key } from "@adviser/cement"; -import { bs, ensureLogger, Logger, NotFoundError, rt, SuperThis } from "@fireproof/core"; +import { Result, URI, KeyedResolvOnce, exception2Result, Logger, param } from "@adviser/cement"; +import { bs, ensureLogger, NotFoundError, SuperThis } from "@fireproof/core"; import { buildErrorMsg, buildReqOpen, @@ -86,12 +86,12 @@ abstract class BaseGateway { conn: MsgConnected ): Promise> { const rParams = uri.getParamsResult({ - key: key.REQUIRED, - store: key.REQUIRED, - path: key.OPTIONAL, - tenant: key.REQUIRED, - name: key.REQUIRED, - index: key.OPTIONAL, + key: param.REQUIRED, + store: param.REQUIRED, + path: param.OPTIONAL, + tenant: param.REQUIRED, + name: param.REQUIRED, + index: param.OPTIONAL, }); if (rParams.isErr()) { return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, rParams.Err()); @@ -444,11 +444,11 @@ export class FireproofCloudGateway implements bs.Gateway { // fireproof://localhost:1999/?name=test-public-api&protocol=ws&store=meta async getCloudConnection(uri: URI): Promise> { const rParams = uri.getParamsResult({ - name: key.REQUIRED, + name: param.REQUIRED, protocol: "https", - store: key.REQUIRED, - storekey: key.OPTIONAL, - tenant: key.REQUIRED, + store: param.REQUIRED, + storekey: param.OPTIONAL, + tenant: param.REQUIRED, }); if (rParams.isErr()) { return this.logger.Error().Url(uri).Err(rParams).Msg("getCloudConnection:err").ResultError(); @@ -555,51 +555,30 @@ export class FireproofCloudGateway implements bs.Gateway { await Promise.all(Array.from(trackPuts).map(async (k) => this.delete(URI.from(k)))); return Result.Ok(undefined); } -} - -// function pkKey(set?: ConnectionKey): string { -// const ret = JSON.stringify( -// Object.entries(set || {}) -// .sort(([a], [b]) => a.localeCompare(b)) -// .filter(([k]) => k !== "id") -// .map(([k, v]) => ({ [k]: v })) -// ); -// return ret; -// } -export class FireproofCloudTestStore implements bs.TestGateway { - readonly logger: Logger; - readonly sthis: SuperThis; - readonly gateway: bs.Gateway; - constructor(gw: bs.Gateway, sthis: SuperThis) { - this.sthis = sthis; - this.logger = ensureLogger(sthis, "FireproofCloudTestStore"); - this.gateway = gw; - } - async get(uri: URI, key: string): Promise { - const url = uri.build().setParam("key", key).URI(); - const dbFile = this.sthis.pathOps.join(rt.getPath(url, this.sthis), rt.getFileName(url, this.sthis)); - this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get"); - const buffer = await this.gateway.get(url); - this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got"); - return buffer.Ok(); + async getPlain(): Promise> { + return Result.Err(new Error("Not implemented")); + // const url = uri.build().setParam("key", key).URI(); + // const dbFile = this.sthis.pathOps.join(rt.getPath(url, this.sthis), rt.getFileName(url, this.sthis)); + // this.logger.Debug().Url(url).Str("dbFile", dbFile).Msg("get"); + // const buffer = await this.gateway.get(url); + // this.logger.Debug().Url(url).Str("dbFile", dbFile).Len(buffer).Msg("got"); + // return buffer.Ok(); } } const onceRegisterFireproofCloudStoreProtocol = new KeyedResolvOnce<() => void>(); -export function registerFireproofCloudStoreProtocol(protocol = "fireproof:", overrideBaseURL?: string) { +export function registerFireproofCloudStoreProtocol(protocol = "fpcloud:") { return onceRegisterFireproofCloudStoreProtocol.get(protocol).once(() => { URI.protocolHasHostpart(protocol); return bs.registerStoreProtocol({ protocol, - overrideBaseURL, + defaultURI() { + return URI.from("fpcloud://fireproof.cloud/"); + }, gateway: async (sthis) => { return new FireproofCloudGateway(sthis); }, - test: async (sthis: SuperThis) => { - const gateway = new FireproofCloudGateway(sthis); - return new FireproofCloudTestStore(gateway, sthis); - }, }); }); } diff --git a/src/v2-cloud/client/index.ts b/src/v2-cloud/client/index.ts index 48804cd3..ec05f824 100644 --- a/src/v2-cloud/client/index.ts +++ b/src/v2-cloud/client/index.ts @@ -1,15 +1,14 @@ -import { BuildURI, CoerceURI, KeyedResolvOnce, runtimeFn, URI } from "@adviser/cement"; -import { bs, Database, fireproof } from "@fireproof/core"; -import { ConnectFunction, connectionFactory, makeKeyBagUrlExtractable } from "../../connection-from-store.js"; +import { CoerceURI, URI } from "@adviser/cement"; +import { Attachable, GatewayUrlsParam } from "@fireproof/core"; import { registerFireproofCloudStoreProtocol } from "./gateway.js"; -interface ConnectData { - readonly remoteName: string; - firstConnect: boolean; - endpoint?: string; -} +// interface ConnectData { +// readonly remoteName: string; +// firstConnect: boolean; +// endpoint?: string; +// } -const SYNC_DB_NAME = "fp_sync"; +// const SYNC_DB_NAME = "fp_sync"; // Usage: // @@ -34,92 +33,113 @@ const SYNC_DB_NAME = "fp_sync"; registerFireproofCloudStoreProtocol(); -const connectionCache = new KeyedResolvOnce(); -export const rawConnect: ConnectFunction = ( - db: Database, - remoteDbName = "", - url = "fireproof://cloud.fireproof.direct" -) => { - const { sthis, blockstore, name: dbName } = db; - if (!dbName) { - throw new Error("dbName is required"); +export function toV2Cloud(url: CoerceURI): Attachable { + const urlObj = URI.from(url); + if (urlObj.protocol !== "fpcloud:") { + throw new Error("url must have fireproof protocol"); } - const urlObj = BuildURI.from(url); - urlObj.protocol("fireproof:"); - const existingName = urlObj.getParam("name"); - urlObj.defParam("name", remoteDbName || existingName || dbName); - urlObj.defParam("localName", dbName); - urlObj.defParam("storekey", `@${dbName}:data@`); - urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/"); - // const fpUrl = urlObj - // .toString() - // .replace(/^http:\/\//, "fireproof://") - // .replace(/^https:\/\//, "fireproof://"); - // eslint-disable-next-line no-console - console.log("Config URL: " + urlObj.toString()); - return connectionCache.get(urlObj.toString()).once(() => { - makeKeyBagUrlExtractable(sthis); - const connection = connectionFactory(sthis, urlObj); - connection.connect_X(blockstore); - return connection; - }); -}; + // const existingName = urlObj.getParam("name"); + // urlObj.defParam("name", remoteDbName || existingName || dbName); + // urlObj.defParam("localName", dbName); + // urlObj.defParam("storekey", `@${dbName}:data@`); + return { + name: urlObj.protocol, + prepare(): Promise { + return Promise.resolve({ + car: { url: urlObj }, + file: { url: urlObj }, + meta: { url: urlObj }, + }); + }, + }; +} -async function getOrCreateRemoteName(dbName: string, remoteName?: string) { - const syncDb = fireproof(SYNC_DB_NAME); +// const connectionCache = new KeyedResolvOnce(); +// export const rawConnect: ConnectFunction = ( +// db: Database, +// remoteDbName = "", +// url = "fireproof://cloud.fireproof.direct" +// ) => { +// const { sthis, blockstore, name: dbName } = db; +// if (!dbName) { +// throw new Error("dbName is required"); +// } +// const urlObj = BuildURI.from(url); +// urlObj.protocol("fireproof:"); +// const existingName = urlObj.getParam("name"); +// urlObj.defParam("name", remoteDbName || existingName || dbName); +// urlObj.defParam("localName", dbName); +// urlObj.defParam("storekey", `@${dbName}:data@`); +// urlObj.defParam("getBaseUrl", "https://storage.fireproof.direct/"); +// // const fpUrl = urlObj +// // .toString() +// // .replace(/^http:\/\//, "fireproof://") +// // .replace(/^https:\/\//, "fireproof://"); +// // eslint-disable-next-line no-console +// console.log("Config URL: " + urlObj.toString()); +// return connectionCache.get(urlObj.toString()).once(() => { +// makeKeyBagUrlExtractable(sthis); +// const connection = connectionFactory(sthis, urlObj); +// connection.connect_X(blockstore); +// return connection; +// }); +// }; - const result = await syncDb.query("localName", { key: dbName, includeDocs: true }); - if (result.rows.length === 0) { - const doc = { - remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str, - localName: dbName, - firstConnect: !remoteName, - } as ConnectData; - const { id } = await syncDb.put(doc); - return { ...doc, _id: id }; - } - const doc = result.rows[0].doc; - return doc; -} +// async function getOrCreateRemoteName(dbName: string, remoteName?: string) { +// const syncDb = fireproof(SYNC_DB_NAME); -export function connect( - db: Database, - remoteName?: string, - dashboardURI: CoerceURI = "https://dashboard.fireproof.storage/", - remoteURI: CoerceURI = "fireproof://cloud.fireproof.direct" -): Promise { - const dbName = db.name as string; - if (!dbName) { - throw new Error("Database name is required for cloud connection"); - } +// const result = await syncDb.query("localName", { key: dbName, includeDocs: true }); +// if (result.rows.length === 0) { +// const doc = { +// remoteName: remoteName || syncDb.sthis.timeOrderedNextId().str, +// localName: dbName, +// firstConnect: !remoteName, +// } as ConnectData; +// const { id } = await syncDb.put(doc); +// return { ...doc, _id: id }; +// } +// const doc = result.rows[0].doc; +// return doc; +// } - return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => { - if (!doc) { - throw new Error("Failed to get or create remote name"); - } - doc.endpoint = URI.from(remoteURI).toString(); - const connection = rawConnect(db, doc.remoteName, URI.from(doc.endpoint).toString()); - const connectURI = URI.from(dashboardURI).build().pathname("/fp/databases/connect"); - connectURI.defParam("localName", dbName); - connectURI.defParam("remoteName", doc.remoteName); - if (doc.endpoint) { - connectURI.defParam("endpoint", doc.endpoint); - } - // eslint-disable-next-line no-console - console.log("Fireproof Cloud: " + connectURI.toString()); - if ( - doc.firstConnect && - runtimeFn().isBrowser && - window.location.href.indexOf(URI.from(dashboardURI).toString()) === -1 - ) { - // Set firstConnect to false after opening the window, so we don't constantly annoy with the dashboard - const syncDb = fireproof(SYNC_DB_NAME); - doc.firstConnect = false; - await syncDb.put(doc); +// export function connect( +// db: Database, +// remoteName?: string, +// dashboardURI: CoerceURI = "https://dashboard.fireproof.storage/", +// remoteURI: CoerceURI = "fireproof://cloud.fireproof.direct" +// ): Promise { +// const dbName = db.name as string; +// if (!dbName) { +// throw new Error("Database name is required for cloud connection"); +// } - // window.open(connectURI.toString(), "_blank"); - } - connection.dashboardUrl = URI.from(connectURI); - return connection; - }); -} +// return getOrCreateRemoteName(dbName, remoteName).then(async (doc) => { +// if (!doc) { +// throw new Error("Failed to get or create remote name"); +// } +// doc.endpoint = URI.from(remoteURI).toString(); +// const connection = rawConnect(db, doc.remoteName, URI.from(doc.endpoint).toString()); +// const connectURI = URI.from(dashboardURI).build().pathname("/fp/databases/connect"); +// connectURI.defParam("localName", dbName); +// connectURI.defParam("remoteName", doc.remoteName); +// if (doc.endpoint) { +// connectURI.defParam("endpoint", doc.endpoint); +// } +// // eslint-disable-next-line no-console +// console.log("Fireproof Cloud: " + connectURI.toString()); +// if ( +// doc.firstConnect && +// runtimeFn().isBrowser && +// window.location.href.indexOf(URI.from(dashboardURI).toString()) === -1 +// ) { +// // Set firstConnect to false after opening the window, so we don't constantly annoy with the dashboard +// const syncDb = fireproof(SYNC_DB_NAME); +// doc.firstConnect = false; +// await syncDb.put(doc); + +// // window.open(connectURI.toString(), "_blank"); +// } +// connection.dashboardUrl = URI.from(connectURI); +// return connection; +// }); +// } diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index b04fe3b1..d5350ea6 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -1,5 +1,5 @@ -import { exception2Result, HttpHeader, param, ResolveOnce, Result, URI } from "@adviser/cement"; -import { Logger, SuperThis } from "@fireproof/core"; +import { exception2Result, HttpHeader, Logger, param, ResolveOnce, Result, URI } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; import { Context, Hono, Next } from "hono"; import { top_uint8 } from "../coerce-binary.js"; import { @@ -14,8 +14,8 @@ import { GwCtx, MsgIsError, } from "./msg-types.js"; -import { MsgDispatcher, WSConnection } from "./msg-dispatch.js"; -import { WSEvents } from "hono/ws"; +import { MsgDispatcher, MsgDispatcherCtx, Promisable, WSConnection } from "./msg-dispatch.js"; +import { WSContext, WSContextInit, WSMessageReceive } from "hono/ws"; import { calculatePreSignedUrl, PreSignedMsg } from "./pre-signed-url.js"; import { buildMsgDispatcher } from "./msg-dispatcher-impl.js"; import { @@ -38,20 +38,46 @@ export interface RunTimeParams { readonly logger: Logger; readonly ende: EnDeCoder; readonly impl: HonoServerImpl; + readonly wsRoom: WSRoom; } + +export class WSContextWithId extends WSContext { + readonly id: string; + constructor(id: string, ws: WSContextInit) { + super(ws); + this.id = id; + } +} + +export interface WSEventsConnId { + readonly onOpen: (evt: Event, ws: WSContextWithId) => void; + readonly onMessage: (evt: MessageEvent, ws: WSContextWithId) => void; + readonly onClose: (evt: CloseEvent, ws: WSContextWithId) => void; + readonly onError: (evt: Event, ws: WSContextWithId) => void; +} + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type export type ConnMiddleware = (conn: WSConnection, c: Context, next: Next) => Promise; export interface HonoServerImpl { start(): Promise; gestalt(): Gestalt; + // getConnected(): Connected[]; calculatePreSignedUrl(p: PreSignedMsg): Promise>; - upgradeWebSocket: (createEvents: (c: Context) => WSEvents | Promise) => ConnMiddleware; + upgradeWebSocket( + createEvents: (c: Context) => WSEventsConnId | Promise> + ): ConnMiddleware; handleBindGetMeta(sthis: SuperThis, logger: Logger, msg: BindGetMeta): Promise>; handleReqPutMeta(sthis: SuperThis, logger: Logger, msg: ReqPutMeta): Promise>; handleReqDelMeta(sthis: SuperThis, logger: Logger, msg: ReqDelMeta): Promise>; readonly headers: HttpHeader; } +// export interface Connected { +// readonly connId: QSId +// readonly ws: WSContextWithId; +// // readonly send: (msg: MsgBase) => Promisable; +// } + export abstract class HonoServerBase implements HonoServerImpl { readonly _gs: Gestalt; readonly sthis: SuperThis; @@ -68,7 +94,11 @@ export abstract class HonoServerBase implements HonoServerImpl { this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); } - abstract upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware; + abstract upgradeWebSocket( + createEvents: (c: Context) => WSEventsConnId | Promise> + ): ConnMiddleware; + + // abstract getConnected(): Connected[]; start(drop = false): Promise { return this.metaMerger.createSchema(drop).then(() => this); @@ -172,6 +202,26 @@ export const CORS = HttpHeader.from({ "Access-Control-Max-Age": "86400", // Cache pre-flight response for 24 hours }); +class NoBackChannel implements MsgDispatcherCtx { + readonly impl: HonoServerImpl; + readonly ctx: Context; + constructor(impl: HonoServerImpl, c: Context) { + this.impl = impl; + this.ctx = c; + } + get ws(): WSContextWithId { + return { + id: "no-id", + send: (msg: string | ArrayBuffer | Uint8Array): Promisable => { + return new Response(msg); + }, + } as unknown as WSContextWithId; + } + get wsRoom(): WSRoom { + throw new Error("NoBackChannel:wsRoom Method not implemented."); + } +} + export class HonoServer { // readonly sthis: SuperThis; // readonly msgP: MsgerParams; @@ -192,30 +242,34 @@ export class HonoServer { // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) app.put("/fp", (c) => - this.factory.inject(c, async ({ sthis, logger, impl }) => { + this.factory.inject(c, async ({ sthis, logger, impl, ende, wsRoom }) => { impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); const rMsg = await exception2Result(() => c.req.json() as Promise); if (rMsg.isErr()) { c.status(400); return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); } - const dispatcher = buildMsgDispatcher(sthis, impl.gestalt()); - return dispatcher.dispatch(impl, rMsg.Ok(), (msg) => c.json(msg)); + const dispatcher = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); + return dispatcher.dispatch(new NoBackChannel(impl, c), rMsg.Ok()); }) ); app.get("/ws", (c, next) => - this.factory.inject(c, async ({ sthis, logger, ende, impl }) => { + this.factory.inject(c, async ({ sthis, logger, ende, impl, wsRoom }) => { return impl.upgradeWebSocket((_c) => { let dp: MsgDispatcher; + // const id = sthis.nextId().str; + // console.log("upgradeWebSocket:inject:", id); return { onOpen: (_e, _ws) => { - dp = buildMsgDispatcher(sthis, impl.gestalt()); + dp = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); + // console.log("onOpen:inject:", id); }, onError: (error) => { logger.Error().Err(error).Msg("WebSocket error"); }, onMessage: async (event, ws) => { const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); + // console.log("onMessage:inject:", id, rMsg); if (rMsg.isErr()) { ws.send( ende.encode( @@ -230,14 +284,20 @@ export class HonoServer { ) ); } else { - await dp.dispatch(impl, rMsg.Ok(), (msg) => { - const str = ende.encode(msg); - ws.send(str); - return new Response(str); - }); + // console.log("dp-dispatch", rMsg.Ok(), dp); + await dp.dispatch( + { + impl, + ws, + wsRoom: dp.wsRoom, + }, + rMsg.Ok() + ); } }, - onClose: () => { + onClose: (_evt, _ws) => { + // impl.delConn(ws); + // console.log("onClose:inject:", id); dp = undefined as unknown as MsgDispatcher; // console.log('Connection closed') }, diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts index 17a07b34..7f41e391 100644 --- a/src/v2-cloud/http-connection.ts +++ b/src/v2-cloud/http-connection.ts @@ -29,6 +29,10 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec this.msgP = msgP; } + send(_msg: Q): Promise> { + throw new Error("Method not implemented."); + } + async start(): Promise> { // if (this._qsOpen.req) { // const sOpen = await this.request(this._qsOpen.req, { waitFor: MsgIsResOpen }); @@ -68,13 +72,11 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec state.timeout = setTimeout(() => this.#poll(state), state.bind.opts.pollInterval ?? 1000); } } catch (err) { - console.log("poll error", err); state.controller?.error(err); state.controller?.close(); } }) .catch((err) => { - console.log("poll catch error", err); state.controller?.error(err); // state.controller?.close(); }); diff --git a/src/v2-cloud/meta-merger/meta-merger.ts b/src/v2-cloud/meta-merger/meta-merger.ts index 5b7a62d5..83458624 100644 --- a/src/v2-cloud/meta-merger/meta-merger.ts +++ b/src/v2-cloud/meta-merger/meta-merger.ts @@ -1,10 +1,11 @@ -import { CRDTEntry, Logger } from "@fireproof/core"; +import { CRDTEntry } from "@fireproof/core"; import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; import { MetaSendSql } from "./meta-send.js"; import { TenantLedgerSql } from "./tenant-ledger.js"; import { TenantSql } from "./tenant.js"; import { SQLDatabase } from "./abstract-sql.js"; import { QSId, TenantLedger } from "../msg-types.js"; +import { Logger } from "@adviser/cement"; export interface Connection { readonly tenant: TenantLedger; diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts index dc39b54a..353fa8e4 100644 --- a/src/v2-cloud/msg-dispatch.ts +++ b/src/v2-cloud/msg-dispatch.ts @@ -1,10 +1,11 @@ import { Logger } from "@adviser/cement"; import { SuperThis, ensureLogger } from "@fireproof/core"; -import { Gestalt, MsgBase, buildErrorMsg, MsgWithError, MsgIsWithConn, MsgWithConn, QSId } from "./msg-types.js"; +import { Gestalt, MsgBase, buildErrorMsg, MsgWithError, MsgWithConn, QSId, EnDeCoder } from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; -import { HonoServerImpl } from "./hono-server.js"; +import { HonoServerImpl, WSContextWithId } from "./hono-server.js"; import { UnReg } from "./msger.js"; +import { WSRoom } from "./ws-room.js"; export interface MsgContext { calculatePreSignedUrl(p: PreSignedMsg): Promise; @@ -27,49 +28,33 @@ export class WSConnection { } } -type Promisable = T | Promise; +export type Promisable = T | Promise; // function WithValidConn(msg: T, rri?: ResOpen): msg is MsgWithConn { // return MsgIsWithConn(msg) && !!rri && rri.conn.resId === msg.conn.resId && rri.conn.reqId === msg.conn.reqId; // } -interface ConnItem { - conn: QSId; +export interface ConnItem { + readonly conn: QSId; touched: Date; + readonly ws: WSContextWithId; } -class ConnectionManager { - readonly conns = new Map(); - readonly maxItems: number; +// const connManager = new ConnectionManager(); - constructor(maxItems?: number) { - this.maxItems = maxItems || 100; - } - - addConn(conn: QSId): QSId { - if (this.conns.size >= this.maxItems) { - const oldest = Array.from(this.conns.values()); - const oneHourAgo = new Date(new Date().getTime() - 60 * 60 * 1000).getTime(); - oldest - .filter((item) => item.touched.getTime() < oneHourAgo) - .forEach((item) => this.conns.delete(item.conn.resId)); - } - this.conns.set(`${conn.reqId}:${conn.resId}`, { conn, touched: new Date() }); - return conn; - } - - isConnected(msg: MsgBase): msg is MsgWithConn { - if (!MsgIsWithConn(msg)) { - return false; - } - return this.conns.has(`${msg.conn.reqId}:${msg.conn.resId}`); - } +export interface ConnectionInfo { + readonly conn: WSConnection; + readonly reqId: string; + readonly resId: string; } -const connManager = new ConnectionManager(); export interface MsgDispatcherCtx { readonly impl: HonoServerImpl; + readonly ws: WSContextWithId; + readonly wsRoom: WSRoom; + // readonly send: (msg: MsgBase) => Promisable; } + export interface MsgDispatchItem { readonly match: (msg: MsgBase) => boolean; readonly isNotConn?: boolean; @@ -82,18 +67,23 @@ export class MsgDispatcher { // wsConn?: WSConnection; readonly gestalt: Gestalt; readonly id: string; + readonly ende: EnDeCoder; + + // readonly connManager = connManager; - readonly connManager = connManager; + readonly wsRoom: WSRoom; - static new(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { - return new MsgDispatcher(sthis, gestalt); + static new(sthis: SuperThis, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom): MsgDispatcher { + return new MsgDispatcher(sthis, gestalt, ende, wsRoom); } - private constructor(sthis: SuperThis, gestalt: Gestalt) { + private constructor(sthis: SuperThis, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom) { this.sthis = sthis; this.logger = ensureLogger(sthis, "Dispatcher"); this.gestalt = gestalt; this.id = sthis.nextId().str; + this.ende = ende; + this.wsRoom = wsRoom; } // addConn(msg: MsgBase): Result { @@ -114,26 +104,39 @@ export class MsgDispatcher { return () => ids.forEach((id) => this.items.delete(id)); } - async dispatch(ctx: HonoServerImpl, msg: MsgBase, send: (msg: MsgBase) => Promisable): Promise { + send(ws: WSContextWithId, msg: MsgBase) { + const str = this.ende.encode(msg); + ws.send(str); + return new Response(str); + } + + async dispatch(ctx: MsgDispatcherCtx, msg: MsgBase): Promise { + // console.log("dispatch-0", msg); const validateConn = async ( msg: T, fn: (msg: MsgWithConn) => Promisable> ): Promise => { - if (!connManager.isConnected(msg)) { - return send(buildErrorMsg(this.sthis, this.logger, { ...msg }, new Error("dispatch missing connection"))); + if (!ctx.wsRoom.isConnected(msg)) { + return this.send( + ctx.ws, + buildErrorMsg(this.sthis, this.logger, { ...msg }, new Error("dispatch missing connection")) + ); // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); } - // if (WithValidConn(msg, this.myOpen)) { const r = await fn(msg); - return Promise.resolve(send(r)); + return Promise.resolve(this.send(ctx.ws, r)); }; + // console.log("dispatch-1", msg); const found = Array.from(this.items.values()).find((item) => item.match(msg)); if (!found) { - return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); + // console.log("dispatch-2", msg); + return this.send(ctx.ws, buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); } if (!found.isNotConn) { - return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, { impl: ctx }, msg)); + // console.log("dispatch-3", msg); + return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, ctx, msg)); } - return send(await found.fn(this.sthis, this.logger, { impl: ctx }, msg)); + // console.log("dispatch-4", msg); + return this.send(ctx.ws, await found.fn(this.sthis, this.logger, ctx, msg)); } } diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts index 1645fc23..5b435044 100644 --- a/src/v2-cloud/msg-dispatcher-impl.ts +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -32,6 +32,14 @@ import { MsgWithConn, ReqGestalt, Gestalt, + EnDeCoder, + buildResChat, + ReqChat, + MsgIsReqChat, + qsidEqual, + MsgIsReqClose, + buildResClose, + ReqClose, } from "./msg-types.js"; import { BindGetMeta, @@ -41,33 +49,57 @@ import { ReqDelMeta, ReqPutMeta, } from "./msg-type-meta.js"; +import { WSRoom } from "./ws-room.js"; -export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt): MsgDispatcher { - const dp = MsgDispatcher.new(sthis, gestalt); +export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom): MsgDispatcher { + const dp = MsgDispatcher.new(sthis, gestalt, ende, wsRoom); dp.registerMsg( { match: MsgIsReqGestalt, isNotConn: true, fn: (_sthis, _logger, _ctx, msg: ReqGestalt) => { - return buildResGestalt(msg, dp.gestalt); + const resGestalt = buildResGestalt(msg, dp.gestalt); + return resGestalt; }, }, { match: MsgIsReqOpen, isNotConn: true, - fn: (sthis, logger, _ctx, msg) => { + fn: (sthis, logger, ctx, msg) => { if (!MsgIsReqOpenWithConn(msg)) { return buildErrorMsg(sthis, logger, msg, new Error("missing connection")); } - if (dp.connManager.isConnected(msg)) { + if (dp.wsRoom.isConnected(msg)) { return buildResOpen(sthis, msg, msg.conn.resId); } - const resId = sthis.nextId(12).str; + // const resId = sthis.nextId(12).str; + const resId = ctx.ws.id; const resOpen = buildResOpen(sthis, msg, resId); - dp.connManager.addConn(resOpen.conn); + dp.wsRoom.addConn(ctx.ws, resOpen.conn); return resOpen; }, }, + { + match: MsgIsReqClose, + fn: (_sthis, _logger, _ctx, msg: MsgWithConn) => { + dp.wsRoom.removeConn(msg.conn); + return buildResClose(msg, msg.conn); + }, + }, + { + match: MsgIsReqChat, + fn: (_sthis, _logger, _ctx, msg: MsgWithConn) => { + const conns = dp.wsRoom.getConns(msg.conn); + const ci = conns.map((c) => c.conn); + for (const conn of conns) { + if (qsidEqual(conn.conn, msg.conn)) { + continue; + } + dp.send(conn.ws, buildResChat(msg, conn.conn, `[${msg.conn.reqId}]: ${msg.message}`, ci)); + } + return buildResChat(msg, msg.conn, `ack: ${msg.message}`, ci); + }, + }, { match: MsgIsReqGetData, fn: (sthis, logger, ctx, msg: MsgWithConn) => { diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts index 43664e83..3ec3e2a6 100644 --- a/src/v2-cloud/msg-types.ts +++ b/src/v2-cloud/msg-types.ts @@ -1,5 +1,5 @@ -import { Future } from "@adviser/cement"; -import { Logger, SuperThis } from "@fireproof/core"; +import { Future, Logger } from "@adviser/cement"; +import { SuperThis } from "@fireproof/core"; import { CalculatePreSignedUrl } from "./msg-types-data.js"; import { PreSignedMsg } from "./pre-signed-url.js"; @@ -59,6 +59,14 @@ export interface QSId { readonly resId: string; } +export function qsidEqual(a: QSId, b: QSId): boolean { + return a.reqId === b.reqId && a.resId === b.resId; +} + +export function qsidKey(qsid: QSId): string { + return `${qsid.reqId}:${qsid.resId}`; +} + // export interface Connection extends ReqResId{ // readonly key: TenantLedger; // } @@ -256,6 +264,47 @@ export function defaultGestalt(msgP: MsgerParams, gestalt: GestaltParam): Gestal }; } +export interface ReqChat extends MsgWithConn { + readonly type: "reqChat"; + readonly message: string; + readonly targets: QSId[]; +} +export interface ResChat extends MsgWithConn { + readonly type: "resChat"; + readonly message: string; + readonly targets: QSId[]; +} + +export function buildReqChat(sthis: NextId, conn: QSId, message: string, targets?: QSId[]): ReqChat { + return { + tid: sthis.nextId().str, + type: "reqChat", + version: VERSION, + conn, + message, + targets: targets ?? [], + }; +} + +export function buildResChat(req: ReqChat, conn?: QSId, message?: string, targets?: QSId[]): ResChat { + return { + ...req, + conn: conn || req.conn, + message: message || req.message, + targets: targets || req.targets, + type: "resChat", + version: VERSION, + }; +} + +export function MsgIsReqChat(msg: MsgBase): msg is ReqChat { + return msg.type === "reqChat"; +} + +export function MsgIsResChat(msg: MsgBase): msg is ResChat { + return msg.type === "resChat"; +} + /** * The ReqGestalt message is used to request the * features of the Responder. @@ -263,21 +312,26 @@ export function defaultGestalt(msgP: MsgerParams, gestalt: GestaltParam): Gestal export interface ReqGestalt extends MsgBase { readonly type: "reqGestalt"; readonly gestalt: Gestalt; + readonly publish?: boolean; // for testing } export function MsgIsReqGestalt(msg: MsgBase): msg is ReqGestalt { return msg.type === "reqGestalt"; } -export function buildReqGestalt(sthis: NextId, gestalt: Gestalt): ReqGestalt { +export function buildReqGestalt(sthis: NextId, gestalt: Gestalt, publish?: boolean): ReqGestalt { return { tid: sthis.nextId().str, type: "reqGestalt", version: VERSION, gestalt, + publish, }; } +export interface ConnInfo { + readonly connIds: string[]; +} /** * The ResGestalt message is used to respond with * the features of the Responder. @@ -370,7 +424,7 @@ export function MsgIsResOpen(msg: MsgBase): msg is ResOpen { return msg.type === "resOpen"; } -export interface ReqClose extends Omit { +export interface ReqClose extends MsgWithConn { readonly type: "reqClose"; } @@ -378,7 +432,7 @@ export function MsgIsReqClose(msg: MsgBase): msg is ReqClose { return msg.type === "reqClose" && MsgIsWithConn(msg); } -export interface ResClose extends Omit { +export interface ResClose extends MsgWithConn { readonly type: "resClose"; } @@ -386,6 +440,23 @@ export function MsgIsResClose(msg: MsgBase): msg is ResClose { return msg.type === "resClose" && MsgIsWithConn(msg); } +export function buildResClose(req: ReqClose, conn: QSId): ResClose { + return { + ...req, + type: "resClose", + conn, + }; +} + +export function buildReqClose(sthis: NextId, conn: QSId): ReqClose { + return { + tid: sthis.nextId().str, + type: "reqClose", + version: VERSION, + conn, + }; +} + export interface SignedUrlParam { readonly method: HttpMethods; readonly store: FPStoreTypes; diff --git a/src/v2-cloud/msger.ts b/src/v2-cloud/msger.ts index d6ea4da2..a53bd568 100644 --- a/src/v2-cloud/msger.ts +++ b/src/v2-cloud/msger.ts @@ -19,6 +19,8 @@ import { QSId, MsgIsTid, ReqGestalt, + buildReqClose, + MsgIsResClose, } from "./msg-types.js"; import { SuperThis } from "@fireproof/core"; import { HttpConnection } from "./http-connection.js"; @@ -74,6 +76,7 @@ export interface MsgRawConnection { readonly activeBinds: Map>; bind(req: Q, opts: RequestOpts): ReadableStream>; request(req: Q, opts: RequestOpts): Promise>; + send(msg: Q): Promise>; start(): Promise>; close(): Promise>; onMsg(msg: OnMsgFn): UnReg; @@ -138,12 +141,14 @@ export class MsgConnected implements MsgRawConnection { readonly raw: MsgRawConnection; readonly exchangedGestalt: ExchangedGestalt; readonly activeBinds: Map>; + readonly id: string; private constructor(raw: MsgRawConnection, conn: QSId) { this.sthis = raw.sthis; this.raw = raw; this.exchangedGestalt = raw.exchangedGestalt; this.conn = conn; this.activeBinds = raw.activeBinds; + this.id = this.sthis.nextId().str; } bind( @@ -163,7 +168,7 @@ export class MsgConnected implements MsgRawConnection { } }, }); - // eslint-disable-next-line no-console + // why the hell pipeTo sends an error that is undefined? stream.pipeThrough(ts); // stream.pipeTo(ts.writable).catch((err) => err && err.message && console.error("bind error", err)); @@ -173,11 +178,18 @@ export class MsgConnected implements MsgRawConnection { request(req: Q, opts: RequestOpts): Promise> { return this.raw.request({ ...req, conn: req.conn || this.conn }, opts); } + + send(msg: Q): Promise> { + return this.raw.send({ ...msg, conn: msg.conn || this.conn }); + } + start(): Promise> { return this.raw.start(); } - close(): Promise> { - return this.raw.close(); + async close(): Promise> { + await this.request(buildReqClose(this.sthis, this.conn), { waitFor: MsgIsResClose }); + return await this.raw.close(); + // return Result.Ok(undefined); } onMsg(msgFn: OnMsgFn): UnReg { return this.raw.onMsg((msg) => { @@ -208,7 +220,7 @@ export class Msger { ): Promise> { let ws: WebSocket; // const { encode } = jsonEnDe(sthis); - url = url.build().URI(); + url = url.build().setParam("random", sthis.nextId().str).URI(); // .setParam("reqOpen", sthis.txt.decode(encode(qOpen))) if (runtimeFn().isNodeIsh) { const { WebSocket } = await import("ws"); diff --git a/src/v2-cloud/node-hono-server.ts b/src/v2-cloud/node-hono-server.ts index 53f75a1f..5039cb5e 100644 --- a/src/v2-cloud/node-hono-server.ts +++ b/src/v2-cloud/node-hono-server.ts @@ -1,12 +1,30 @@ -import { UpgradeWebSocket, WSContext, WSContextInit, WSEvents } from "hono/ws"; -import { ConnMiddleware, HonoServerBase, HonoServerFactory, HonoServerImpl, RunTimeParams } from "./hono-server.js"; +import { UpgradeWebSocket, WSContext, WSEvents, WSMessageReceive } from "hono/ws"; +import { + ConnMiddleware, + HonoServerBase, + HonoServerFactory, + HonoServerImpl, + RunTimeParams, + WSContextWithId, + WSEventsConnId, +} from "./hono-server.js"; import { HttpHeader, URI } from "@adviser/cement"; import { Context, Hono } from "hono"; import { ensureLogger, SuperThis } from "@fireproof/core"; import { defaultMsgParams, jsonEnDe } from "./msger.js"; -import { defaultGestalt, Gestalt, MsgerParams } from "./msg-types.js"; +import { + defaultGestalt, + Gestalt, + MsgBase, + MsgerParams, + MsgIsWithConn, + MsgWithConn, + QSId, + qsidKey, +} from "./msg-types.js"; import { SQLDatabase } from "./meta-merger/abstract-sql.js"; import { WSRoom } from "./ws-room.js"; +import { ConnItem } from "./msg-dispatch.js"; interface ServerType { close(fn: () => void): void; @@ -20,34 +38,113 @@ export interface NodeHonoFactoryParams { readonly sql: SQLDatabase; } -const wsConnections = new Map(); +// const wsConnections = new Map>(); class NodeWSRoom implements WSRoom { readonly sthis: SuperThis; + readonly id: string; + + readonly _conns = new Map(); constructor(sthis: SuperThis) { this.sthis = sthis; + this.id = sthis.nextId(12).str; } - acceptConnection(ws: WebSocket, wse: WSEvents): Promise { + + getConns(): ConnItem[] { + return Array.from(this._conns.values()); + } + removeConn(conn: QSId): void { + // console.log("removeConn", this.id, qsidKey(conn)); + this._conns.delete(qsidKey(conn)); + } + addConn(ws: WSContextWithId, conn: QSId): QSId { + // console.log("addConn", this.id, qsidKey(conn)); + const key = qsidKey(conn); + let ci = this._conns.get(key); + if (!ci) { + ci = { ws, conn, touched: new Date() }; + this._conns.set(key, ci); + } + return ci.conn; + } + + isConnected(msg: MsgBase): msg is MsgWithConn { + if (!MsgIsWithConn(msg)) { + return false; + } + return this._conns.has(qsidKey(msg.conn)); + } + + // addConn(ws: WSContextWithId): void { + // wsConnections.add(ws); + // } + + // delConn(ws: WSContextWithId): void { + // wsConnections.delete(ws); + // } + + // #ensureWSContextWithId(id: string, ws: WSContext) { + // let wsId = wsConnections.get(id); + // if (wsId) { + // return wsId; + // } + // wsId = new WSContextWithId(this.sthis.nextId(12).str, ws); + // wsConnections.set(id, wsId); + // return wsId; + // } + + createEvents(outer: WSEventsConnId): (c: Context) => WSEvents { const id = this.sthis.nextId(12).str; - wsConnections.set(id, ws); + return (_c: Context) => ({ + onOpen: (evt: Event, ws: WSContext) => { + // console.log("onOpen", id); + outer.onOpen(evt, new WSContextWithId(id, ws)); + }, + onMessage: (evt: MessageEvent, ws: WSContext) => { + outer.onMessage(evt, new WSContextWithId(id, ws)); + }, + onClose: (evt: CloseEvent, ws: WSContext) => { + // console.log("onClose", id); + outer.onClose(evt, new WSContextWithId(id, ws)); + // wsConnections.delete(id); + }, + onError: (evt: Event, ws: WSContext) => { + outer.onError(evt, new WSContextWithId(id, ws)); + }, + }); + } - const wsCtx = new WSContext(ws as WSContextInit); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + acceptConnection(ws: WebSocket, wse: WSEvents): Promise { + // const id = this.sthis.nextId(12).str; + // wsConnections.set(id, ws); + // this. - ws.onerror = (err) => { - // console.log("onerror", err); - wse.onError?.(err, wsCtx); - }; - ws.onclose = (ev) => { - // console.log("onclose", ev); - wse.onClose?.(ev, wsCtx); - }; - ws.onmessage = (evt) => { - // console.log("onmessage", evt); - // wsCtx.send("Hellox from server"); - wse.onMessage?.(evt, wsCtx); - }; + throw new Error("Method not implemented."); + // const wsCtx = new WSContextWithId(this.sthis.nextId(12).str, ws as WSContextInit); + + // console.log("acceptConnection", wsCtx); + // ws.onopen = function(this, ev) { + // console.log("onopen", ev); + // wsConnections.set(wsCtx.id, wsCtx); + // wse.onOpen?.(ev, wsCtx); + // } + // ws.onerror = (err) => { + // console.log("onerror", err); + // wse.onError?.(err, wsCtx); + // }; + // ws.onclose = function(this, ev) { + // console.log("onclose", ev); + // wse.onClose?.(ev, wsCtx); + // wsConnections.delete(wsCtx.id); + // }; + // ws.onmessage = (evt) => { + // console.log("onmessage", evt); + // // wsCtx.send("Hellox from server"); + // wse.onMessage?.(evt, wsCtx); + // }; - ws.accept(); - return Promise.resolve(); + // ws.accept(); + // return Promise.resolve(); } } @@ -56,6 +153,8 @@ export class NodeHonoFactory implements HonoServerFactory { _injectWebSocket!: (t: unknown) => void; _serve!: serveFn; _server!: ServerType; + + readonly _wsRoom: NodeWSRoom; // _env!: Env; readonly sthis: SuperThis; @@ -63,6 +162,7 @@ export class NodeHonoFactory implements HonoServerFactory { constructor(sthis: SuperThis, params: NodeHonoFactoryParams) { this.sthis = sthis; this.params = params; + this._wsRoom = new NodeWSRoom(sthis); } // eslint-disable-next-line @typescript-eslint/no-invalid-void-type @@ -85,9 +185,8 @@ export class NodeHonoFactory implements HonoServerFactory { defaultGestalt(msgP, { id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", }); - const wsRoom = new NodeWSRoom(sthis); - const nhs = new NodeHonoServer(sthis, this, gs, this.params.sql, wsRoom); - return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs })); + const nhs = new NodeHonoServer(sthis, this, gs, this.params.sql, this._wsRoom); + return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs, wsRoom: this._wsRoom })); } async start(app: Hono): Promise { @@ -121,6 +220,7 @@ export class NodeHonoFactory implements HonoServerFactory { export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { readonly _upgradeWebSocket: UpgradeWebSocket; + // readonly wsRoom: NodeWSRoom; constructor( sthis: SuperThis, factory: NodeHonoFactory, @@ -133,10 +233,21 @@ export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { this._upgradeWebSocket = factory._upgradeWebSocket; } - override upgradeWebSocket(createEvents: (c: Context) => WSEvents | Promise): ConnMiddleware { + // upgradeWebSocket(createEvents: (c: Context) => WSEventsConnId | Promise>): ConnMiddleware { + upgradeWebSocket( + createEvents: (c: Context) => WSEventsConnId | Promise> + ): ConnMiddleware { return async (_conn, c, next) => { - // conn.attachWSPair({ client: c.req, server: c.res }); - return this._upgradeWebSocket(createEvents)(c, next); + const wse = await createEvents(c); + return this._upgradeWebSocket((this.wsRoom as NodeWSRoom).createEvents(wse))(c, next); }; } + + // override getConnected(): Connected[] { + // // console.log("getConnected", wsConnections.size); + // return Array.from(wsConnections.values()).map(m => ({ + // connId: m.id, + // ws: m, + // })) + // } } diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 144ff493..02250086 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -1,6 +1,6 @@ import { Future, Result, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; -import { $, fs } from "zx"; +import { $, fs, sleep } from "zx"; import { HttpConnection } from "./http-connection.js"; import { MsgerParams, @@ -150,7 +150,7 @@ export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnD } export async function resolveToml(backend: "D1" | "DO") { - const tomlFile = "src/cloud/backend/wrangler.toml"; + const tomlFile = "src/v2-cloud/backend/wrangler.toml"; const tomeStr = await fs.readFile(tomlFile, "utf-8"); const wranglerFile = toml.parse(tomeStr) as unknown as { env: Record; @@ -176,6 +176,7 @@ export function NodeHonoServerFactory() { }, }; } + export function CFHonoServerFactory(backend: "D1" | "DO") { return { name: `CFHonoServer(${backend})`, @@ -198,7 +199,7 @@ export function CFHonoServerFactory(backend: "D1" | "DO") { if (mightPid) { pid = +mightPid; } - if (chunk.includes("Ready on http")) { + if (chunk.includes("Starting local serv")) { waitReady.resolve(true); } }); @@ -207,6 +208,7 @@ export function CFHonoServerFactory(backend: "D1" | "DO") { console.error("!!", chunk.toString()); }); await waitReady.asPromise(); + await sleep(300); return new HonoServer( new CFHonoFactory(() => { if (pid) process.kill(pid); diff --git a/src/v2-cloud/ws-connection.ts b/src/v2-cloud/ws-connection.ts index 5c29cfd7..0811b4af 100644 --- a/src/v2-cloud/ws-connection.ts +++ b/src/v2-cloud/ws-connection.ts @@ -74,6 +74,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti }; this.ws.onclose = () => { this.opened = false; + // console.log("onclose", this.id); this.close().catch((ierr) => { const err = this.logger.Error().Err(ierr).Msg("close error").AsError(); onOpenFuture.resolve(Result.Err(err)); @@ -103,7 +104,10 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti } const msg = rMsg.Ok(); const waitFor = this.waitForTid.get(msg.tid); - this.#onMsg.forEach((cb) => cb(msg)); + Array.from(this.#onMsg.values()).forEach((cb) => { + // console.log("cb-onmessage", this.id, msg, cb.toString()); + cb(msg); + }); if (waitFor) { if (MsgIsError(msg)) { this.waitForTid.delete(msg.tid); @@ -133,9 +137,9 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti return msg; } - sendMsg(msg: MsgBase): Promise { + send(msg: Q): Promise { this.ws.send(this.msgP.ende.encode(msg)); - return Promise.resolve(); + return Promise.resolve(msg as unknown as S); } onMsg(fn: OnMsgFn): UnReg { @@ -180,7 +184,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti controller.enqueue(msg); } }); - this.sendMsg(req); + this.send(req); const future = new Future(); this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); future.asPromise().then((msg) => { @@ -200,7 +204,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti } const future = new Future(); this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); - await this.sendMsg(req); + await this.send(req); return future.asPromise(); } diff --git a/src/v2-cloud/ws-room.ts b/src/v2-cloud/ws-room.ts index ebe8e33b..02cd4c60 100644 --- a/src/v2-cloud/ws-room.ts +++ b/src/v2-cloud/ws-room.ts @@ -1,5 +1,50 @@ -import { WSEvents } from "hono/ws"; +import { WSContextWithId } from "./hono-server.js"; +import { MsgBase, MsgWithConn, QSId } from "./msg-types.js"; +import { ConnItem } from "./msg-dispatch.js"; export interface WSRoom { - acceptConnection(ws: WebSocket, wse: WSEvents): Promise; + // acceptConnection(ws: WebSocket, wse: WSEvents, ctx: CTX): Promise; + + getConns(conn: QSId): ConnItem[]; + removeConn(conn: QSId): void; + addConn(ws: WSContextWithId, conn: QSId): QSId; + isConnected(msg: MsgBase): msg is MsgWithConn; } + +// class ConnectionManager { +// readonly conns = new Map(); +// readonly maxItems: number; + +// constructor(maxItems?: number) { +// this.maxItems = maxItems || 100; +// } + +// getConns(): ConnItem[] { +// console.log("getConns", this.conns); +// return Array.from(this.conns.values()); +// } + +// removeConn(conn: QSId): void { +// this.conns.delete(qsidKey(conn)); +// } + +// addConn(ws: WSContextWithId, conn: QSId): QSId { +// console.log("addConn", conn); +// if (this.conns.size >= this.maxItems) { +// const oldest = Array.from(this.conns.values()); +// const oneHourAgo = new Date(new Date().getTime() - 60 * 60 * 1000).getTime(); +// oldest +// .filter((item) => item.touched.getTime() < oneHourAgo) +// .forEach((item) => this.conns.delete(item.conn.resId)); +// } +// this.conns.set(qsidKey(conn), { ws, conn, touched: new Date() }); +// return conn; +// } + +// isConnected(msg: MsgBase): msg is MsgWithConn { +// if (!MsgIsWithConn(msg)) { +// return false; +// } +// return this.conns.has(`${msg.conn.reqId}:${msg.conn.resId}`); +// } +// } diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts new file mode 100644 index 00000000..c71039fc --- /dev/null +++ b/src/v2-cloud/ws-sockets.test.ts @@ -0,0 +1,78 @@ +import { ensureSuperThis } from "@fireproof/core"; + +import { CFHonoServerFactory, NodeHonoServerFactory, wsStyle } from "./test-helper.js"; +import { defaultMsgParams, Msger } from "./msger.js"; +import { buildReqChat, defaultGestalt, MsgIsResChat } from "./msg-types.js"; +import { Hono } from "hono"; +import { HonoServer } from "./hono-server.js"; +import { Future } from "@adviser/cement"; + +describe("test multiple connections", () => { + const sthis = ensureSuperThis(); + + describe.each([ + // dummy + NodeHonoServerFactory(), + CFHonoServerFactory("D1"), + ])("$name - Gateway", ({ factory }) => { + const msgP = defaultMsgParams(sthis, { hasPersistent: true }); + const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); + const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); + const stype = wsStyle(sthis, port, msgP, my); + const connections = 3; + + let hserv: HonoServer; + + beforeAll(async () => { + const app = new Hono(); + hserv = await factory(sthis, msgP, stype.remoteGestalt, port).then((srv) => srv.register(app, port)); + }); + afterAll(async () => { + await hserv.close(); + }); + + it("could open multiple connections", async () => { + const conns = await Promise.all( + Array(connections) + .fill(0) + .map(() => { + return Msger.connect(sthis, "http://localhost:" + port + "/fp"); + }) + ).then((cs) => cs.map((c) => c.Ok())); + + const ready = new Future(); + let total = (connections * (connections + 1)) / 2; + // const recvSet = new Set(conns.map((c) => c.conn.reqId)); + for (const c of conns) { + c.onMsg((m) => { + if (MsgIsResChat(m)) { + // console.log("Got a chat response", total--, qsidKey(m.conn)); + total--; + if (total === 0) { + ready.resolve(); + } + // recvSet.delete(m.conn.reqId); + // if (recvSet.size === 0) { + // ready.resolve(); + // } + } + }); + } + + const rest = [...conns]; + for (const c of conns) { + const act = await c.request(buildReqChat(sthis, c.conn, "Hello"), { waitFor: MsgIsResChat }); + if (MsgIsResChat(act)) { + expect(act.targets.length).toBe(rest.length); + } else { + assert.fail("Expected a response"); + } + await c.close(); + rest.shift(); + } + + // await conns[0].send(buildReqGestalt(sthis, my, true)); + await ready.asPromise(); + }); + }); +}); diff --git a/tests/Dockerfile.connect-netlify b/tests/Dockerfile.connect-netlify index 29d10c8b..0fd4b15d 100644 --- a/tests/Dockerfile.connect-netlify +++ b/tests/Dockerfile.connect-netlify @@ -1,14 +1,21 @@ -FROM node:20-slim AS base +FROM node:22-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN npm install -g corepack@latest RUN corepack enable -RUN apt update && apt install -y git unzip curl -RUN curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && deno upgrade 1.44.4 -#-- --no-modify-path --yes -COPY src/netlify /usr/src/app/src/netlify -COPY tests/connect-netlify/app /usr/src/app/tests/connect-netlify/app -COPY dist/netlify/server.js /usr/src/app/tests/connect-netlify/app/netlify/edge-functions/fireproof.js -WORKDIR /usr/src/app/tests/connect-netlify/app -RUN rm -rf node_modules && pnpm install && pnpm run copy-server && pnpm install -g netlify-cli -CMD pnpm dev --no-open +RUN apt update && apt install -y git unzip curl python3 make g++ +RUN curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && deno upgrade 1.46.3 + +COPY src /usr/src/app/src + +WORKDIR /usr/src/app + +RUN pnpm init && \ + pnpm install -g netlify netlify-cli json \ + --allow-build @parcel/watcher --allow-build esbuild \ + --allow-build netlify-cli --allow-build sharp \ + --allow-build unix-dgram && \ + pnpm add @netlify/blobs @netlify/functions && \ + json -I -f package.json -e 'this.type="module"' + +CMD ["netlify","functions:serve","--functions","src/netlify/backend", "--port", "8888"] diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index f3eb3b0e..e71b2dc3 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -143,7 +143,8 @@ services: wait-for-ready: image: curlimages/curl - command: "sh -c 'while [ ! -e healthy ] ; do curl -f http://netlify:8888 > netlify && curl -f http://minio:9000/minio/health/live > minio && curl -f http://ucan:8787/health > ucan && curl -f http://partykit:1999 > partykit && curl -f http://v1-cloud:1998/health > v1-cloud && echo ready > ready ; ls; sleep 5; done ; sleep 60'" + # curl -f \"http://netlify:8888/fireproof?health=1\" > netlify && + command: "sh -c 'while [ ! -e healthy ] ; do curl -f http://minio:9000/minio/health/live > minio && curl -f http://ucan:8787/health > ucan && curl -f http://partykit:1999 > partykit && curl -f http://v1-cloud:1998/health > v1-cloud && echo ready > ready ; ls; sleep 5; done ; sleep 60'" depends_on: netlify: condition: service_started diff --git a/vitest.cf-worker.config.ts b/vitest.cf-worker.config.ts index de606e3f..42f0c2d6 100644 --- a/vitest.cf-worker.config.ts +++ b/vitest.cf-worker.config.ts @@ -6,12 +6,12 @@ export default defineWorkersConfig({ test: { poolOptions: { workers: { - wrangler: { configPath: "./src/cloud/backend/wrangler.toml", environment: "test" }, + wrangler: { configPath: "./src/v2-cloud/backend/wrangler.toml", environment: "test" }, }, }, name: "cf-worker", exclude: ["node_modules/@fireproof/core/tests/react/**"], - include: ["src/cloud/meta-merger/*.test.ts"], + include: ["src/v2-cloud/meta-merger/*.test.ts"], globals: true, setupFiles: "./setup.cf-kv.ts", }, diff --git a/vitest.libsql.config.ts b/vitest.libsql.config.ts index 778772d7..8665bdce 100644 --- a/vitest.libsql.config.ts +++ b/vitest.libsql.config.ts @@ -11,6 +11,11 @@ export default defineConfig({ "node_modules/@fireproof/core/tests/react/**", "node_modules/@fireproof/core/tests/fireproof/config.test.ts", "node_modules/@fireproof/core/tests/fireproof/utils.test.ts", + "node_modules/@fireproof/core/tests/blockstore/interceptor-gateway.test.ts", + "node_modules/@fireproof/core/tests/gateway/indexeddb/loader-config.test.ts", + "node_modules/@fireproof/core/tests/gateway/file/loader-config.test.ts", + "node_modules/@fireproof/core/tests/blockstore/keyed-crypto-indexeddb-file.test.ts", + "node_modules/@fireproof/core/tests/blockstore/keyed-crypto.test.ts", ], globals: true, setupFiles: "./setup.libsql.ts", diff --git a/vitest.v2-cloud.config.ts b/vitest.v2-cloud.config.ts index e4a250ab..43441c8d 100644 --- a/vitest.v2-cloud.config.ts +++ b/vitest.v2-cloud.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [tsconfigPaths()], test: { - name: "cloud", + name: "v2-cloud", exclude: [ "node_modules/@fireproof/core/tests/react/**", "node_modules/@fireproof/core/tests/fireproof/config.test.ts", @@ -16,10 +16,10 @@ export default defineConfig({ // "node_modules/@fireproof/core/tests/**/*test.?(c|m)[jt]s?(x)", // "node_modules/@fireproof/core/tests/**/*gateway.test.?(c|m)[jt]s?(x)", // "src/connector.test.ts", - "src/cloud/**/*test.?(c|m)[jt]s?(x)", + "src/v2-cloud/**/*test.?(c|m)[jt]s?(x)", ], globals: true, - setupFiles: "./setup.cloud.ts", + setupFiles: "./setup.v2-cloud.ts", testTimeout: 25000, // poolOptions: { // workers: { wrangler: { configPath: './src/cloud/backend/wrangler.toml' } }, diff --git a/vitest.workspace.ts b/vitest.workspace.ts index c50b34ab..71fef3bd 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -7,6 +7,7 @@ import libsql from "./vitest.libsql.config.ts"; // import nodeSqlite3Wasm from "./vitest.node-sqlite3-wasm.config.ts"; import partykit from "./vitest.partykit.config.ts"; import v1Cloud from "./vitest.v1-cloud.config.ts"; +import v2Cloud from "./vitest.v2-cloud.config.ts"; import s3 from "./vitest.s3.config.ts"; // import connector from "./vitest.connector.config.ts"; // import netlify from "./vitest.netlify.config.ts"; @@ -27,6 +28,7 @@ export default defineWorkspace([ // netlify, partykit, v1Cloud, + v2Cloud, cfWorker, ucan, ]); From 70b3f888d2d6938e542d75d4dc7b104a45531063 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 4 Mar 2025 17:31:59 +0100 Subject: [PATCH 04/14] chore: remove netlify server copy --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a5f8421..4c38ecdd 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "pub:noop": "echo", "pub:aws": "tsx ./version-copy-package.ts ./dist/aws/package-aws.json && cp ./src/aws/README.md ./dist/aws/", "pub:v1-cloud": "tsx ./version-copy-package.ts ./dist/v1-cloud/package-v1-cloud.json && cp ./src/v1-cloud/README.md ./dist/v1-cloud/", - "pub:netlify": "tsx ./version-copy-package.ts ./dist/netlify/package-netlify.json && cp ./src/netlify/server.ts ./dist/netlify/ && cp ./src/netlify/README.md ./dist/netlify/", + "pub:netlify": "tsx ./version-copy-package.ts ./dist/netlify/package-netlify.json && cp ./src/netlify/README.md ./dist/netlify/", "pub:s3": "tsx ./version-copy-package.ts ./dist/s3/package-s3.json && cp ./src/s3/README.md ./dist/s3/", "pub:partykit": "tsx ./version-copy-package.ts ./dist/partykit/package-partykit.json && cp ./src/partykit/server.ts ./dist/partykit/ && cp ./src/partykit/README.md ./dist/partykit/", "pub:ucan": "tsx ./version-copy-package.ts ./dist/ucan/package-ucan.json && cp ./src/ucan/README.md ./dist/ucan/", From f0fe8f512c3cfb950dd3191b49b8b160558da619 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 5 Mar 2025 14:33:13 +0100 Subject: [PATCH 05/14] chore: before ctx change --- src/aws/backend/app.ts | 2 +- src/v2-cloud/backend/cf-dobj-abstract-sql.ts | 10 +- src/v2-cloud/backend/cf-hono-server.ts | 309 +++++++++--------- src/v2-cloud/backend/server.ts | 27 +- src/v2-cloud/backend/wrangler.toml | 4 + src/v2-cloud/client/cloud-gateway.test.ts | 2 +- src/v2-cloud/connection.test.ts | 2 +- src/v2-cloud/hono-server.ts | 208 +++++++----- .../meta-merger/meta-by-tenant-ledger.ts | 24 +- src/v2-cloud/meta-merger/meta-merger.test.ts | 4 +- src/v2-cloud/meta-merger/meta-merger.ts | 9 +- src/v2-cloud/meta-merger/meta-send.ts | 80 +++-- src/v2-cloud/meta-merger/tenant-ledger.ts | 18 +- src/v2-cloud/meta-merger/tenant.ts | 18 +- src/v2-cloud/msg-dispatch.ts | 27 +- src/v2-cloud/msg-dispatcher-impl.ts | 1 + src/v2-cloud/node-hono-server.ts | 7 +- src/v2-cloud/test-helper.ts | 2 +- src/v2-cloud/ws-sockets.test.ts | 4 +- 19 files changed, 425 insertions(+), 333 deletions(-) diff --git a/src/aws/backend/app.ts b/src/aws/backend/app.ts index 66f3d954..43d0dd12 100644 --- a/src/aws/backend/app.ts +++ b/src/aws/backend/app.ts @@ -17,7 +17,7 @@ // 'use strict' // import AWS from 'aws-sdk' // import { DynamoDB, Lambda, S3 } from 'aws-sdk'; -import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { InvocationRequest, Lambda } from "@aws-sdk/client-lambda"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; diff --git a/src/v2-cloud/backend/cf-dobj-abstract-sql.ts b/src/v2-cloud/backend/cf-dobj-abstract-sql.ts index 37e44ab6..2d838dec 100644 --- a/src/v2-cloud/backend/cf-dobj-abstract-sql.ts +++ b/src/v2-cloud/backend/cf-dobj-abstract-sql.ts @@ -6,16 +6,20 @@ import { ExecSQLResult, FPBackendDurableObject } from "./server.js"; export class CFDObjSQLStatement implements SQLStatement { readonly sql: string; readonly db: CFDObjSQLDatabase; - constructor(db: CFDObjSQLDatabase, sql: string) { + readonly isSchema: boolean; + constructor(db: CFDObjSQLDatabase, sql: string, isSchema = false) { this.db = db; this.sql = sql; + this.isSchema = isSchema; } async run(...params: SQLParams): Promise { - const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; + // console.log("CFDObjSQLStatement.run", this.sql, params); + const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params), this.isSchema)) as ExecSQLResult; return res.rawResults[0] as T; } async all(...params: SQLParams): Promise { - const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params))) as ExecSQLResult; + // console.log("CFDObjSQLStatement.all", this.sql, params); + const res = (await this.db.dobj.execSql(this.sql, sqliteCoerceParams(params), this.isSchema)) as ExecSQLResult; return res.rawResults as T[]; } } diff --git a/src/v2-cloud/backend/cf-hono-server.ts b/src/v2-cloud/backend/cf-hono-server.ts index 4e3b7008..5b7d25e5 100644 --- a/src/v2-cloud/backend/cf-hono-server.ts +++ b/src/v2-cloud/backend/cf-hono-server.ts @@ -1,4 +1,4 @@ -import { BuildURI, HttpHeader, KeyedResolvOnce, Logger, LoggerImpl, URI } from "@adviser/cement"; +import { BuildURI, HttpHeader, Logger, LoggerImpl, URI } from "@adviser/cement"; import { Context, Hono } from "hono"; import { ConnMiddleware, @@ -31,25 +31,26 @@ import { WSRoom } from "../ws-room.js"; import { FPBackendDurableObject, FPRoomDurableObject } from "./server.js"; import { ConnItem } from "../msg-dispatch.js"; -const startedChs = new KeyedResolvOnce(); +// const startedChs = new KeyedResolvOnce(); -export function getBackendDurableObject(env: Env) { +export function getBackendDurableObject(env: Env, _id: string) { // console.log("getDurableObject", env); const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; + // console.log("getBackendDurableObject", cfBackendKey, id); const rany = env as unknown as Record>; const dObjNs = rany[cfBackendKey]; - const id = dObjNs.idFromName(env.FP_BACKEND_DO_ID ?? cfBackendKey); - return dObjNs.get(id); + const did = dObjNs.idFromName(env.FP_BACKEND_DO_ID ?? cfBackendKey); + return dObjNs.get(did); } -export function getRoomDurableObject(env: Env) { - // console.log("getDurableObject", env); +export function getRoomDurableObject(env: Env, _id: string) { const cfBackendKey = env.CF_BACKEND_KEY ?? "FP_WS_ROOM"; + // console.log("getRoomDurableObject", cfBackendKey, id); const rany = env as unknown as Record>; // console.log("getRoomDurableObject", cfBackendKey); const dObjNs = rany[cfBackendKey]; - const id = dObjNs.idFromName(cfBackendKey); - return dObjNs.get(id); + const did = dObjNs.idFromName(cfBackendKey); + return dObjNs.get(did); } function webSocket2WSContextInit(ws: WebSocket): WSContextInit { @@ -65,52 +66,71 @@ function webSocket2WSContextInit(ws: WebSocket): WSContextInit { }; } -const eventsWithConnId = new Map< - string, - { - getWebSockets?: () => WebSocket[]; - events?: WSEventsConnId; - } ->(); +// const eventsWithConnId = new Map< +// string, +// { +// getWebSockets?: () => WebSocket[]; +// events?: WSEventsConnId; +// } +// >(); + class CFWSRoom implements WSRoom { readonly sthis: SuperThis; readonly id: string; - readonly eventsWithConnId = eventsWithConnId; + // readonly eventsWithConnId = eventsWithConnId; + + isWebsocket = false; + readonly notWebSockets: ConnItem[] = []; + + #events?: WSEventsConnId; constructor(sthis: SuperThis) { this.sthis = sthis; this.id = sthis.nextId(12).str; + console.log("CFWSRoom", this.id); } - // private _getWebSocketsCtx = (): WebSocket[] => { - // throw new Error("Method not ready"); - // } + #getWebSockets?: () => WebSocket[]; applyGetWebSockets(id: string, fn: () => WebSocket[]): void { - // console.log("applyGetWebSockets", this.id, fn); - let val = this.eventsWithConnId.get(id); - if (!val) { - val = {}; - this.eventsWithConnId.set(id, val); - } - val.getWebSockets = fn; + console.log("applyGetWebSockets", this.id, id, fn); + // let val = this.eventsWithConnId.get(id); + // if (!val) { + // val = {}; + // this.eventsWithConnId.set(id, val); + // } + this.isWebsocket = true; + this.#getWebSockets = fn; } - getConns(conn: QSId): ConnItem[] { - if (!this.eventsWithConnId.has(conn.resId)) { - // eslint-disable-next-line no-console - console.error("getConns:missing", conn); - return []; + applyEvents(_id: string, events: WSEventsConnId): void { + // let val = this.eventsWithConnId.get(id); + // if (!val) { + // val = {}; + // this.eventsWithConnId.set(id, val); + // } + this.#events = events; + } + + getConns(): ConnItem[] { + if (!this.isWebsocket) { + return this.notWebSockets as ConnItem[]; } - const getWebSockets = this.eventsWithConnId.get(conn.resId)?.getWebSockets; + // if (!this.eventsWithConnId.has(conn.resId)) { + // // eslint-disable-next-line no-console + // // console.error("getConns:missing", conn); + // return []; + // } + const getWebSockets = this.#getWebSockets; if (!getWebSockets) { // eslint-disable-next-line no-console - console.error("getConns:missing-getWebSockets", conn); + // console.error("getConns:missing-getWebSockets", conn); return []; } // console.log("getConns-enter:", this.id); try { - const res = getWebSockets() + const conns = getWebSockets(); + const res = conns .map((i) => { const o = i.deserializeAttachment(); if (!o.conn) { @@ -126,7 +146,8 @@ class CFWSRoom implements WSRoom { }) .filter((i) => !!i); // console.log("getConns", this.id, res); - return res ?? []; + console.log("getConns-leave:", this.id, conns.length, res.length); + return res; } catch (e) { // eslint-disable-next-line no-console console.error("getConns", e); @@ -135,7 +156,14 @@ class CFWSRoom implements WSRoom { // throw new Error("Method not implemented."); } removeConn(conn: QSId): void { - const found = this.getConns(conn).find((i) => qsidEqual(i.conn, conn)); + if (!this.isWebsocket) { + const idx = this.notWebSockets.findIndex((i) => qsidEqual(i.conn, conn)); + if (idx >= 0) { + this.notWebSockets.splice(idx, 1); + } + return; + } + const found = this.getConns().find((i) => qsidEqual(i.conn, conn)); if (!found) { return; } @@ -147,17 +175,26 @@ class CFWSRoom implements WSRoom { // throw new Error("Method not implemented."); } addConn(ws: WSContextWithId, conn: QSId): QSId { + if (!this.isWebsocket) { + console.log("addConn-local", this.id, conn); + this.notWebSockets.push({ conn, touched: new Date(), ws }); + return conn; + } const x = ws.raw?.deserializeAttachment(); ws.raw?.serializeAttachment({ ...x, conn }); - // console.log("addConn", this.id, conn); // throw new Error("Method not implemented."); + console.log("addConn", this.id, conn); return conn; } isConnected(msg: MsgBase): msg is MsgWithConn { if (!MsgIsWithConn(msg)) { return false; } - return !!this.getConns(msg.conn).find((i) => qsidEqual(i.conn, msg.conn)); + if (!this.isWebsocket) { + // return !!this.notWebSockets.find((i) => qsidEqual(i.conn, msg.conn)) + return true; + } + return !!this.getConns().find((i) => qsidEqual(i.conn, msg.conn)); // // eslint-disable-next-line no-console // console.log("isConnected", this.id, this.getWebSockets().length); // // throw new Error("Method not implemented."); @@ -168,53 +205,40 @@ class CFWSRoom implements WSRoom { // this.dobj = dobj; // } - applyEvents(id: string, events: WSEventsConnId): void { - // if (this.eventsWithConnId.has(id)) { - // throw new Error("applyEvents:already set"); - // } - let val = this.eventsWithConnId.get(id); - if (!val) { - val = {}; - this.eventsWithConnId.set(id, val); - } - val.events = events; - // console.log("applyEvents", this.id, id); - } - readonly events = { onOpen: (id: string, evt: Event, ws: WebSocket) => { - if (!this.eventsWithConnId.has(id)) { - throw new Error(`applyEvents:onOpen missing not ${id} => ${Array.from(this.eventsWithConnId.keys())}`); - } + // if (!this.eventsWithConnId.has(id)) { + // throw new Error(`applyEvents:onOpen missing not ${id} => ${Array.from(this.eventsWithConnId.keys())}`); + // } // const o = ws.deserializeAttachment(); - this.eventsWithConnId.get(id)?.events?.onOpen(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); + this.#events?.onOpen(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); }, onMessage: (id: string, evt: MessageEvent, ws: WebSocket) => { - if (!this.eventsWithConnId.has(id)) { - // console.log("onMessaged:Error", this.id); - throw new Error(`applyEvents:onMessagee missing not ${id}`); - } + // if (!this.eventsWithConnId.has(id)) { + // // console.log("onMessaged:Error", this.id); + // throw new Error(`applyEvents:onMessagee missing not ${id}`); + // } // const o = ws.deserializeAttachment(); const wci = new WSContextWithId(id, webSocket2WSContextInit(ws)); - this.eventsWithConnId.get(id)?.events?.onMessage(evt, wci); + this.#events?.onMessage(evt, wci); // console.log("onMessaged", this.id); }, onClose: (id: string, evt: CloseEvent, ws: WebSocket) => { - // console.log("onClosing", ws); - if (!this.eventsWithConnId.has(id)) { - throw new Error(`applyEvents:onClose missing not ${id}`); - } + // // console.log("onClosing", ws); + // if (!this.eventsWithConnId.has(id)) { + // throw new Error(`applyEvents:onClose missing not ${id}`); + // } // const o = ws.deserializeAttachment(); - this.eventsWithConnId.get(id)?.events?.onClose(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); + this.#events?.onClose(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); // console.log("onClosed", this.id); }, onError: (id: string, evt: Event, ws: WebSocket) => { // console.log("onError", ws); - if (!this.eventsWithConnId.has(id)) { - throw new Error(`applyEvents:onError missing not ${id}`); - } + // if (!this.eventsWithConnId.has(id)) { + // throw new Error(`applyEvents:onError missing not ${id}`); + // } // const o = ws.deserializeAttachment(); - this.eventsWithConnId.get(id)?.events?.onError(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); + this.#events?.onError(evt, new WSContextWithId(id, webSocket2WSContextInit(ws))); }, }; // satisfies CFWSEvents; @@ -243,21 +267,56 @@ class CFWSRoom implements WSRoom { // } } -export class CFExposeCtx { +interface CFExposeCtxItem { readonly sthis: SuperThis; readonly wsRoom: CFWSRoom; readonly logger: Logger; readonly ende: EnDeCoder; readonly gs: Gestalt; readonly db: SQLDatabase; + readonly id: string; +} - constructor(sthis: SuperThis, logger: Logger, ende: EnDeCoder, gs: Gestalt, db: SQLDatabase, wsRoom: CFWSRoom) { - this.sthis = sthis; - this.logger = logger; - this.ende = ende; - this.gs = gs; - this.db = db; - this.wsRoom = wsRoom; +export class CFExposeCtx { + #ctxs = new Map(); + + public static attach( + env: Env, + id: string, + sthis: SuperThis, + logger: Logger, + ende: EnDeCoder, + gs: Gestalt, + db: SQLDatabase, + wsRoom: CFWSRoom + ): void { + // const ctx = new CFExposeCtx(id, sthis, logger, ende, gs, db, wsRoom); + env.FP_EXPOSE_CTX = env.FP_EXPOSE_CTX ?? new CFExposeCtx(); + env.FP_EXPOSE_CTX.attach(id, sthis, logger, ende, gs, db, wsRoom); + } + + private constructor() { + /* noop */ + } + + public get(id: string): CFExposeCtxItem { + const ctx = this.#ctxs.get(id); + if (!ctx) { + throw new Error(`CFExposeCtx: missing ${id}`); + } + return ctx; + } + + public attach( + id: string, + sthis: SuperThis, + logger: Logger, + ende: EnDeCoder, + gs: Gestalt, + db: SQLDatabase, + wsRoom: CFWSRoom + ) { + this.#ctxs.set(id, { id, sthis, logger, ende, gs, db, wsRoom }); } } @@ -278,7 +337,9 @@ export class CFHonoFactory implements HonoServerFactory { }); sthis.env.sets(c.env); - const logger = ensureLogger(sthis, `CFHono[${URI.from(c.req.url).pathname}]`); + const id = sthis.nextId(12).str; + + const logger = ensureLogger(sthis, `CFHono[${id}-${URI.from(c.req.url).pathname}]`); const ende = jsonEnDe(sthis); const fpProtocol = sthis.env.get("FP_PROTOCOL"); const msgP = defaultMsgParams(sthis, { @@ -297,7 +358,7 @@ export class CFHonoFactory implements HonoServerFactory { { cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; // console.log("DO-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); - db = new CFDObjSQLDatabase(getBackendDurableObject(c.env)); + db = new CFDObjSQLDatabase(getBackendDurableObject(c.env, id)); } break; @@ -321,20 +382,23 @@ export class CFHonoFactory implements HonoServerFactory { } const wsRoom = new CFWSRoom(sthis); - c.env.FP_EXPOSE_CTX = new CFExposeCtx(sthis, logger, ende, gs, db, wsRoom); + CFExposeCtx.attach(c.env, id, sthis, logger, ende, gs, db, wsRoom); // wsRoom.applyGetWebSockets(c.env.FP_EXPOSE_CTX.getWebSockets); // TODO WE NEED TO START THE DURABLE OBJECT // but then on every request we import the schema - // return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs })); - return startedChs - .get(cfBackendKey) - .once(async () => { - const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); - await chs.start(); - return chs; - }) - .then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); + + const chs = new CFHonoServer(id, sthis, logger, ende, gs, db, wsRoom); + return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); + + // return startedChs + // .get(cfBackendKey) + // .once(async () => { + // const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); + // await chs.start(); + // return chs; + // }) + // .then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); // return ret; // .then((v) => sthis.logger.Flush().then(() => v)) } @@ -360,6 +424,7 @@ export class CFHonoServer extends HonoServerBase { // readonly env: Env; // readonly wsConnections = new Map() constructor( + id: string, sthis: SuperThis, logger: Logger, ende: EnDeCoder, @@ -368,7 +433,7 @@ export class CFHonoServer extends HonoServerBase { wsRoom: WSRoom, headers?: HttpHeader ) { - super(sthis, logger, gs, sqlDb, wsRoom, headers); + super(id, sthis, logger, gs, sqlDb, wsRoom, headers); this.ende = ende; // this.env = env; } @@ -381,10 +446,6 @@ export class CFHonoServer extends HonoServerBase { upgradeWebSocket( createEvents: (c: Context) => WSEventsConnId | Promise> ): ConnMiddleware { - // throw new Error("upgradeWebSocket Method not implemented."); - // if (!this._upgradeWebSocket) { - // throw new Error("upgradeWebSocket not implemented"); - // } return async (_conn, c, _next) => { const upgradeHeader = c.req.header("Upgrade"); if (!upgradeHeader || upgradeHeader !== "websocket") { @@ -393,63 +454,13 @@ export class CFHonoServer extends HonoServerBase { { status: 426 } ); } - - // console.log("upgradeWebSocket", Object.keys(_conn)); - - //wsRoom.getEvents(); - //wsRoom.applyExposeCtx(c.env.EXPOSE_CTX); - - const id = c.env.FP_EXPOSE_CTX.sthis.nextId().str; - // console.log("upgradeWebSocket:createEvents: ", id); - c.env.FP_EXPOSE_CTX.wsRoom.applyEvents(id, await createEvents(c)); - - // const { sthis, logger, ende, wsRoom, gs, db } = c.env.EXPOSE_CTX; - // const chs = new CFHonoServer(sthis, logger, ende, gs, db, wsRoom); - // await chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); - + const id = this.id; + c.env.FP_EXPOSE_CTX.get(id).wsRoom.applyEvents(id, await createEvents(c)); const url = BuildURI.from(c.req.url).setParam("ctxId", id).toString(); - - const dobjRoom = getRoomDurableObject(c.env); + console.log("upgradeWebSocket", id, url); + const dobjRoom = getRoomDurableObject(c.env, id); const ret = dobjRoom.fetch(url, c.req.raw); return ret; - - // // const env = c.env as Env; - // // const id = env.FP_META_GROUPS.idFromName([conn.key.tenant, conn.key.ledger].join(":")); - // // const dObj = env.FP_META_GROUPS.get(id); - // // c.env.WS_EVENTS = createEvents(c); - // // return dObj.fetch(c.req.raw as unknown as CFRequestInfo) as unknown as Promise; - // // this._upgradeWebSocket!(createEvents)(c, next); - - // const { 0: client, 1: server } = new WebSocketPair(); - // conn.attachWSPair({ client, server }); - - // const wsEvents = await createEvents(c); - // (this.wsRoom as CFWSRoom).applyEvents(wsEvents); - - // // console.log("applyEvents", c.env.WS_EVENTS); - - // // const wsEvents = await createEvents(c); - // // console.log("upgradeWebSocket", c.req.url); - - // // const wsCtx = new WSContext(server as WSContextInit); - - // // server.onopen = (ev) => { - // // console.log("onopen", ev); - // // wsEvents.onOpen?.(ev, wsCtx); - // // } - - // // await this.wsRoom.acceptConnection(server, wsEvents , c.env); - - // // server.send("Hello from server"); - - // // this.wsConnections.set(this.sthis.nextId().str, { client, server }); - // // const client = webSocketPair[0], - // // server = webSocketPair[1]; - - // return new Response(null, { - // status: 101, - // webSocket: client, - // }); }; } } diff --git a/src/v2-cloud/backend/server.ts b/src/v2-cloud/backend/server.ts index 383da984..74617f2d 100644 --- a/src/v2-cloud/backend/server.ts +++ b/src/v2-cloud/backend/server.ts @@ -10,11 +10,14 @@ import { WSMessageReceive } from "hono/ws"; import { URI } from "@adviser/cement"; const app = new Hono(); -const honoServer = new HonoServer(new CFHonoFactory()); +const honoServer = new HonoServer(new CFHonoFactory()).register(app); export default { fetch: async (req, env, ctx): Promise => { - await honoServer.register(app); + // console.log("fetch-1", req.url); + await honoServer.start(); + // await honoServer.register(app); + // console.log("fetch-2", req.url); return app.fetch(req, env, ctx); }, } satisfies ExportedHandler; @@ -35,7 +38,11 @@ export interface ExecSQLResult { } export class FPBackendDurableObject extends DurableObject { - async execSql(sql: string, params: unknown[]): Promise { + doneSchema = false; + async execSql(sql: string, params: unknown[], schema?: boolean): Promise { + if (schema && this.doneSchema) { + return { rowsRead: 0, rowsWritten: 0, rawResults: [] }; + } const cursor = await this.ctx.storage.sql.exec(sql, ...params); const rawResults = cursor.toArray(); const res = { @@ -95,12 +102,12 @@ export class FPRoomDurableObject extends DurableObject { const id = URI.from(request.url).getParam("ctxId", "none"); - // console.log("DO-ids:", id, this.id); + console.log("DO-ids:", id, this.id); - this.env.FP_EXPOSE_CTX.wsRoom.applyGetWebSockets(id, () => this.ctx.getWebSockets()); + this.env.FP_EXPOSE_CTX.get(id).wsRoom.applyGetWebSockets(id, () => this.ctx.getWebSockets()); server.serializeAttachment({ id }); - this.env.FP_EXPOSE_CTX.wsRoom.events.onOpen(id, {} as Event, server); + this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onOpen(id, {} as Event, server); // for (const ws of wss) { // ws.setnd(`New WebSocket connection established: ${wss.length}`); @@ -119,22 +126,22 @@ export class FPRoomDurableObject extends DurableObject { webSocketOpen(ws: WebSocket): void | Promise { const { id } = ws.deserializeAttachment(); - this.env.FP_EXPOSE_CTX.wsRoom.events.onOpen(id, {} as Event, ws); + this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onOpen(id, {} as Event, ws); } webSocketError(ws: WebSocket, error: unknown): void | Promise { const { id } = ws.deserializeAttachment(); - this.env.FP_EXPOSE_CTX.wsRoom.events.onError(id, error as Event, ws); + this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onError(id, error as Event, ws); } async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer): Promise { const { id } = ws.deserializeAttachment(); // console.log("webSocketMessage", msg); - this.env.FP_EXPOSE_CTX.wsRoom.events.onMessage(id, { data: msg } as MessageEvent, ws); + this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onMessage(id, { data: msg } as MessageEvent, ws); } webSocketClose(ws: WebSocket, code: number, reason: string): void | Promise { const { id } = ws.deserializeAttachment(); - this.env.FP_EXPOSE_CTX.wsRoom.events.onClose(id, { code, reason } as CloseEvent, ws); + this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onClose(id, { code, reason } as CloseEvent, ws); } } diff --git a/src/v2-cloud/backend/wrangler.toml b/src/v2-cloud/backend/wrangler.toml index dfdee3a2..651010e7 100644 --- a/src/v2-cloud/backend/wrangler.toml +++ b/src/v2-cloud/backend/wrangler.toml @@ -94,6 +94,10 @@ FP_DEBUG = "FPMetaGroups" FP_PROTOCOL = "http" CF_BACKEND_MODE = "DURABLE_OBJECT" +[[env.test-reqRes-DO.migrations]] +tag = "v1" # Should be unique for each entry +new_sqlite_classes = ["FPBackendDurableObject"] + [env.test-reqRes-DO.durable_objects] bindings = [ { name = "FP_BACKEND_DO", class_name = "FPBackendDurableObject" }, diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index 2b5056d5..fda95050 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -21,7 +21,7 @@ describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gat let url: BuildURI; beforeAll(async () => { const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.register(app, port)); + server = await factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.once(app, port)); unregister = registerFireproofCloudStoreProtocol("fireproof:"); gw = new FireproofCloudGateway(sthis); url = BuildURI.from(`fireproof://localhost:${port}/`) diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index 43c4fa3a..67913085 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -87,7 +87,7 @@ describe("Connection", () => { const app = new Hono(); server = await honoServer .factory(sthis, msgP, style.remoteGestalt, port) - .then((srv) => srv.register(app, port)); + .then((srv) => srv.once(app, port)); }); afterAll(async () => { // console.log("closing server"); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index d5350ea6..26eae540 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -1,4 +1,4 @@ -import { exception2Result, HttpHeader, Logger, param, ResolveOnce, Result, URI } from "@adviser/cement"; +import { exception2Result, HttpHeader, Logger, param, Result, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; import { Context, Hono, Next } from "hono"; import { top_uint8 } from "../coerce-binary.js"; @@ -85,13 +85,24 @@ export abstract class HonoServerBase implements HonoServerImpl { readonly metaMerger: MetaMerger; readonly headers: HttpHeader; readonly wsRoom: WSRoom; - constructor(sthis: SuperThis, logger: Logger, gs: Gestalt, sqlDb: SQLDatabase, wsRoom: WSRoom, headers?: HttpHeader) { + readonly id: string; + constructor( + id: string, + sthis: SuperThis, + logger: Logger, + gs: Gestalt, + sqlDb: SQLDatabase, + wsRoom: WSRoom, + headers?: HttpHeader + ) { this.logger = logger; this._gs = gs; this.sthis = sthis; this.wsRoom = wsRoom; - this.metaMerger = new MetaMerger(sqlDb); + this.metaMerger = new MetaMerger(id, sqlDb); this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); + this.id = id; + // console.log("HonoServerBase-ctor", this.id, sqlDb); } abstract upgradeWebSocket( @@ -147,20 +158,25 @@ export abstract class HonoServerBase implements HonoServerImpl { msg: MsgWithConn, gwCtx: GwCtx = msg ): Promise> { - const rUrl = await buildRes("GET", "meta", "eventGetMeta", sthis, logger, msg, this); - if (MsgIsError(rUrl)) { - return rUrl; + const rMsg = await buildRes("GET", "meta", "eventGetMeta", sthis, logger, msg, this); + if (MsgIsError(rMsg)) { + return rMsg; } - return buildEventGetMeta( + console.log("handleBindGetMeta-in", msg, this.id); + const metas = await this.metaMerger.metaToSend(msg); + console.log("handleBindGetMeta-meta", metas); + const res = buildEventGetMeta( sthis, logger, msg, { - ...rUrl, - metas: await this.metaMerger.metaToSend(msg), + ...rMsg, + metas, }, gwCtx ); + console.log("handleBindGetMeta-out", res); + return res; } calculatePreSignedUrl(p: PreSignedMsg): Promise> { @@ -205,9 +221,11 @@ export const CORS = HttpHeader.from({ class NoBackChannel implements MsgDispatcherCtx { readonly impl: HonoServerImpl; readonly ctx: Context; - constructor(impl: HonoServerImpl, c: Context) { + readonly _wsRoom: WSRoom; + constructor(impl: HonoServerImpl, c: Context, wsRoom: WSRoom) { this.impl = impl; this.ctx = c; + this._wsRoom = wsRoom; } get ws(): WSContextWithId { return { @@ -218,7 +236,8 @@ class NoBackChannel implements MsgDispatcherCtx { } as unknown as WSContextWithId; } get wsRoom(): WSRoom { - throw new Error("NoBackChannel:wsRoom Method not implemented."); + return this._wsRoom; + // throw new Error("NoBackChannel:wsRoom Method not implemented."); } } @@ -235,79 +254,102 @@ export class HonoServer { // this.gestalt = gestalt; this.factory = factory; } - readonly _register = new ResolveOnce(); - async register(app: Hono, port?: number): Promise { - return this._register.once(async () => { - await this.factory.start(app); - // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) - // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) - app.put("/fp", (c) => - this.factory.inject(c, async ({ sthis, logger, impl, ende, wsRoom }) => { - impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); - const rMsg = await exception2Result(() => c.req.json() as Promise); - if (rMsg.isErr()) { - c.status(400); - return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); - } - const dispatcher = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); - return dispatcher.dispatch(new NoBackChannel(impl, c), rMsg.Ok()); - }) - ); - app.get("/ws", (c, next) => - this.factory.inject(c, async ({ sthis, logger, ende, impl, wsRoom }) => { - return impl.upgradeWebSocket((_c) => { - let dp: MsgDispatcher; - // const id = sthis.nextId().str; - // console.log("upgradeWebSocket:inject:", id); - return { - onOpen: (_e, _ws) => { - dp = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); - // console.log("onOpen:inject:", id); - }, - onError: (error) => { - logger.Error().Err(error).Msg("WebSocket error"); - }, - onMessage: async (event, ws) => { - const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); - // console.log("onMessage:inject:", id, rMsg); - if (rMsg.isErr()) { - ws.send( - ende.encode( - buildErrorMsg( - sthis, - logger, - { - message: event.data, - } as ErrorMsg, - rMsg.Err() - ) + + start(): Promise { + return this.factory.start(new Hono()).then(() => this); + } + + /* only for testing */ + async once(app: Hono, port?: number): Promise { + this.register(app); + await this.factory.start(app); + await this.factory.serve(app, port); + return this; + } + + async serve(app: Hono, port?: number): Promise { + await this.factory.serve(app, port); + return this; + } + // readonly _register = new ResolveOnce(); + register(app: Hono): HonoServer { + // return this._register.once(async () => { + // console.log("register-1"); + // await this.factory.start(app); + // console.log("register-2"); + // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) + // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) + app.put("/fp", (c) => + this.factory.inject(c, async ({ sthis, logger, impl, ende, wsRoom }) => { + impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); + const rMsg = await exception2Result(() => c.req.json() as Promise); + if (rMsg.isErr()) { + c.status(400); + return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); + } + const dispatcher = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); + return dispatcher.dispatch(new NoBackChannel(impl, c, wsRoom), rMsg.Ok()); + }) + ); + // console.log("register-2.1"); + app.get("/ws", (c, next) => + this.factory.inject(c, async ({ sthis, logger, ende, impl, wsRoom }) => { + return impl.upgradeWebSocket((_c) => { + let dp: MsgDispatcher; + const id = sthis.nextId().str; + // console.log("upgradeWebSocket:inject:", id); + return { + onOpen: (_e, _ws) => { + dp = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); + console.log("onOpen:inject:", id); + }, + onError: (error) => { + logger.Error().Err(error).Msg("WebSocket error"); + }, + onMessage: async (event, ws) => { + const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); + console.log("onMessage:inject:", id, rMsg); + if (rMsg.isErr()) { + ws.send( + ende.encode( + buildErrorMsg( + sthis, + logger, + { + message: event.data, + } as ErrorMsg, + rMsg.Err() ) - ); - } else { - // console.log("dp-dispatch", rMsg.Ok(), dp); - await dp.dispatch( - { - impl, - ws, - wsRoom: dp.wsRoom, - }, - rMsg.Ok() - ); - } - }, - onClose: (_evt, _ws) => { - // impl.delConn(ws); - // console.log("onClose:inject:", id); - dp = undefined as unknown as MsgDispatcher; - // console.log('Connection closed') - }, - }; - })(new WSConnection(), c, next); - }) - ); - await this.factory.serve(app, port); - return this; - }); + ) + ); + } else { + // console.log("dp-dispatch", rMsg.Ok(), dp); + await dp.dispatch( + { + impl, + ws, + wsRoom: dp.wsRoom, + }, + rMsg.Ok() + ); + } + }, + onClose: (_evt, _ws) => { + // impl.delConn(ws); + console.log("onClose:inject:", id); + dp = undefined as unknown as MsgDispatcher; + // console.log('Connection closed') + }, + }; + })(new WSConnection(), c, next); + }) + ); + return this; + // console.log("register-3"); + // await this.factory.serve(app, port); + // console.log("register-4"); + // return this; + // }); } async close() { const ret = await this.factory.close(); diff --git a/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts b/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts index b8f0e94b..9b70de55 100644 --- a/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts +++ b/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts @@ -65,29 +65,29 @@ export class MetaByTenantLedgerSql { this.tenantLedgerSql = tenantLedgerSql; } - readonly #sqlCreateMetaByTenantLedger = new ResolveOnce(); + // readonly #sqlCreateMetaByTenantLedger = new ResolveOnce(); sqlCreateMetaByTenantLedger(): SQLStatement[] { - return this.#sqlCreateMetaByTenantLedger.once(() => { - return MetaByTenantLedgerSql.schema().map((i) => this.db.prepare(i)); - }); + // return this.#sqlCreateMetaByTenantLedger.once(() => { + return MetaByTenantLedgerSql.schema().map((i) => this.db.prepare(i)); + // }); } - readonly #sqlInsertMetaByTenantLedger = new ResolveOnce(); + // readonly #sqlInsertMetaByTenantLedger = new ResolveOnce(); sqlEnsureMetaByTenantLedger(): SQLStatement { - return this.#sqlInsertMetaByTenantLedger.once(() => { - return this.db.prepare(` + // return this.#sqlInsertMetaByTenantLedger.once(() => { + return this.db.prepare(` INSERT INTO MetaByTenantLedger(tenant, ledger, reqId, resId, metaCID, meta, updatedAt) SELECT ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS ( SELECT 1 FROM MetaByTenantLedger WHERE metaCID = ? ) `); - }); + // }); } - readonly #sqlDeleteByConnection = new ResolveOnce(); + // readonly #sqlDeleteByConnection = new ResolveOnce(); sqlDeleteByConnection(): SQLStatement { - return this.#sqlDeleteByConnection.once(() => { - return this.db.prepare(` + // return this.#sqlDeleteByConnection.once(() => { + return this.db.prepare(` DELETE FROM MetaByTenantLedger WHERE tenant = ? @@ -100,7 +100,7 @@ export class MetaByTenantLedgerSql { AND metaCID NOT IN (SELECT value FROM json_each(?)) `); - }); + // }); } /* diff --git a/src/v2-cloud/meta-merger/meta-merger.test.ts b/src/v2-cloud/meta-merger/meta-merger.test.ts index 621715eb..406c3310 100644 --- a/src/v2-cloud/meta-merger/meta-merger.test.ts +++ b/src/v2-cloud/meta-merger/meta-merger.test.ts @@ -43,7 +43,7 @@ function getSQLFlavours(): { name: string; factory: () => Promise } factory: async () => { const { CFDObjSQLDatabase } = await import("../backend/cf-dobj-abstract-sql.js"); const { env } = await import("cloudflare:test"); - return new CFDObjSQLDatabase(getBackendDurableObject(env as Env)); + return new CFDObjSQLDatabase(getBackendDurableObject(env as Env, "the-id")); }, }, ]; @@ -68,7 +68,7 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { beforeAll(async () => { // db = new Database(':memory:'); const db = await flavour.factory(); - mm = new MetaMerger(db); + mm = new MetaMerger("bong", db); await mm.createSchema(); }); diff --git a/src/v2-cloud/meta-merger/meta-merger.ts b/src/v2-cloud/meta-merger/meta-merger.ts index 83458624..26094dfc 100644 --- a/src/v2-cloud/meta-merger/meta-merger.ts +++ b/src/v2-cloud/meta-merger/meta-merger.ts @@ -43,8 +43,11 @@ export class MetaMerger { readonly metaSend: MetaSendSql; }; - constructor(db: SQLDatabase) { + readonly id: string; + + constructor(id: string, db: SQLDatabase) { this.db = db; + this.id = id; // this.sthis = sthis; const tenant = new TenantSql(db); const tenantLedger = new TenantLedgerSql(db, tenant); @@ -102,8 +105,11 @@ export class MetaMerger { } async metaToSend(sink: Connection, now = new Date()): Promise { + console.log("metaToSend-1", this.id); const bySink = toByConnection(sink); + console.log("metaToSend-2", this.id); const rows = await this.sql.metaSend.selectToAddSend({ ...bySink, now }); + console.log("metaToSend-3", this.id); await this.sql.metaSend.insert( rows.map((row) => ({ metaCID: row.metaCID, @@ -112,6 +118,7 @@ export class MetaMerger { sendAt: row.sendAt, })) ); + console.log("metaToSend-4"); return rows.map((row) => row.meta); } } diff --git a/src/v2-cloud/meta-merger/meta-send.ts b/src/v2-cloud/meta-merger/meta-send.ts index 2debc00a..208cbdea 100644 --- a/src/v2-cloud/meta-merger/meta-send.ts +++ b/src/v2-cloud/meta-merger/meta-send.ts @@ -1,4 +1,4 @@ -import { ResolveOnce } from "@adviser/cement"; +// import { ResolveOnce } from "@adviser/cement"; import { MetaByTenantLedgerSql } from "./meta-by-tenant-ledger.js"; import { ByConnection } from "./meta-merger.js"; import { CRDTEntry } from "@fireproof/core"; @@ -40,26 +40,26 @@ export class MetaSendSql { this.db = db; } - readonly #sqlCreateMetaSend = new ResolveOnce(); + // readonly #sqlCreateMetaSend = new ResolveOnce(); sqlCreateMetaSend(drop: boolean): SQLStatement[] { - return this.#sqlCreateMetaSend.once(() => { - return MetaSendSql.schema(drop).map((i) => this.db.prepare(i)); - }); + // return this.#sqlCreateMetaSend.once(() => { + return MetaSendSql.schema(drop).map((i) => this.db.prepare(i)); + // }); } - readonly #sqlInsertMetaSend = new ResolveOnce(); + // readonly #sqlInsertMetaSend = new ResolveOnce(); sqlInsertMetaSend(): SQLStatement { - return this.#sqlInsertMetaSend.once(() => { - return this.db.prepare(` + // return this.#sqlInsertMetaSend.once(() => { + return this.db.prepare(` INSERT INTO MetaSend(metaCID, reqId, resId, sendAt) VALUES(?, ?, ?, ?) `); - }); + // }); } - readonly #sqlSelectToAddSend = new ResolveOnce(); + // readonly #sqlSelectToAddSend = new ResolveOnce(); sqlSelectToAddSend(): SQLStatement { - return this.#sqlSelectToAddSend.once(() => { - return this.db.prepare(` + // return this.#sqlSelectToAddSend.once(() => { + return this.db.prepare(` SELECT t.metaCID, ? as reqId, ? as resId, ? as sendAt, t.meta FROM MetaByTenantLedger as t WHERE t.tenant = ? @@ -68,30 +68,38 @@ export class MetaSendSql { AND NOT EXISTS (SELECT 1 FROM MetaSend AS s WHERE t.metaCID = s.metaCID and s.reqId = ? and s.resId = ?) `); - }); + // }); } async selectToAddSend(conn: ByConnection & { now: Date }): Promise { + console.log("selectToAddSend-1"); const stmt = this.sqlSelectToAddSend(); - const rows = await stmt.all( - conn.reqId, - conn.resId, - conn.now, - conn.tenant, - conn.ledger, - conn.reqId, - conn.resId - ); - return rows.map( - (i) => - ({ - metaCID: i.metaCID, - reqId: i.reqId, - resId: i.resId, - sendAt: new Date(i.sendAt), - meta: JSON.parse(i.meta) as CRDTEntry, - }) satisfies MetaSendRowWithMeta - ); + console.log("selectToAddSend-2"); + try { + const rows = await stmt.all( + conn.reqId, + conn.resId, + conn.now, + conn.tenant, + conn.ledger, + conn.reqId, + conn.resId + ); + console.log("selectToAddSend-3", rows); + return rows.map( + (i) => + ({ + metaCID: i.metaCID, + reqId: i.reqId, + resId: i.resId, + sendAt: new Date(i.sendAt), + meta: JSON.parse(i.meta) as CRDTEntry, + }) satisfies MetaSendRowWithMeta + ); + } catch (e) { + console.log("selectToAddSend-2-error", e); + throw e; + } } async insert(t: MetaSendRow[]) { @@ -101,10 +109,10 @@ export class MetaSendSql { } } - readonly #sqlDeleteByConnection = new ResolveOnce(); + // readonly #sqlDeleteByConnection = new ResolveOnce(); sqlDeleteByMetaCID(): SQLStatement { - return this.#sqlDeleteByConnection.once(() => { - return this.db.prepare(` + // return this.#sqlDeleteByConnection.once(() => { + return this.db.prepare(` DELETE FROM MetaSend WHERE metaCID in (SELECT metaCID FROM MetaByTenantLedger WHERE @@ -118,7 +126,7 @@ export class MetaSendSql { AND metaCID NOT IN (SELECT value FROM json_each(?))) `); - }); + // }); } async deleteByConnection(dmi: ByConnection & { metaCIDs: string[] }) { diff --git a/src/v2-cloud/meta-merger/tenant-ledger.ts b/src/v2-cloud/meta-merger/tenant-ledger.ts index b5dc5fa7..a129cebc 100644 --- a/src/v2-cloud/meta-merger/tenant-ledger.ts +++ b/src/v2-cloud/meta-merger/tenant-ledger.ts @@ -1,4 +1,4 @@ -import { ResolveOnce } from "@adviser/cement"; +// import { ResolveOnce } from "@adviser/cement"; import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; import { TenantSql } from "./tenant.js"; @@ -35,22 +35,22 @@ export class TenantLedgerSql { this.tenantSql = tenantSql; } - readonly #sqlCreateTenantLedger = new ResolveOnce(); + // readonly #sqlCreateTenantLedger = new ResolveOnce(); sqlCreateTenantLedger(): SQLStatement[] { - return this.#sqlCreateTenantLedger.once(() => { - return TenantLedgerSql.schema().map((i) => this.db.prepare(i)); - }); + // return this.#sqlCreateTenantLedger.once(() => { + return TenantLedgerSql.schema().map((i) => this.db.prepare(i)); + // }); } - readonly #sqlInsertTenantLedger = new ResolveOnce(); + // readonly #sqlInsertTenantLedger = new ResolveOnce(); sqlEnsureTenantLedger(): SQLStatement { - return this.#sqlInsertTenantLedger.once(() => { - return this.db.prepare(` + // return this.#sqlInsertTenantLedger.once(() => { + return this.db.prepare(` INSERT INTO TenantLedger(tenant, ledger, createdAt) SELECT ?, ?, ? WHERE NOT EXISTS(SELECT 1 FROM TenantLedger WHERE tenant = ? and ledger = ?) `); - }); + // }); } async ensure(t: TenantLedgerRow) { diff --git a/src/v2-cloud/meta-merger/tenant.ts b/src/v2-cloud/meta-merger/tenant.ts index 3ee67001..c88cff1d 100644 --- a/src/v2-cloud/meta-merger/tenant.ts +++ b/src/v2-cloud/meta-merger/tenant.ts @@ -1,4 +1,4 @@ -import { ResolveOnce } from "@adviser/cement"; +// import { ResolveOnce } from "@adviser/cement"; import { conditionalDrop, SQLDatabase, SQLStatement } from "./abstract-sql.js"; export interface TenantRow { @@ -27,21 +27,21 @@ export class TenantSql { this.db = db; } - readonly #sqlCreateTenant = new ResolveOnce(); + // readonly #sqlCreateTenant = new ResolveOnce(); sqlCreateTenant(): SQLStatement[] { - return this.#sqlCreateTenant.once(() => { - return TenantSql.schema().map((i) => this.db.prepare(i)); - }); + // return this.#sqlCreateTenant.once(() => { + return TenantSql.schema().map((i) => this.db.prepare(i)); + // }); } - readonly #sqlInsertTenant = new ResolveOnce(); + // readonly #sqlInsertTenant = new ResolveOnce(); sqlEnsureTenant(): SQLStatement { - return this.#sqlInsertTenant.once(() => { - return this.db.prepare(` + // return this.#sqlInsertTenant.once(() => { + return this.db.prepare(` INSERT INTO Tenant(tenant, createdAt) SELECT ?, ? WHERE NOT EXISTS(SELECT 1 FROM Tenant WHERE tenant = ?) `); - }); + // }); } async ensure(t: TenantRow) { diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts index 353fa8e4..57f2fd4c 100644 --- a/src/v2-cloud/msg-dispatch.ts +++ b/src/v2-cloud/msg-dispatch.ts @@ -111,7 +111,6 @@ export class MsgDispatcher { } async dispatch(ctx: MsgDispatcherCtx, msg: MsgBase): Promise { - // console.log("dispatch-0", msg); const validateConn = async ( msg: T, fn: (msg: MsgWithConn) => Promisable> @@ -126,17 +125,21 @@ export class MsgDispatcher { const r = await fn(msg); return Promise.resolve(this.send(ctx.ws, r)); }; - // console.log("dispatch-1", msg); - const found = Array.from(this.items.values()).find((item) => item.match(msg)); - if (!found) { - // console.log("dispatch-2", msg); - return this.send(ctx.ws, buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); - } - if (!found.isNotConn) { - // console.log("dispatch-3", msg); - return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, ctx, msg)); + try { + // console.log("dispatch-1", msg); + const found = Array.from(this.items.values()).find((item) => item.match(msg)); + if (!found) { + // console.log("dispatch-2", msg); + return this.send(ctx.ws, buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); + } + if (!found.isNotConn) { + // console.log("dispatch-3", msg); + return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, ctx, msg)); + } + // console.log("dispatch-4", msg); + return this.send(ctx.ws, await found.fn(this.sthis, this.logger, ctx, msg)); + } catch (e) { + return this.send(ctx.ws, buildErrorMsg(this.sthis, this.logger, msg, e as Error)); } - // console.log("dispatch-4", msg); - return this.send(ctx.ws, await found.fn(this.sthis, this.logger, ctx, msg)); } } diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts index 5b435044..3ae74e95 100644 --- a/src/v2-cloud/msg-dispatcher-impl.ts +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -139,6 +139,7 @@ export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt, ende: EnD { match: MsgIsBindGetMeta, fn: (sthis, logger, ctx, msg: MsgWithConn) => { + // console.log("MsgIsBindGetMeta", msg); return ctx.impl.handleBindGetMeta(sthis, logger, msg); }, }, diff --git a/src/v2-cloud/node-hono-server.ts b/src/v2-cloud/node-hono-server.ts index 5039cb5e..e8cbf7c7 100644 --- a/src/v2-cloud/node-hono-server.ts +++ b/src/v2-cloud/node-hono-server.ts @@ -173,6 +173,8 @@ export class NodeHonoFactory implements HonoServerFactory { const logger = ensureLogger(sthis, `NodeHono[${URI.from(c.req.url).pathname}]`); const ende = jsonEnDe(sthis); + const id = sthis.nextId(12).str; + const fpProtocol = sthis.env.get("FP_PROTOCOL"); const msgP = this.params.msgP ?? @@ -185,7 +187,7 @@ export class NodeHonoFactory implements HonoServerFactory { defaultGestalt(msgP, { id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", }); - const nhs = new NodeHonoServer(sthis, this, gs, this.params.sql, this._wsRoom); + const nhs = new NodeHonoServer(id, sthis, this, gs, this.params.sql, this._wsRoom); return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs, wsRoom: this._wsRoom })); } @@ -222,6 +224,7 @@ export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { readonly _upgradeWebSocket: UpgradeWebSocket; // readonly wsRoom: NodeWSRoom; constructor( + id: string, sthis: SuperThis, factory: NodeHonoFactory, gs: Gestalt, @@ -229,7 +232,7 @@ export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { wsRoom: WSRoom, headers?: HttpHeader ) { - super(sthis, sthis.logger, gs, sqldb, wsRoom, headers); + super(id, sthis, sthis.logger, gs, sqldb, wsRoom, headers); this._upgradeWebSocket = factory._upgradeWebSocket; } diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 02250086..5c0a6943 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -187,7 +187,7 @@ export function CFHonoServerFactory(backend: "D1" | "DO") { const { tomlFile } = await resolveToml(backend); $.verbose = !!process.env.FP_DEBUG; const runningWrangler = $` - wrangler dev -c ${tomlFile} --port ${port} --env test-${remoteGestalt.protocolCapabilities[0]}-${backend} --no-show-interactive-dev-session & + wrangler dev -c ${tomlFile} --port ${port} --env test-${remoteGestalt.protocolCapabilities[0]}-${backend} --no-show-interactive-dev-session --no-live-reload & waitPid=$! echo "PID:$waitPid" wait $waitPid`; diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts index c71039fc..7cc44f97 100644 --- a/src/v2-cloud/ws-sockets.test.ts +++ b/src/v2-cloud/ws-sockets.test.ts @@ -14,6 +14,7 @@ describe("test multiple connections", () => { // dummy NodeHonoServerFactory(), CFHonoServerFactory("D1"), + CFHonoServerFactory("DO"), ])("$name - Gateway", ({ factory }) => { const msgP = defaultMsgParams(sthis, { hasPersistent: true }); const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); @@ -25,7 +26,7 @@ describe("test multiple connections", () => { beforeAll(async () => { const app = new Hono(); - hserv = await factory(sthis, msgP, stype.remoteGestalt, port).then((srv) => srv.register(app, port)); + hserv = await factory(sthis, msgP, stype.remoteGestalt, port).then((srv) => srv.once(app, port)); }); afterAll(async () => { await hserv.close(); @@ -61,6 +62,7 @@ describe("test multiple connections", () => { const rest = [...conns]; for (const c of conns) { + console.log("Sending a chat request", rest.length, conns.length); const act = await c.request(buildReqChat(sthis, c.conn, "Hello"), { waitFor: MsgIsResChat }); if (MsgIsResChat(act)) { expect(act.targets.length).toBe(rest.length); From 5f1178445a10e10df8db75f767c02bfec5ddeb2a Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 5 Mar 2025 19:24:01 +0100 Subject: [PATCH 06/14] chore: fix the v2-cloud tests --- src/v2-cloud/backend/cf-hono-server.ts | 120 ++-- src/v2-cloud/backend/server.ts | 34 +- src/v2-cloud/client/gateway.ts | 4 +- src/v2-cloud/connection.test.ts | 537 +++++++++--------- src/v2-cloud/hono-server.ts | 224 ++++---- src/v2-cloud/http-connection.ts | 17 +- .../meta-merger/meta-by-tenant-ledger.ts | 4 +- src/v2-cloud/meta-merger/meta-merger.test.ts | 11 +- src/v2-cloud/meta-merger/meta-merger.ts | 29 +- src/v2-cloud/meta-merger/meta-send.ts | 13 +- src/v2-cloud/meta-merger/tenant-ledger.ts | 4 +- src/v2-cloud/meta-merger/tenant.ts | 4 +- src/v2-cloud/msg-dispatch.ts | 61 +- src/v2-cloud/msg-dispatcher-impl.ts | 80 +-- src/v2-cloud/msg-raw-connection-base.ts | 9 +- src/v2-cloud/msg-type-meta.ts | 12 +- src/v2-cloud/msg-types-data.ts | 21 +- src/v2-cloud/msg-types-wal.ts | 35 +- src/v2-cloud/msg-types.ts | 19 +- src/v2-cloud/node-hono-server.ts | 46 +- src/v2-cloud/ws-connection.ts | 15 +- src/v2-cloud/ws-sockets.test.ts | 2 +- 22 files changed, 634 insertions(+), 667 deletions(-) diff --git a/src/v2-cloud/backend/cf-hono-server.ts b/src/v2-cloud/backend/cf-hono-server.ts index 5b7d25e5..3f1998e4 100644 --- a/src/v2-cloud/backend/cf-hono-server.ts +++ b/src/v2-cloud/backend/cf-hono-server.ts @@ -1,12 +1,13 @@ -import { BuildURI, HttpHeader, Logger, LoggerImpl, URI } from "@adviser/cement"; +import { BuildURI, Logger, LoggerImpl, URI } from "@adviser/cement"; import { Context, Hono } from "hono"; import { ConnMiddleware, HonoServerFactory, - RunTimeParams, HonoServerBase, WSEventsConnId, WSContextWithId, + ExposeCtxItem, + ExposeCtxItemWithImpl, } from "../hono-server.js"; import { SendOptions, WSContextInit, WSMessageReceive, WSReadyState } from "hono/ws"; import { @@ -88,12 +89,12 @@ class CFWSRoom implements WSRoom { constructor(sthis: SuperThis) { this.sthis = sthis; this.id = sthis.nextId(12).str; - console.log("CFWSRoom", this.id); + // console.log("CFWSRoom", this.id); } #getWebSockets?: () => WebSocket[]; - applyGetWebSockets(id: string, fn: () => WebSocket[]): void { - console.log("applyGetWebSockets", this.id, id, fn); + applyGetWebSockets(_id: string, fn: () => WebSocket[]): void { + // console.log("applyGetWebSockets", this.id, id, fn); // let val = this.eventsWithConnId.get(id); // if (!val) { // val = {}; @@ -123,8 +124,6 @@ class CFWSRoom implements WSRoom { // } const getWebSockets = this.#getWebSockets; if (!getWebSockets) { - // eslint-disable-next-line no-console - // console.error("getConns:missing-getWebSockets", conn); return []; } // console.log("getConns-enter:", this.id); @@ -146,7 +145,7 @@ class CFWSRoom implements WSRoom { }) .filter((i) => !!i); // console.log("getConns", this.id, res); - console.log("getConns-leave:", this.id, conns.length, res.length); + // console.log("getConns-leave:", this.id, conns.length, res.length); return res; } catch (e) { // eslint-disable-next-line no-console @@ -176,14 +175,14 @@ class CFWSRoom implements WSRoom { } addConn(ws: WSContextWithId, conn: QSId): QSId { if (!this.isWebsocket) { - console.log("addConn-local", this.id, conn); + // console.log("addConn-local", this.id, conn); this.notWebSockets.push({ conn, touched: new Date(), ws }); return conn; } const x = ws.raw?.deserializeAttachment(); ws.raw?.serializeAttachment({ ...x, conn }); // throw new Error("Method not implemented."); - console.log("addConn", this.id, conn); + // console.log("addConn", this.id, conn); return conn; } isConnected(msg: MsgBase): msg is MsgWithConn { @@ -267,18 +266,10 @@ class CFWSRoom implements WSRoom { // } } -interface CFExposeCtxItem { - readonly sthis: SuperThis; - readonly wsRoom: CFWSRoom; - readonly logger: Logger; - readonly ende: EnDeCoder; - readonly gs: Gestalt; - readonly db: SQLDatabase; - readonly id: string; -} +export type CFExposeCtxItem = ExposeCtxItem; export class CFExposeCtx { - #ctxs = new Map(); + #ctxs = new Map>(); public static attach( env: Env, @@ -287,12 +278,12 @@ export class CFExposeCtx { logger: Logger, ende: EnDeCoder, gs: Gestalt, - db: SQLDatabase, + dbFactory: () => SQLDatabase, wsRoom: CFWSRoom - ): void { + ): CFExposeCtxItem { // const ctx = new CFExposeCtx(id, sthis, logger, ende, gs, db, wsRoom); env.FP_EXPOSE_CTX = env.FP_EXPOSE_CTX ?? new CFExposeCtx(); - env.FP_EXPOSE_CTX.attach(id, sthis, logger, ende, gs, db, wsRoom); + return env.FP_EXPOSE_CTX.attach(id, sthis, logger, ende, gs, dbFactory, wsRoom); } private constructor() { @@ -312,11 +303,13 @@ export class CFExposeCtx { sthis: SuperThis, logger: Logger, ende: EnDeCoder, - gs: Gestalt, - db: SQLDatabase, + gestalt: Gestalt, + dbFactory: () => SQLDatabase, wsRoom: CFWSRoom ) { - this.#ctxs.set(id, { id, sthis, logger, ende, gs, db, wsRoom }); + const item = { id, sthis, logger, ende, gestalt, dbFactory, wsRoom }; + this.#ctxs.set(id, item); + return item; } } @@ -329,8 +322,12 @@ export class CFHonoFactory implements HonoServerFactory { ) { this._onClose = onClose; } - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - async inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise { + async inject( + c: Context, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + fn: (rt: ExposeCtxItemWithImpl) => Promise + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + ): Promise { // this._env = c.env const sthis = ensureSuperThis({ logger: new LoggerImpl(), @@ -351,24 +348,20 @@ export class CFHonoFactory implements HonoServerFactory { }); const cfBackendMode = c.env.CF_BACKEND_MODE && c.env.CF_BACKEND_MODE === "DURABLE_OBJECT" ? "DURABLE_OBJECT" : "D1"; - let db: SQLDatabase; - let cfBackendKey: string; + let db: () => SQLDatabase; + // let cfBackendKey: string; switch (cfBackendMode) { case "DURABLE_OBJECT": - { - cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; - // console.log("DO-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); - db = new CFDObjSQLDatabase(getBackendDurableObject(c.env, id)); - } + // const cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; + // console.log("DO-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); + db = () => new CFDObjSQLDatabase(getBackendDurableObject(c.env, id)); break; case "D1": default: - { - cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"; - // console.log("D1-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); - db = new CFWorkerSQLDatabase(c.env[cfBackendKey] as D1Database); - } + // const cfBackendKey = ; + // console.log("D1-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); + db = () => new CFWorkerSQLDatabase(c.env[c.env.CF_BACKEND_KEY ?? "FP_BACKEND_D1"] as D1Database); break; // return startedChs // .get(cfBackendKey) @@ -382,15 +375,14 @@ export class CFHonoFactory implements HonoServerFactory { } const wsRoom = new CFWSRoom(sthis); - CFExposeCtx.attach(c.env, id, sthis, logger, ende, gs, db, wsRoom); + const item = CFExposeCtx.attach(c.env, id, sthis, logger, ende, gs, db, wsRoom); // wsRoom.applyGetWebSockets(c.env.FP_EXPOSE_CTX.getWebSockets); // TODO WE NEED TO START THE DURABLE OBJECT // but then on every request we import the schema - const chs = new CFHonoServer(id, sthis, logger, ende, gs, db, wsRoom); - return chs.start().then((chs) => fn({ sthis, logger, ende, impl: chs, wsRoom })); - + const chs = new CFHonoServer(item); + return chs.start(item).then((chs) => fn({ ...item, impl: chs })); // return startedChs // .get(cfBackendKey) // .once(async () => { @@ -420,29 +412,35 @@ export class CFHonoFactory implements HonoServerFactory { export class CFHonoServer extends HonoServerBase { // _upgradeWebSocket?: UpgradeWebSocket - readonly ende: EnDeCoder; + // readonly ende: EnDeCoder; // readonly env: Env; // readonly wsConnections = new Map() - constructor( - id: string, - sthis: SuperThis, - logger: Logger, - ende: EnDeCoder, - gs: Gestalt, - sqlDb: SQLDatabase, - wsRoom: WSRoom, - headers?: HttpHeader - ) { - super(id, sthis, logger, gs, sqlDb, wsRoom, headers); - this.ende = ende; - // this.env = env; - } + // constructor( + // id: string, + // // sthis: SuperThis, + // // logger: Logger, + // // ende: EnDeCoder, + // // gs: Gestalt, + // // sqlDb: SQLDatabase, + // // wsRoom: WSRoom, + // // headers?: HttpHeader + // ) { + // super(id); + // // this.ende = ende; + // // this.env = env; + // } // getDurableObject(conn: Connection) { // const id = env.FP_META_GROUPS.idFromName("fireproof"); // const stub = env.FP_META_GROUPS.get(id); // } + readonly ctx: CFExposeCtxItem; + constructor(ctx: CFExposeCtxItem) { + super(ctx.id); + this.ctx = ctx; + } + upgradeWebSocket( createEvents: (c: Context) => WSEventsConnId | Promise> ): ConnMiddleware { @@ -450,14 +448,14 @@ export class CFHonoServer extends HonoServerBase { const upgradeHeader = c.req.header("Upgrade"); if (!upgradeHeader || upgradeHeader !== "websocket") { return new Response( - this.ende.encode(buildErrorMsg(this.sthis, this.logger, {}, new Error("expected Upgrade: websocket"))), + this.ctx.ende.encode(buildErrorMsg(this.ctx, {}, new Error("expected Upgrade: websocket"))), { status: 426 } ); } const id = this.id; c.env.FP_EXPOSE_CTX.get(id).wsRoom.applyEvents(id, await createEvents(c)); const url = BuildURI.from(c.req.url).setParam("ctxId", id).toString(); - console.log("upgradeWebSocket", id, url); + // console.log("upgradeWebSocket", id, url); const dobjRoom = getRoomDurableObject(c.env, id); const ret = dobjRoom.fetch(url, c.req.raw); return ret; diff --git a/src/v2-cloud/backend/server.ts b/src/v2-cloud/backend/server.ts index 74617f2d..8f4d718d 100644 --- a/src/v2-cloud/backend/server.ts +++ b/src/v2-cloud/backend/server.ts @@ -75,55 +75,23 @@ export class FPRoomDurableObject extends DurableObject { const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair); - // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating - // request within the Durable Object. It has the effect of "accepting" the connection, - // and allowing the WebSocket to send and receive messages. - // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket - // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while - // the connection is open. During periods of inactivity, the Durable Object can be evicted - // from memory, but the WebSocket connection will remain open. If at some later point the - // WebSocket receives a message, the runtime will recreate the Durable Object - // (run the `constructor`) and deliver the message to the appropriate handler. this.ctx.acceptWebSocket(server); - // server.onopen = () => { - // console.log("client onopen"); - // } - // server.onmessage = (event) => { - // console.log("client onmessage", event.data); - // } - // server.onclose = (event) => { - // console.log("client onclose", event.code, event.reason); - // } - // server.onerror = (event) => { - // console.log("client onerror", event); - // } - // const wss = this.ctx.getWebSockets(); - const id = URI.from(request.url).getParam("ctxId", "none"); - console.log("DO-ids:", id, this.id); + // console.log("DO-ids:", id, this.id); this.env.FP_EXPOSE_CTX.get(id).wsRoom.applyGetWebSockets(id, () => this.ctx.getWebSockets()); server.serializeAttachment({ id }); this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onOpen(id, {} as Event, server); - // for (const ws of wss) { - // ws.setnd(`New WebSocket connection established: ${wss.length}`); - // } - return new Response(null, { status: 101, webSocket: client, }); } - // acceptWebSocket(ws: WebSocket, wsEvents: WSEvents): void { - // this.ctx.acceptWebSocket(ws); - // this.wsEvents = wsEvents; - // } - webSocketOpen(ws: WebSocket): void | Promise { const { id } = ws.deserializeAttachment(); this.env.FP_EXPOSE_CTX.get(id).wsRoom.events.onOpen(id, {} as Event, ws); diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index bfc778d0..d47cbe60 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -94,11 +94,11 @@ abstract class BaseGateway { index: param.OPTIONAL, }); if (rParams.isErr()) { - return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, rParams.Err()); + return buildErrorMsg(this, {} as MsgBase, rParams.Err()); } const params = rParams.Ok(); if (store !== params.store) { - return buildErrorMsg(this.sthis, this.logger, {} as MsgBase, new Error("store mismatch")); + return buildErrorMsg(this, {} as MsgBase, new Error("store mismatch")); } const rsu = { tid: this.sthis.nextId().str, diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index 67913085..00052131 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -73,290 +73,291 @@ describe("Connection", () => { sthis.env.sets((await resolveToml("D1")).env as unknown as Record); }); - describe.each([NodeHonoServerFactory(), CFHonoServerFactory("DO"), CFHonoServerFactory("D1")])( - "$name - Connection", - (honoServer) => { - const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); - const qOpen = buildReqOpen(sthis, { reqId: "req-open-test" }); - const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - describe.each([httpStyle(sthis, port, msgP, my), wsStyle(sthis, port, msgP, my)])( - `${honoServer.name} - $name`, - (style) => { - let server: HonoServer; - beforeAll(async () => { - const app = new Hono(); - server = await honoServer - .factory(sthis, msgP, style.remoteGestalt, port) - .then((srv) => srv.once(app, port)); - }); - afterAll(async () => { - // console.log("closing server"); - await server.close(); + describe.each([ + // force multiple lines + NodeHonoServerFactory(), + CFHonoServerFactory("DO"), + CFHonoServerFactory("D1"), + ])("$name - Connection", (honoServer) => { + const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); + const qOpen = buildReqOpen(sthis, { reqId: "req-open-test" }); + const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); + describe.each([ + // force multiple lines + httpStyle(sthis, port, msgP, my), + wsStyle(sthis, port, msgP, my), + ])(`${honoServer.name} - $name`, (style) => { + let server: HonoServer; + beforeAll(async () => { + const app = new Hono(); + server = await honoServer.factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.once(app, port)); + }); + afterAll(async () => { + // console.log("closing server"); + await server.close(); + }); + it(`conn refused`, async () => { + const rC = await applyStart(style.connRefused.open()); + expect(rC.isErr()).toBeTruthy(); + expect(rC.Err().message).toMatch(/ECONNREFUSED/); + }); + + it(`timeout`, async () => { + const rC = await applyStart(style.timeout.open()); + expect(rC.isErr()).toBeTruthy(); + expect(rC.Err().message).toMatch(/Timeout/i); + }); + + describe(`connection`, () => { + let c: MsgConnected; + beforeEach(async () => { + const rC = await style.ok.open().then((r) => MsgConnected.connect(r, { reqId: "req-open-testx" })); + expect(rC.isOk()).toBeTruthy(); + c = rC.Ok(); + expect(c.conn).toEqual({ + reqId: "req-open-testx", + resId: c.conn.resId, }); - it(`conn refused`, async () => { - const rC = await applyStart(style.connRefused.open()); - expect(rC.isErr()).toBeTruthy(); - expect(rC.Err().message).toMatch(/ECONNREFUSED/); + }); + afterEach(async () => { + await c.close(); + }); + + it("kaputt url http", async () => { + const r = await c.raw.request( + { + tid: "test", + type: "kaputt", + version: "FP-MSG-1.0", + }, + { waitFor: () => true } + ); + if (!MsgIsError(r)) { + assert.fail("expected MsgError"); + return; + } + expect(r).toEqual({ + message: "unexpected message", + tid: "test", + type: "error", + version: "FP-MSG-1.0", + src: { + tid: "test", + type: "kaputt", + version: "FP-MSG-1.0", + }, }); + }); + it("gestalt url http", async () => { + const msgP = defaultMsgParams(sthis, {}); + const req = buildReqGestalt(sthis, defaultGestalt(msgP, { id: "test" })); + const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); + if (!MsgIsResGestalt(r)) { + assert.fail("expected MsgError", JSON.stringify(r)); + } + expect(r.gestalt).toEqual(c.exchangedGestalt?.remote); + }); - it(`timeout`, async () => { - const rC = await applyStart(style.timeout.open()); - expect(rC.isErr()).toBeTruthy(); - expect(rC.Err().message).toMatch(/Timeout/i); + it("openConnection", async () => { + const req = buildReqOpen(sthis, { ...c.conn }); + const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); + if (!MsgIsResOpen(r)) { + assert.fail(JSON.stringify(r)); + } + expect(r).toEqual({ + conn: { ...c.conn, resId: r.conn?.resId }, + tid: req.tid, + type: "resOpen", + version: "FP-MSG-1.0", }); + }); + }); - describe(`connection`, () => { - let c: MsgConnected; - beforeEach(async () => { - const rC = await style.ok.open().then((r) => MsgConnected.connect(r, { reqId: "req-open-testx" })); - expect(rC.isOk()).toBeTruthy(); - c = rC.Ok(); - expect(c.conn).toEqual({ - reqId: "req-open-testx", - resId: c.conn.resId, - }); - }); - afterEach(async () => { - await c.close(); - }); + it("open", async () => { + const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, { + reqId: "req-open-testy", + }); + expect(rC.isOk()).toBeTruthy(); + const c = rC.Ok(); + expect(c.conn).toEqual({ + reqId: "req-open-testy", + resId: c.conn.resId, + }); + expect(c.raw).toBeInstanceOf(style.cInstance); + expect(c.exchangedGestalt).toEqual({ + my, + remote: style.remoteGestalt, + }); + await c.close(); + }); + describe(`${honoServer.name} - Msgs`, () => { + let gwCtx: GwCtx; + let conn: MsgConnected; + beforeAll(async () => { + const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); + expect(rC.isOk()).toBeTruthy(); + conn = rC.Ok(); + gwCtx = { + conn: conn.conn, + tenant: { + tenant: "Tenant", + ledger: "Ledger", + }, + }; + }); + afterAll(async () => { + await conn.close(); + }); + it("Open", async () => { + const res = await conn.raw.request(buildReqOpen(sthis, conn.conn), { waitFor: MsgIsResOpen }); + if (!MsgIsResOpen(res)) { + assert.fail("expected MsgResOpen", JSON.stringify(res)); + } + expect(MsgIsResOpen(res)).toBeTruthy(); + expect(res.conn).toEqual({ ...qOpen.conn, resId: res.conn.resId }); + }); - it("kaputt url http", async () => { - const r = await c.raw.request( - { - tid: "test", - type: "kaputt", - version: "FP-MSG-1.0", - }, - { waitFor: () => true } - ); - if (!MsgIsError(r)) { - assert.fail("expected MsgError"); - return; - } - expect(r).toEqual({ - message: "unexpected message", - tid: "test", - type: "error", - version: "FP-MSG-1.0", - src: { - tid: "test", - type: "kaputt", - version: "FP-MSG-1.0", - }, - }); - }); - it("gestalt url http", async () => { - const msgP = defaultMsgParams(sthis, {}); - const req = buildReqGestalt(sthis, defaultGestalt(msgP, { id: "test" })); - const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); - if (!MsgIsResGestalt(r)) { - assert.fail("expected MsgError", JSON.stringify(r)); - } - expect(r.gestalt).toEqual(c.exchangedGestalt?.remote); - }); - - it("openConnection", async () => { - const req = buildReqOpen(sthis, { ...c.conn }); - const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); - if (!MsgIsResOpen(r)) { - assert.fail(JSON.stringify(r)); - } - expect(r).toEqual({ - conn: { ...c.conn, resId: r.conn?.resId }, - tid: req.tid, - type: "resOpen", - version: "FP-MSG-1.0", - }); - }); + function sup() { + return { + path: "test/me", + key: "key-test", + } satisfies ReqSignedUrlParam; + } + describe("Data", async () => { + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildReqGetData(sthis, sp, gwCtx), { waitFor: MsgIsResGetData }); + if (MsgIsResGetData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResGetData", JSON.stringify(res)); + } }); - - it("open", async () => { - const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, { - reqId: "req-open-testy", - }); - expect(rC.isOk()).toBeTruthy(); - const c = rC.Ok(); - expect(c.conn).toEqual({ - reqId: "req-open-testy", - resId: c.conn.resId, - }); - expect(c.raw).toBeInstanceOf(style.cInstance); - expect(c.exchangedGestalt).toEqual({ - my, - remote: style.remoteGestalt, - }); - await c.close(); + it("Put", async () => { + const sp = sup(); + const res = await conn.request(buildReqPutData(sthis, sp, gwCtx), { waitFor: MsgIsResPutData }); + if (MsgIsResPutData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResPutData", JSON.stringify(res)); + } }); - describe(`${honoServer.name} - Msgs`, () => { - let gwCtx: GwCtx; - let conn: MsgConnected; - beforeAll(async () => { - const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); - expect(rC.isOk()).toBeTruthy(); - conn = rC.Ok(); - gwCtx = { - conn: conn.conn, - tenant: { - tenant: "Tenant", - ledger: "Ledger", - }, - }; - }); - afterAll(async () => { - await conn.close(); - }); - it("Open", async () => { - const res = await conn.raw.request(buildReqOpen(sthis, conn.conn), { waitFor: MsgIsResOpen }); - if (!MsgIsResOpen(res)) { - assert.fail("expected MsgResOpen", JSON.stringify(res)); - } - expect(MsgIsResOpen(res)).toBeTruthy(); - expect(res.conn).toEqual({ ...qOpen.conn, resId: res.conn.resId }); - }); - - function sup() { - return { - path: "test/me", - key: "key-test", - } satisfies ReqSignedUrlParam; + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelData(sthis, sp, gwCtx), { waitFor: MsgIsResDelData }); + if (MsgIsResDelData(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelData", JSON.stringify(res)); } - describe("Data", async () => { - it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildReqGetData(sthis, sp, gwCtx), { waitFor: MsgIsResGetData }); - if (MsgIsResGetData(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResGetData", JSON.stringify(res)); - } - }); - it("Put", async () => { - const sp = sup(); - const res = await conn.request(buildReqPutData(sthis, sp, gwCtx), { waitFor: MsgIsResPutData }); - if (MsgIsResPutData(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResPutData", JSON.stringify(res)); - } - }); - it("Del", async () => { - const sp = sup(); - const res = await conn.request(buildReqDelData(sthis, sp, gwCtx), { waitFor: MsgIsResDelData }); - if (MsgIsResDelData(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResDelData", JSON.stringify(res)); - } - }); - }); - - describe("Meta", async () => { - it("bind stop", async () => { - const sp = sup(); - expect(conn.raw.activeBinds.size).toBe(0); - const streams: ReadableStream>[] = Array(5) - .fill(0) - .map(() => { - return conn.bind(buildBindGetMeta(sthis, sp, gwCtx), { - waitFor: MsgIsEventGetMeta, - }); - }); - for await (const stream of streams) { - const reader = stream.getReader(); - while (true) { - const { done, value: msg } = await reader.read(); - if (done) { - break; - } - if (MsgIsEventGetMeta(msg)) { - // expect(msg.params).toEqual(sp); - expect(URI.from(msg.signedUrl).asObj()).toEqual(await refURL(msg)); - } else { - assert.fail("expected MsgEventGetMeta", JSON.stringify(msg)); - } - await reader.cancel(); - } - } - expect(conn.raw.activeBinds.size).toBe(0); - // await Promise.all(streams.map((s) => s.cancel())); - }); + }); + }); - it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildBindGetMeta(sthis, sp, gwCtx), { waitFor: MsgIsEventGetMeta }); - if (MsgIsEventGetMeta(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgIsEventGetMeta", JSON.stringify(res)); - } - }); - it("Put", async () => { - const sp = sup(); - const metas = Array(5) - .fill({ cid: "x", parents: [], data: "MomRkYXRho" }) - .map((data) => { - return { ...data, cid: sthis.timeOrderedNextId().str }; - }); - const res = await conn.request(buildReqPutMeta(sthis, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta }); - if (MsgIsResPutMeta(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgIsResPutMeta", JSON.stringify(res)); - } - }); - it("Del", async () => { - const sp = sup(); - const res = await conn.request(buildReqDelMeta(sthis, sp, gwCtx), { - waitFor: MsgIsResDelMeta, + describe("Meta", async () => { + it("bind stop", async () => { + const sp = sup(); + expect(conn.raw.activeBinds.size).toBe(0); + const streams: ReadableStream>[] = Array(5) + .fill(0) + .map(() => { + return conn.bind(buildBindGetMeta(sthis, sp, gwCtx), { + waitFor: MsgIsEventGetMeta, }); - if (MsgIsResDelMeta(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResDelWAL", JSON.stringify(res)); - } }); - }); - describe("WAL", async () => { - it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildReqGetWAL(sthis, sp, gwCtx), { waitFor: MsgIsResGetWAL }); - if (MsgIsResGetWAL(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResGetWAL", JSON.stringify(res)); - } - }); - it("Put", async () => { - const sp = sup(); - const res = await conn.request(buildReqPutWAL(sthis, sp, gwCtx), { waitFor: MsgIsResPutWAL }); - if (MsgIsResPutWAL(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); - } else { - assert.fail("expected MsgResPutWAL", JSON.stringify(res)); + for await (const stream of streams) { + const reader = stream.getReader(); + while (true) { + const { done, value: msg } = await reader.read(); + if (done) { + break; } - }); - it("Del", async () => { - const sp = sup(); - const res = await conn.request(buildReqDelWAL(sthis, sp, gwCtx), { waitFor: MsgIsResDelWAL }); - if (MsgIsResDelWAL(res)) { - // expect(res.params).toEqual(sp); - expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + if (MsgIsEventGetMeta(msg)) { + // expect(msg.params).toEqual(sp); + expect(URI.from(msg.signedUrl).asObj()).toEqual(await refURL(msg)); } else { - assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + assert.fail("expected MsgEventGetMeta", JSON.stringify(msg)); } + await reader.cancel(); + } + } + expect(conn.raw.activeBinds.size).toBe(0); + // await Promise.all(streams.map((s) => s.cancel())); + }); + + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildBindGetMeta(sthis, sp, gwCtx), { waitFor: MsgIsEventGetMeta }); + if (MsgIsEventGetMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgIsEventGetMeta", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const metas = Array(5) + .fill({ cid: "x", parents: [], data: "MomRkYXRho" }) + .map((data) => { + return { ...data, cid: sthis.timeOrderedNextId().str }; }); + const res = await conn.request(buildReqPutMeta(sthis, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta }); + if (MsgIsResPutMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgIsResPutMeta", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelMeta(sthis, sp, gwCtx), { + waitFor: MsgIsResDelMeta, }); + if (MsgIsResDelMeta(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + } }); - } - ); - } - ); + }); + describe("WAL", async () => { + it("Get", async () => { + const sp = sup(); + const res = await conn.request(buildReqGetWAL(sthis, sp, gwCtx), { waitFor: MsgIsResGetWAL }); + if (MsgIsResGetWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResGetWAL", JSON.stringify(res)); + } + }); + it("Put", async () => { + const sp = sup(); + const res = await conn.request(buildReqPutWAL(sthis, sp, gwCtx), { waitFor: MsgIsResPutWAL }); + if (MsgIsResPutWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResPutWAL", JSON.stringify(res)); + } + }); + it("Del", async () => { + const sp = sup(); + const res = await conn.request(buildReqDelWAL(sthis, sp, gwCtx), { waitFor: MsgIsResDelWAL }); + if (MsgIsResDelWAL(res)) { + // expect(res.params).toEqual(sp); + expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); + } else { + assert.fail("expected MsgResDelWAL", JSON.stringify(res)); + } + }); + }); + }); + }); + }); }); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index 26eae540..dafcf9fc 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -1,18 +1,18 @@ import { exception2Result, HttpHeader, Logger, param, Result, URI } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; import { Context, Hono, Next } from "hono"; import { top_uint8 } from "../coerce-binary.js"; import { - Gestalt, buildErrorMsg, MsgBase, - EnDeCoder, ErrorMsg, MsgWithError, buildRes, MsgWithConn, GwCtx, MsgIsError, + SuperThisLogger, + EnDeCoder, + Gestalt, } from "./msg-types.js"; import { MsgDispatcher, MsgDispatcherCtx, Promisable, WSConnection } from "./msg-dispatch.js"; import { WSContext, WSContextInit, WSMessageReceive } from "hono/ws"; @@ -29,17 +29,21 @@ import { ResDelMeta, ResPutMeta, } from "./msg-type-meta.js"; -import { MetaMerger } from "./meta-merger/meta-merger.js"; -import { SQLDatabase } from "./meta-merger/abstract-sql.js"; +// import { MetaMerger } from "./meta-merger/meta-merger.js"; +// import { SQLDatabase } from "./meta-merger/abstract-sql.js"; import { WSRoom } from "./ws-room.js"; +import { CFExposeCtxItem } from "./backend/cf-hono-server.js"; +import { metaMerger } from "./meta-merger/meta-merger.js"; +import { SuperThis } from "@fireproof/core"; +import { SQLDatabase } from "./meta-merger/abstract-sql.js"; -export interface RunTimeParams { - readonly sthis: SuperThis; - readonly logger: Logger; - readonly ende: EnDeCoder; - readonly impl: HonoServerImpl; - readonly wsRoom: WSRoom; -} +// export interface RunTimeParams { +// readonly sthis: SuperThis; +// readonly logger: Logger; +// readonly ende: EnDeCoder; +// readonly impl: HonoServerImpl; +// readonly wsRoom: WSRoom; +// } export class WSContextWithId extends WSContext { readonly id: string; @@ -49,6 +53,19 @@ export class WSContextWithId extends WSContext { } } +export interface ExposeCtxItem { + readonly sthis: SuperThis; + readonly wsRoom: T; + readonly logger: Logger; + readonly ende: EnDeCoder; + readonly gestalt: Gestalt; + readonly dbFactory: () => SQLDatabase; + // readonly metaMerger: MetaMerger; + readonly id: string; +} + +export type ExposeCtxItemWithImpl = ExposeCtxItem & { impl: HonoServerImpl }; + export interface WSEventsConnId { readonly onOpen: (evt: Event, ws: WSContextWithId) => void; readonly onMessage: (evt: MessageEvent, ws: WSContextWithId) => void; @@ -59,17 +76,17 @@ export interface WSEventsConnId { // eslint-disable-next-line @typescript-eslint/no-invalid-void-type export type ConnMiddleware = (conn: WSConnection, c: Context, next: Next) => Promise; export interface HonoServerImpl { - start(): Promise; - gestalt(): Gestalt; + start(ctx: CFExposeCtxItem): Promise; + // gestalt(): Gestalt; // getConnected(): Connected[]; - calculatePreSignedUrl(p: PreSignedMsg): Promise>; + calculatePreSignedUrl(slogger: SuperThisLogger, p: PreSignedMsg): Promise>; upgradeWebSocket( createEvents: (c: Context) => WSEventsConnId | Promise> ): ConnMiddleware; - handleBindGetMeta(sthis: SuperThis, logger: Logger, msg: BindGetMeta): Promise>; - handleReqPutMeta(sthis: SuperThis, logger: Logger, msg: ReqPutMeta): Promise>; - handleReqDelMeta(sthis: SuperThis, logger: Logger, msg: ReqDelMeta): Promise>; - readonly headers: HttpHeader; + handleBindGetMeta(ctx: MsgDispatcherCtx, msg: BindGetMeta): Promise>; + handleReqPutMeta(ctx: MsgDispatcherCtx, msg: ReqPutMeta): Promise>; + handleReqDelMeta(ctx: MsgDispatcherCtx, msg: ReqDelMeta): Promise>; + // readonly headers: HttpHeader; } // export interface Connected { @@ -79,28 +96,28 @@ export interface HonoServerImpl { // } export abstract class HonoServerBase implements HonoServerImpl { - readonly _gs: Gestalt; - readonly sthis: SuperThis; - readonly logger: Logger; - readonly metaMerger: MetaMerger; - readonly headers: HttpHeader; - readonly wsRoom: WSRoom; + // readonly _gs: Gestalt; + // readonly sthis: SuperThis; + // readonly logger: Logger; + // readonly metaMerger: MetaMerger; + // readonly headers: HttpHeader; + // readonly wsRoom: WSRoom; readonly id: string; constructor( - id: string, - sthis: SuperThis, - logger: Logger, - gs: Gestalt, - sqlDb: SQLDatabase, - wsRoom: WSRoom, - headers?: HttpHeader + id: string + // sthis: SuperThis, + // logger: Logger, + // gs: Gestalt, + // sqlDb: SQLDatabase, + // wsRoom: WSRoom, + // headers?: HttpHeader ) { - this.logger = logger; - this._gs = gs; - this.sthis = sthis; - this.wsRoom = wsRoom; - this.metaMerger = new MetaMerger(id, sqlDb); - this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); + // this.logger = logger; + // this._gs = gs; + // this.sthis = sthis; + // this.wsRoom = wsRoom; + // this.metaMerger = new MetaMerger(id, sqlDb); + // this.headers = headers ? headers.Clone().Merge(CORS) : CORS.Clone(); this.id = id; // console.log("HonoServerBase-ctor", this.id, sqlDb); } @@ -111,63 +128,53 @@ export abstract class HonoServerBase implements HonoServerImpl { // abstract getConnected(): Connected[]; - start(drop = false): Promise { - return this.metaMerger.createSchema(drop).then(() => this); + start(ctx: ExposeCtxItem, drop = false): Promise { + return metaMerger(ctx) + .createSchema(drop) + .then(() => this); } - gestalt(): Gestalt { - return this._gs; - } + // gestalt(): Gestalt { + // return this._gs; + // } - async handleReqPutMeta( - sthis: SuperThis, - logger: Logger, - msg: MsgWithConn - ): Promise> { - const rUrl = await buildRes("PUT", "meta", "resPutMeta", sthis, logger, msg, this); + async handleReqPutMeta(ctx: MsgDispatcherCtx, msg: MsgWithConn): Promise> { + const rUrl = await buildRes("PUT", "meta", "resPutMeta", ctx, msg, this); if (MsgIsError(rUrl)) { return rUrl; } - await this.metaMerger.addMeta({ - logger, + await metaMerger(ctx).addMeta({ connection: msg, metas: msg.metas, }); - return buildResPutMeta(sthis, logger, msg, { ...rUrl, metas: await this.metaMerger.metaToSend(msg) }); + return buildResPutMeta(ctx, msg, { ...rUrl, metas: await metaMerger(ctx).metaToSend(msg) }); } - async handleReqDelMeta( - sthis: SuperThis, - logger: Logger, - msg: MsgWithConn - ): Promise> { - const rUrl = await buildRes("DELETE", "meta", "resDelMeta", sthis, logger, msg, this); + async handleReqDelMeta(ctx: MsgDispatcherCtx, msg: MsgWithConn): Promise> { + const rUrl = await buildRes("DELETE", "meta", "resDelMeta", ctx, msg, this); if (MsgIsError(rUrl)) { return rUrl; } - await this.metaMerger.delMeta({ - logger, + await metaMerger(ctx).delMeta({ connection: msg, }); - return buildResDelMeta(sthis, logger, msg, rUrl.signedUrl); + return buildResDelMeta(ctx, msg, rUrl.signedUrl); } async handleBindGetMeta( - sthis: SuperThis, - logger: Logger, + ctx: MsgDispatcherCtx, msg: MsgWithConn, gwCtx: GwCtx = msg ): Promise> { - const rMsg = await buildRes("GET", "meta", "eventGetMeta", sthis, logger, msg, this); + const rMsg = await buildRes("GET", "meta", "eventGetMeta", ctx, msg, this); if (MsgIsError(rMsg)) { return rMsg; } - console.log("handleBindGetMeta-in", msg, this.id); - const metas = await this.metaMerger.metaToSend(msg); - console.log("handleBindGetMeta-meta", metas); + // console.log("handleBindGetMeta-in", msg, this.id); + const metas = await metaMerger(ctx).metaToSend(msg); + // console.log("handleBindGetMeta-meta", metas); const res = buildEventGetMeta( - sthis, - logger, + ctx, msg, { ...rMsg, @@ -175,12 +182,12 @@ export abstract class HonoServerBase implements HonoServerImpl { }, gwCtx ); - console.log("handleBindGetMeta-out", res); + // console.log("handleBindGetMeta-out", res); return res; } - calculatePreSignedUrl(p: PreSignedMsg): Promise> { - const rRes = this.sthis.env.gets({ + calculatePreSignedUrl(ctx: SuperThisLogger, p: PreSignedMsg): Promise> { + const rRes = ctx.sthis.env.gets({ STORAGE_URL: param.REQUIRED, ACCESS_KEY_ID: param.REQUIRED, SECRET_ACCESS_KEY: param.REQUIRED, @@ -201,9 +208,9 @@ export abstract class HonoServerBase implements HonoServerImpl { } } -export interface HonoServerFactory { +export interface HonoServerFactory { // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - inject(c: Context, fn: (rt: RunTimeParams) => Promise): Promise; + inject(c: Context, fn: (rt: ExposeCtxItemWithImpl) => Promise): Promise; start(app: Hono): Promise; serve(app: Hono, port?: number): Promise; @@ -219,14 +226,25 @@ export const CORS = HttpHeader.from({ }); class NoBackChannel implements MsgDispatcherCtx { - readonly impl: HonoServerImpl; - readonly ctx: Context; - readonly _wsRoom: WSRoom; - constructor(impl: HonoServerImpl, c: Context, wsRoom: WSRoom) { - this.impl = impl; - this.ctx = c; - this._wsRoom = wsRoom; + readonly ctx: ExposeCtxItemWithImpl; + constructor(ctx: ExposeCtxItemWithImpl) { + this.ctx = ctx; + this.impl = ctx.impl; + this.id = ctx.id; + this.sthis = ctx.sthis; + this.logger = ctx.logger; + this.ende = ctx.ende; + this.gestalt = ctx.gestalt; + this.dbFactory = ctx.dbFactory; } + readonly impl: HonoServerImpl; + readonly sthis: SuperThis; + readonly logger: Logger; + readonly ende: EnDeCoder; + readonly gestalt: Gestalt; + readonly dbFactory: () => SQLDatabase; + readonly id: string; + get ws(): WSContextWithId { return { id: "no-id", @@ -236,7 +254,7 @@ class NoBackChannel implements MsgDispatcherCtx { } as unknown as WSContextWithId; } get wsRoom(): WSRoom { - return this._wsRoom; + return this.ctx.wsRoom; // throw new Error("NoBackChannel:wsRoom Method not implemented."); } } @@ -280,41 +298,40 @@ export class HonoServer { // app.put('/gestalt', async (c) => c.json(buildResGestalt(await c.req.json(), defaultGestaltItem({ id: "server", hasPersistent: true }).gestalt))) // app.put('/error', async (c) => c.json(buildErrorMsg(sthis, sthis.logger, await c.req.json(), new Error("test error")))) app.put("/fp", (c) => - this.factory.inject(c, async ({ sthis, logger, impl, ende, wsRoom }) => { - impl.headers.Items().forEach(([k, v]) => c.res.headers.set(k, v[0])); + this.factory.inject(c, async (ctx) => { + Object.entries(c.req.header()).forEach(([k, v]) => c.res.headers.set(k, v[0])); const rMsg = await exception2Result(() => c.req.json() as Promise); if (rMsg.isErr()) { c.status(400); - return c.json(buildErrorMsg(sthis, logger, { tid: "internal" }, rMsg.Err())); + return c.json(buildErrorMsg(ctx, { tid: "internal" }, rMsg.Err())); } - const dispatcher = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); - return dispatcher.dispatch(new NoBackChannel(impl, c, wsRoom), rMsg.Ok()); + const dispatcher = buildMsgDispatcher(ctx.sthis); + return dispatcher.dispatch(new NoBackChannel(ctx), rMsg.Ok()); }) ); // console.log("register-2.1"); app.get("/ws", (c, next) => - this.factory.inject(c, async ({ sthis, logger, ende, impl, wsRoom }) => { - return impl.upgradeWebSocket((_c) => { + this.factory.inject(c, async (ctx) => { + return ctx.impl.upgradeWebSocket((_c) => { let dp: MsgDispatcher; - const id = sthis.nextId().str; + // const id = ctx.sthis.nextId().str; // console.log("upgradeWebSocket:inject:", id); return { onOpen: (_e, _ws) => { - dp = buildMsgDispatcher(sthis, impl.gestalt(), ende, wsRoom); - console.log("onOpen:inject:", id); + dp = buildMsgDispatcher(ctx.sthis); + // console.log("onOpen:inject:", id); }, onError: (error) => { - logger.Error().Err(error).Msg("WebSocket error"); + ctx.logger.Error().Err(error).Msg("WebSocket error"); }, onMessage: async (event, ws) => { - const rMsg = await exception2Result(async () => ende.decode(await top_uint8(event.data)) as MsgBase); - console.log("onMessage:inject:", id, rMsg); + const rMsg = await exception2Result(async () => ctx.ende.decode(await top_uint8(event.data)) as MsgBase); + // console.log("onMessage:inject:", id, rMsg); if (rMsg.isErr()) { ws.send( - ende.encode( + ctx.ende.encode( buildErrorMsg( - sthis, - logger, + ctx, { message: event.data, } as ErrorMsg, @@ -324,19 +341,12 @@ export class HonoServer { ); } else { // console.log("dp-dispatch", rMsg.Ok(), dp); - await dp.dispatch( - { - impl, - ws, - wsRoom: dp.wsRoom, - }, - rMsg.Ok() - ); + await dp.dispatch({ ...ctx, ws }, rMsg.Ok()); } }, onClose: (_evt, _ws) => { // impl.delConn(ws); - console.log("onClose:inject:", id); + // console.log("onClose:inject:", id); dp = undefined as unknown as MsgDispatcher; // console.log('Connection closed') }, diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts index 7f41e391..8fc4e4dc 100644 --- a/src/v2-cloud/http-connection.ts +++ b/src/v2-cloud/http-connection.ts @@ -112,12 +112,7 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec const rReqBody = exception2Result(() => this.msgP.ende.encode(req)); if (rReqBody.isErr()) { return this.toMsg( - buildErrorMsg( - this.sthis, - this.logger, - req, - this.logger.Error().Err(rReqBody.Err()).Any("req", req).Msg("encode error").AsError() - ) + buildErrorMsg(this, req, this.logger.Error().Err(rReqBody.Err()).Any("req", req).Msg("encode error").AsError()) ); } headers.Set("Content-Length", rReqBody.Ok().byteLength.toString()); @@ -135,16 +130,13 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec ); this.logger.Debug().Url(url).Any("body", rRes).Msg("response"); if (rRes.isErr()) { - return this.toMsg( - buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Err(rRes).Msg("fetch error").AsError()) - ); + return this.toMsg(buildErrorMsg(this, req, this.logger.Error().Err(rRes).Msg("fetch error").AsError())); } const res = rRes.Ok(); if (!res.ok) { return this.toMsg( buildErrorMsg( - this.sthis, - this.logger, + this, req, this.logger .Error() @@ -162,8 +154,7 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec if (ret.isErr()) { return this.toMsg( buildErrorMsg( - this.sthis, - this.logger, + this, req, this.logger.Error().Err(ret.Err()).Msg("decode error").AsError(), this.sthis.txt.decode(data) diff --git a/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts b/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts index 9b70de55..618f75ca 100644 --- a/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts +++ b/src/v2-cloud/meta-merger/meta-by-tenant-ledger.ts @@ -60,9 +60,11 @@ export class MetaByTenantLedgerSql { readonly db: SQLDatabase; readonly tenantLedgerSql: TenantLedgerSql; - constructor(db: SQLDatabase, tenantLedgerSql: TenantLedgerSql) { + readonly id: string; + constructor(id: string, db: SQLDatabase, tenantLedgerSql: TenantLedgerSql) { this.db = db; this.tenantLedgerSql = tenantLedgerSql; + this.id = id; } // readonly #sqlCreateMetaByTenantLedger = new ResolveOnce(); diff --git a/src/v2-cloud/meta-merger/meta-merger.test.ts b/src/v2-cloud/meta-merger/meta-merger.test.ts index 406c3310..0d93e1b3 100644 --- a/src/v2-cloud/meta-merger/meta-merger.test.ts +++ b/src/v2-cloud/meta-merger/meta-merger.test.ts @@ -68,7 +68,7 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { beforeAll(async () => { // db = new Database(':memory:'); const db = await flavour.factory(); - mm = new MetaMerger("bong", db); + mm = new MetaMerger("bong", logger, db); await mm.createSchema(); }); @@ -88,14 +88,12 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { afterEach(async () => { await mm.delMeta({ - logger, connection, }); }); it("insert nothing", async () => { await mm.addMeta({ - logger, connection, metas: [], now: new Date(), @@ -113,7 +111,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { data: "MomRkYXRho", }); await mm.addMeta({ - logger, connection, metas, now: new Date(), @@ -143,7 +140,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { }; conns.push(conn); await mm.addMeta({ - logger, connection: { ...connection, conn, @@ -157,7 +153,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { await Promise.all( conns.map(async (conn) => mm.delMeta({ - logger, connection: { ...connection, conn }, metas: [], }) @@ -180,7 +175,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { .map((m) => ({ ...m, cid: sthis.timeOrderedNextId().str })); ref.push({ metas, connection }); await mm.addMeta({ - logger, connection, metas, now: new Date(), @@ -205,7 +199,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { await Promise.all( connections.map(async (connection) => mm.delMeta({ - logger, connection, metas: [], }) @@ -215,7 +208,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { it("delMeta", async () => { await mm.addMeta({ - logger, connection, metas: [ { @@ -234,7 +226,6 @@ describe.each(getSQLFlavours())("$name - MetaMerger", (flavour) => { const rows = await mm.metaToSend(connection); expect(rows.length).toBe(2); await mm.delMeta({ - logger, connection, metas: rows, now: new Date(), diff --git a/src/v2-cloud/meta-merger/meta-merger.ts b/src/v2-cloud/meta-merger/meta-merger.ts index 26094dfc..2ed130d9 100644 --- a/src/v2-cloud/meta-merger/meta-merger.ts +++ b/src/v2-cloud/meta-merger/meta-merger.ts @@ -13,7 +13,7 @@ export interface Connection { } export interface MetaMerge { - readonly logger: Logger; + // readonly logger Logger; readonly connection: Connection; readonly metas: CRDTEntry[]; readonly now?: Date; @@ -33,6 +33,15 @@ function toByConnection(connection: Connection): ByConnection { }; } +export function metaMerger(ctx: { + readonly id: string; + readonly logger: Logger; + readonly dbFactory: () => SQLDatabase; + // readonly sthis: SuperThis; +}) { + return new MetaMerger(ctx.id, ctx.logger, ctx.dbFactory()); +} + export class MetaMerger { readonly db: SQLDatabase; // readonly sthis: SuperThis; @@ -43,19 +52,21 @@ export class MetaMerger { readonly metaSend: MetaSendSql; }; + readonly logger: Logger; readonly id: string; - constructor(id: string, db: SQLDatabase) { + constructor(id: string, logger: Logger, db: SQLDatabase) { this.db = db; this.id = id; + this.logger = logger; // this.sthis = sthis; - const tenant = new TenantSql(db); - const tenantLedger = new TenantLedgerSql(db, tenant); + const tenant = new TenantSql(id, db); + const tenantLedger = new TenantLedgerSql(id, db, tenant); this.sql = { tenant, tenantLedger, - metaByTenantLedger: new MetaByTenantLedgerSql(db, tenantLedger), - metaSend: new MetaSendSql(db), + metaByTenantLedger: new MetaByTenantLedgerSql(id, db, tenantLedger), + metaSend: new MetaSendSql(id, db), }; } @@ -99,17 +110,14 @@ export class MetaMerger { updateAt: now, }); } catch (e) { - mm.logger.Warn().Err(e).Str("metaCID", meta.cid).Msg("addMeta"); + this.logger.Warn().Err(e).Str("metaCID", meta.cid).Msg("addMeta"); } } } async metaToSend(sink: Connection, now = new Date()): Promise { - console.log("metaToSend-1", this.id); const bySink = toByConnection(sink); - console.log("metaToSend-2", this.id); const rows = await this.sql.metaSend.selectToAddSend({ ...bySink, now }); - console.log("metaToSend-3", this.id); await this.sql.metaSend.insert( rows.map((row) => ({ metaCID: row.metaCID, @@ -118,7 +126,6 @@ export class MetaMerger { sendAt: row.sendAt, })) ); - console.log("metaToSend-4"); return rows.map((row) => row.meta); } } diff --git a/src/v2-cloud/meta-merger/meta-send.ts b/src/v2-cloud/meta-merger/meta-send.ts index 208cbdea..6e2814f5 100644 --- a/src/v2-cloud/meta-merger/meta-send.ts +++ b/src/v2-cloud/meta-merger/meta-send.ts @@ -36,8 +36,10 @@ export class MetaSendSql { } readonly db: SQLDatabase; - constructor(db: SQLDatabase) { + readonly id: string; + constructor(id: string, db: SQLDatabase) { this.db = db; + this.id = id; } // readonly #sqlCreateMetaSend = new ResolveOnce(); @@ -72,9 +74,9 @@ export class MetaSendSql { } async selectToAddSend(conn: ByConnection & { now: Date }): Promise { - console.log("selectToAddSend-1"); + // console.log("selectToAddSend-1"); const stmt = this.sqlSelectToAddSend(); - console.log("selectToAddSend-2"); + // console.log("selectToAddSend-2"); try { const rows = await stmt.all( conn.reqId, @@ -85,7 +87,7 @@ export class MetaSendSql { conn.reqId, conn.resId ); - console.log("selectToAddSend-3", rows); + // console.log("selectToAddSend-3", rows); return rows.map( (i) => ({ @@ -97,7 +99,8 @@ export class MetaSendSql { }) satisfies MetaSendRowWithMeta ); } catch (e) { - console.log("selectToAddSend-2-error", e); + // eslint-disable-next-line no-console + console.error("selectToAddSend:error", this.id, e); throw e; } } diff --git a/src/v2-cloud/meta-merger/tenant-ledger.ts b/src/v2-cloud/meta-merger/tenant-ledger.ts index a129cebc..a584c9e0 100644 --- a/src/v2-cloud/meta-merger/tenant-ledger.ts +++ b/src/v2-cloud/meta-merger/tenant-ledger.ts @@ -30,9 +30,11 @@ export class TenantLedgerSql { readonly db: SQLDatabase; readonly tenantSql: TenantSql; - constructor(db: SQLDatabase, tenantSql: TenantSql) { + readonly id: string; + constructor(id: string, db: SQLDatabase, tenantSql: TenantSql) { this.db = db; this.tenantSql = tenantSql; + this.id = id; } // readonly #sqlCreateTenantLedger = new ResolveOnce(); diff --git a/src/v2-cloud/meta-merger/tenant.ts b/src/v2-cloud/meta-merger/tenant.ts index c88cff1d..ee3f571b 100644 --- a/src/v2-cloud/meta-merger/tenant.ts +++ b/src/v2-cloud/meta-merger/tenant.ts @@ -23,8 +23,10 @@ export class TenantSql { } readonly db: SQLDatabase; - constructor(db: SQLDatabase) { + readonly id: string; + constructor(id: string, db: SQLDatabase) { this.db = db; + this.id = id; } // readonly #sqlCreateTenant = new ResolveOnce(); diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts index 57f2fd4c..0e71e470 100644 --- a/src/v2-cloud/msg-dispatch.ts +++ b/src/v2-cloud/msg-dispatch.ts @@ -1,9 +1,8 @@ -import { Logger } from "@adviser/cement"; -import { SuperThis, ensureLogger } from "@fireproof/core"; -import { Gestalt, MsgBase, buildErrorMsg, MsgWithError, MsgWithConn, QSId, EnDeCoder } from "./msg-types.js"; +import { SuperThis } from "@fireproof/core"; +import { MsgBase, buildErrorMsg, MsgWithError, MsgWithConn, QSId } from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; -import { HonoServerImpl, WSContextWithId } from "./hono-server.js"; +import { ExposeCtxItemWithImpl, HonoServerImpl, WSContextWithId } from "./hono-server.js"; import { UnReg } from "./msger.js"; import { WSRoom } from "./ws-room.js"; @@ -48,42 +47,41 @@ export interface ConnectionInfo { readonly resId: string; } -export interface MsgDispatcherCtx { +export interface MsgDispatcherCtx extends ExposeCtxItemWithImpl { + readonly id: string; readonly impl: HonoServerImpl; readonly ws: WSContextWithId; - readonly wsRoom: WSRoom; - // readonly send: (msg: MsgBase) => Promisable; } export interface MsgDispatchItem { readonly match: (msg: MsgBase) => boolean; readonly isNotConn?: boolean; - fn(sthis: SuperThis, logger: Logger, ctx: MsgDispatcherCtx, msg: Q): Promisable>; + fn(ctx: MsgDispatcherCtx, msg: Q): Promisable>; } export class MsgDispatcher { readonly sthis: SuperThis; - readonly logger: Logger; - // wsConn?: WSConnection; - readonly gestalt: Gestalt; + // readonly logger: Logger; + // // wsConn?: WSConnection; + // readonly gestalt: Gestalt; readonly id: string; - readonly ende: EnDeCoder; + // readonly ende: EnDeCoder; - // readonly connManager = connManager; + // // readonly connManager = connManager; - readonly wsRoom: WSRoom; + // readonly wsRoom: WSRoom; - static new(sthis: SuperThis, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom): MsgDispatcher { - return new MsgDispatcher(sthis, gestalt, ende, wsRoom); + static new(sthis: SuperThis /*, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom*/): MsgDispatcher { + return new MsgDispatcher(sthis /*, gestalt, ende, wsRoom*/); } - private constructor(sthis: SuperThis, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom) { + private constructor(sthis: SuperThis /*, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom*/) { this.sthis = sthis; - this.logger = ensureLogger(sthis, "Dispatcher"); - this.gestalt = gestalt; + // this.logger = ensureLogger(sthis, "Dispatcher"); + // this.gestalt = gestalt; this.id = sthis.nextId().str; - this.ende = ende; - this.wsRoom = wsRoom; + // this.ende = ende; + // this.wsRoom = wsRoom; } // addConn(msg: MsgBase): Result { @@ -104,9 +102,9 @@ export class MsgDispatcher { return () => ids.forEach((id) => this.items.delete(id)); } - send(ws: WSContextWithId, msg: MsgBase) { - const str = this.ende.encode(msg); - ws.send(str); + send(ctx: MsgDispatcherCtx, msg: MsgBase) { + const str = ctx.ende.encode(msg); + ctx.ws.send(str); return new Response(str); } @@ -116,30 +114,27 @@ export class MsgDispatcher { fn: (msg: MsgWithConn) => Promisable> ): Promise => { if (!ctx.wsRoom.isConnected(msg)) { - return this.send( - ctx.ws, - buildErrorMsg(this.sthis, this.logger, { ...msg }, new Error("dispatch missing connection")) - ); + return this.send(ctx, buildErrorMsg(ctx, { ...msg }, new Error("dispatch missing connection"))); // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); } const r = await fn(msg); - return Promise.resolve(this.send(ctx.ws, r)); + return Promise.resolve(this.send(ctx, r)); }; try { // console.log("dispatch-1", msg); const found = Array.from(this.items.values()).find((item) => item.match(msg)); if (!found) { // console.log("dispatch-2", msg); - return this.send(ctx.ws, buildErrorMsg(this.sthis, this.logger, msg, new Error("unexpected message"))); + return this.send(ctx, buildErrorMsg(ctx, msg, new Error("unexpected message"))); } if (!found.isNotConn) { // console.log("dispatch-3", msg); - return validateConn(msg, (msg) => found.fn(this.sthis, this.logger, ctx, msg)); + return validateConn(msg, (msg) => found.fn(ctx, msg)); } // console.log("dispatch-4", msg); - return this.send(ctx.ws, await found.fn(this.sthis, this.logger, ctx, msg)); + return this.send(ctx, await found.fn(ctx, msg)); } catch (e) { - return this.send(ctx.ws, buildErrorMsg(this.sthis, this.logger, msg, e as Error)); + return this.send(ctx, buildErrorMsg(ctx, msg, e as Error)); } } } diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts index 3ae74e95..256a30b9 100644 --- a/src/v2-cloud/msg-dispatcher-impl.ts +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -31,8 +31,8 @@ import { MsgIsReqOpenWithConn, MsgWithConn, ReqGestalt, - Gestalt, - EnDeCoder, + // Gestalt, + // EnDeCoder, buildResChat, ReqChat, MsgIsReqChat, @@ -49,110 +49,118 @@ import { ReqDelMeta, ReqPutMeta, } from "./msg-type-meta.js"; -import { WSRoom } from "./ws-room.js"; +// import { WSRoom } from "./ws-room.js"; -export function buildMsgDispatcher(sthis: SuperThis, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom): MsgDispatcher { - const dp = MsgDispatcher.new(sthis, gestalt, ende, wsRoom); +export function buildMsgDispatcher( + _sthis: SuperThis /*, gestalt: Gestalt, ende: EnDeCoder, wsRoom: WSRoom*/ +): MsgDispatcher { + const dp = MsgDispatcher.new(_sthis /*, gestalt, ende, wsRoom*/); dp.registerMsg( { match: MsgIsReqGestalt, isNotConn: true, - fn: (_sthis, _logger, _ctx, msg: ReqGestalt) => { - const resGestalt = buildResGestalt(msg, dp.gestalt); + fn: (ctx, msg: ReqGestalt) => { + const resGestalt = buildResGestalt(msg, ctx.gestalt); return resGestalt; }, }, { match: MsgIsReqOpen, isNotConn: true, - fn: (sthis, logger, ctx, msg) => { + fn: (ctx, msg) => { if (!MsgIsReqOpenWithConn(msg)) { - return buildErrorMsg(sthis, logger, msg, new Error("missing connection")); + return buildErrorMsg(ctx, msg, new Error("missing connection")); } - if (dp.wsRoom.isConnected(msg)) { - return buildResOpen(sthis, msg, msg.conn.resId); + if (ctx.wsRoom.isConnected(msg)) { + return buildResOpen(ctx.sthis, msg, msg.conn.resId); } // const resId = sthis.nextId(12).str; const resId = ctx.ws.id; - const resOpen = buildResOpen(sthis, msg, resId); - dp.wsRoom.addConn(ctx.ws, resOpen.conn); + const resOpen = buildResOpen(ctx.sthis, msg, resId); + ctx.wsRoom.addConn(ctx.ws, resOpen.conn); return resOpen; }, }, { match: MsgIsReqClose, - fn: (_sthis, _logger, _ctx, msg: MsgWithConn) => { - dp.wsRoom.removeConn(msg.conn); + fn: (ctx, msg: MsgWithConn) => { + ctx.wsRoom.removeConn(msg.conn); return buildResClose(msg, msg.conn); }, }, { match: MsgIsReqChat, - fn: (_sthis, _logger, _ctx, msg: MsgWithConn) => { - const conns = dp.wsRoom.getConns(msg.conn); + fn: (ctx, msg: MsgWithConn) => { + const conns = ctx.wsRoom.getConns(msg.conn); const ci = conns.map((c) => c.conn); for (const conn of conns) { if (qsidEqual(conn.conn, msg.conn)) { continue; } - dp.send(conn.ws, buildResChat(msg, conn.conn, `[${msg.conn.reqId}]: ${msg.message}`, ci)); + dp.send( + { + ...ctx, + ws: conn.ws, + }, + buildResChat(msg, conn.conn, `[${msg.conn.reqId}]: ${msg.message}`, ci) + ); } return buildResChat(msg, msg.conn, `ack: ${msg.message}`, ci); }, }, { match: MsgIsReqGetData, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResGetData(sthis, logger, msg, ctx.impl); + fn: (ctx, msg: MsgWithConn) => { + return buildResGetData(ctx, msg, ctx.impl); }, }, { match: MsgIsReqPutData, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResPutData(sthis, logger, msg, ctx.impl); + fn: (ctx, msg: MsgWithConn) => { + return buildResPutData(ctx, msg, ctx.impl); }, }, { match: MsgIsReqDelData, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResDelData(sthis, logger, msg, ctx.impl); + fn: (ctx, msg: MsgWithConn) => { + return buildResDelData(ctx, msg, ctx.impl); }, }, { match: MsgIsReqGetWAL, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResGetWAL(sthis, logger, msg, ctx.impl); + fn: (ctx, msg: MsgWithConn) => { + return buildResGetWAL(ctx, msg, ctx.impl); }, }, { match: MsgIsReqPutWAL, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResPutWAL(sthis, logger, msg, ctx.impl); + fn: (ctx, msg: MsgWithConn) => { + return buildResPutWAL(ctx, msg, ctx.impl); }, }, { match: MsgIsReqDelWAL, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return buildResDelWAL(sthis, logger, msg, ctx.impl); + fn: (ctx, msg: MsgWithConn) => { + return buildResDelWAL(ctx, msg, ctx.impl); }, }, { match: MsgIsBindGetMeta, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConn) => { // console.log("MsgIsBindGetMeta", msg); - return ctx.impl.handleBindGetMeta(sthis, logger, msg); + return ctx.impl.handleBindGetMeta(ctx, msg); }, }, { match: MsgIsReqPutMeta, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return ctx.impl.handleReqPutMeta(sthis, logger, msg); + fn: (ctx, msg: MsgWithConn) => { + return ctx.impl.handleReqPutMeta(ctx, msg); }, }, { match: MsgIsReqDelMeta, - fn: (sthis, logger, ctx, msg: MsgWithConn) => { - return ctx.impl.handleReqDelMeta(sthis, logger, msg); + fn: (ctx, msg: MsgWithConn) => { + return ctx.impl.handleReqDelMeta(ctx, msg); }, } ); diff --git a/src/v2-cloud/msg-raw-connection-base.ts b/src/v2-cloud/msg-raw-connection-base.ts index 66bf75e1..e8a29dbd 100644 --- a/src/v2-cloud/msg-raw-connection-base.ts +++ b/src/v2-cloud/msg-raw-connection-base.ts @@ -1,6 +1,5 @@ -import { Logger } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; -import { MsgBase, ErrorMsg, buildErrorMsg } from "./msg-types.js"; +import { MsgBase, ErrorMsg, buildErrorMsg, SuperThisLogger } from "./msg-types.js"; import { ExchangedGestalt, OnErrorFn, UnReg } from "./msger.js"; export class MsgRawConnectionBase { @@ -19,13 +18,13 @@ export class MsgRawConnectionBase { return () => this.onErrorFns.delete(key); } - buildErrorMsg(logger: Logger, msg: Partial, err: Error): ErrorMsg { + buildErrorMsg(slogger: SuperThisLogger, msg: Partial, err: Error): ErrorMsg { // const logLine = this.sthis.logger.Error().Err(err).Any("msg", msg); const rmsg = Array.from(this.onErrorFns.values()).reduce((msg, fn) => { return fn(msg, err); }, msg); - const emsg = buildErrorMsg(this.sthis, logger, rmsg, err); - logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); + const emsg = buildErrorMsg(slogger, rmsg, err); + slogger.logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); return emsg; } } diff --git a/src/v2-cloud/msg-type-meta.ts b/src/v2-cloud/msg-type-meta.ts index 80c2201d..71783674 100644 --- a/src/v2-cloud/msg-type-meta.ts +++ b/src/v2-cloud/msg-type-meta.ts @@ -1,4 +1,4 @@ -import { Logger, VERSION } from "@adviser/cement"; +import { VERSION } from "@adviser/cement"; import { CRDTEntry } from "@fireproof/core"; import { GwCtx, @@ -9,6 +9,7 @@ import { NextId, ReqSignedUrlParam, ResOptionalSignedUrl, + SuperThisLogger, } from "./msg-types.js"; /* Put Meta */ @@ -43,8 +44,7 @@ export function MsgIsReqPutMeta(msg: MsgBase): msg is ReqPutMeta { } export function buildResPutMeta( - _sthis: NextId, - _logger: Logger, + _slogger: SuperThisLogger, req: MsgWithTenantLedger>, meta: QSMeta ): ResPutMeta { @@ -93,8 +93,7 @@ export function buildBindGetMeta(sthis: NextId, params: ReqSignedUrlParam, gwCtx } export function buildEventGetMeta( - _sthis: NextId, - _logger: Logger, + _slogger: SuperThisLogger, req: MsgWithTenantLedger>, metaParam: QSMeta, gwCtx: GwCtx @@ -138,8 +137,7 @@ export interface ResDelMeta extends MsgWithTenantLedger, ResOptiona } export function buildResDelMeta( - _sthis: NextId, - _logger: Logger, + _slogger: SuperThisLogger, req: MsgWithTenantLedger>, signedUrl?: string ): ResDelMeta { diff --git a/src/v2-cloud/msg-types-data.ts b/src/v2-cloud/msg-types-data.ts index 12ceeb77..b8c616a1 100644 --- a/src/v2-cloud/msg-types-data.ts +++ b/src/v2-cloud/msg-types-data.ts @@ -1,5 +1,4 @@ -import { Logger, Result, URI } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; +import { Result, URI } from "@adviser/cement"; import { ReqSignedUrl, NextId, @@ -12,6 +11,7 @@ import { GwCtx, MsgIsTenantLedger, MsgWithConn, + SuperThisLogger, } from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; @@ -37,16 +37,15 @@ export function MsgIsResGetData(msg: MsgBase): msg is ResGetData { } export interface CalculatePreSignedUrl { - calculatePreSignedUrl(p: PreSignedMsg): Promise>; + calculatePreSignedUrl(ctx: SuperThisLogger, p: PreSignedMsg): Promise>; } export function buildResGetData( - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: MsgWithConn, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResGetData>("GET", "data", "resGetData", sthis, logger, req, ctx); + return buildRes, ResGetData>("GET", "data", "resGetData", slogger, req, ctx); } export interface ReqPutData extends ReqSignedUrl { @@ -71,12 +70,11 @@ export function MsgIsResPutData(msg: MsgBase): msg is ResPutData { } export function buildResPutData( - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: MsgWithConn, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResPutData>("PUT", "data", "resPutData", sthis, logger, req, ctx); + return buildRes, ResPutData>("PUT", "data", "resPutData", slogger, req, ctx); } export interface ReqDelData extends ReqSignedUrl { @@ -100,10 +98,9 @@ export function MsgIsResDelData(msg: MsgBase): msg is ResDelData { } export function buildResDelData( - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: MsgWithConn, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResDelData>("DELETE", "data", "resDelData", sthis, logger, req, ctx); + return buildRes, ResDelData>("DELETE", "data", "resDelData", slogger, req, ctx); } diff --git a/src/v2-cloud/msg-types-wal.ts b/src/v2-cloud/msg-types-wal.ts index 3363cd43..898703c1 100644 --- a/src/v2-cloud/msg-types-wal.ts +++ b/src/v2-cloud/msg-types-wal.ts @@ -1,5 +1,3 @@ -import { Logger } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; import { MsgBase, MsgWithError, @@ -13,6 +11,7 @@ import { MsgIsTenantLedger, MsgWithTenantLedger, MsgWithConn, + SuperThisLogger, } from "./msg-types.js"; import { CalculatePreSignedUrl } from "./msg-types-data.js"; @@ -38,20 +37,11 @@ export function MsgIsResGetWAL(msg: MsgBase): msg is ResGetWAL { } export function buildResGetWAL( - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResGetWAL>( - "GET", - "wal", - "resGetWAL", - sthis, - logger, - req, - ctx - ); + return buildRes>, ResGetWAL>("GET", "wal", "resGetWAL", slogger, req, ctx); } export interface ReqPutWAL extends Omit { @@ -76,20 +66,11 @@ export function MsgIsResPutWAL(msg: MsgBase): msg is ResPutWAL { } export function buildResPutWAL( - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResPutWAL>( - "PUT", - "wal", - "resPutWAL", - sthis, - logger, - req, - ctx - ); + return buildRes>, ResPutWAL>("PUT", "wal", "resPutWAL", slogger, req, ctx); } export interface ReqDelWAL extends Omit { @@ -113,8 +94,7 @@ export function MsgIsResDelWAL(msg: MsgBase): msg is ResDelWAL { } export function buildResDelWAL( - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { @@ -122,8 +102,7 @@ export function buildResDelWAL( "DELETE", "wal", "resDelWAL", - sthis, - logger, + slogger, req, ctx ); diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts index 3ec3e2a6..2b4d3b26 100644 --- a/src/v2-cloud/msg-types.ts +++ b/src/v2-cloud/msg-types.ts @@ -530,14 +530,13 @@ export type ReqRes = Readonly, error: Error, body?: string, stack?: string[] ): ErrorMsg { - if (!stack && sthis.env.get("FP_STACK")) { + if (!stack && slogger.sthis.env.get("FP_STACK")) { stack = error.stack?.split("\n"); } const msg = { @@ -549,7 +548,7 @@ export function buildErrorMsg( body, stack, } satisfies ErrorMsg; - logger.Any("ErrorMsg", msg); + slogger.logger.Any("ErrorMsg", msg); return msg; } @@ -604,12 +603,16 @@ export interface ResOptionalSignedUrl extends MsgWithTenantLedger { readonly signedUrl?: string; } +export interface SuperThisLogger { + readonly sthis: SuperThis; + readonly logger: Logger; +} + export async function buildRes>, S extends ResSignedUrl>( method: SignedUrlParam["method"], store: FPStoreTypes, type: string, - sthis: SuperThis, - logger: Logger, + slogger: SuperThisLogger, req: Q, ctx: CalculatePreSignedUrl ): Promise> { @@ -625,9 +628,9 @@ export async function buildRes Promise): Promise { + inject( + c: Context, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + fn: (rt: ExposeCtxItemWithImpl) => Promise + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + ): Promise { // this._env = c.env; // const sthis = ensureSuperThis(); const sthis = this.sthis; @@ -182,13 +187,24 @@ export class NodeHonoFactory implements HonoServerFactory { hasPersistent: true, protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], }); - const gs = + const gestalt = this.params.gs ?? defaultGestalt(msgP, { id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", }); - const nhs = new NodeHonoServer(id, sthis, this, gs, this.params.sql, this._wsRoom); - return nhs.start().then((nhs) => fn({ sthis, logger, ende, impl: nhs, wsRoom: this._wsRoom })); + + const ctx: ExposeCtxItem = { + id, + sthis, + logger, + wsRoom: this._wsRoom, + gestalt, + ende, + dbFactory: () => this.params.sql, + }; + + const nhs = new NodeHonoServer(id, this); + return nhs.start(ctx).then((nhs) => fn({ ...ctx, impl: nhs })); } async start(app: Hono): Promise { @@ -223,16 +239,18 @@ export class NodeHonoFactory implements HonoServerFactory { export class NodeHonoServer extends HonoServerBase implements HonoServerImpl { readonly _upgradeWebSocket: UpgradeWebSocket; // readonly wsRoom: NodeWSRoom; + readonly wsRoom: WSRoom; constructor( id: string, - sthis: SuperThis, - factory: NodeHonoFactory, - gs: Gestalt, - sqldb: SQLDatabase, - wsRoom: WSRoom, - headers?: HttpHeader + // sthis: SuperThis, + factory: NodeHonoFactory + // gs: Gestalt, + // sqldb: SQLDatabase, + // wsRoom: WSRoom, + // headers?: HttpHeader ) { - super(id, sthis, sthis.logger, gs, sqldb, wsRoom, headers); + super(id); + this.wsRoom = factory._wsRoom; this._upgradeWebSocket = factory._upgradeWebSocket; } diff --git a/src/v2-cloud/ws-connection.ts b/src/v2-cloud/ws-connection.ts index 0811b4af..b8eb2ca6 100644 --- a/src/v2-cloud/ws-connection.ts +++ b/src/v2-cloud/ws-connection.ts @@ -46,7 +46,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti const onOpenFuture: Future> = new Future>(); const timer = setTimeout(() => { const err = this.logger.Error().Dur("timeout", this.msgP.timeout).Msg("Timeout").AsError(); - this.toMsg(buildErrorMsg(this.sthis, this.logger, {} as MsgBase, err)); + this.toMsg(buildErrorMsg(this, {} as MsgBase, err)); onOpenFuture.resolve(Result.Err(err)); }, this.msgP.timeout); this.ws.onopen = () => { @@ -56,18 +56,13 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti this.ws.onerror = (ierr) => { const err = this.logger.Error().Err(ierr).Msg("WS Error").AsError(); onOpenFuture.resolve(Result.Err(err)); - const res = this.buildErrorMsg(this.logger.Error(), {}, err); + const res = this.buildErrorMsg(this, {}, err); this.toMsg(res); }; this.ws.onmessage = (evt) => { if (!this.opened) { this.toMsg( - buildErrorMsg( - this.sthis, - this.logger, - {} as MsgBase, - this.logger.Error().Msg("Received message before onOpen").AsError() - ) + buildErrorMsg(this, {} as MsgBase, this.logger.Error().Msg("Received message before onOpen").AsError()) ); } this.#wsOnMessage(evt); @@ -78,7 +73,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti this.close().catch((ierr) => { const err = this.logger.Error().Err(ierr).Msg("close error").AsError(); onOpenFuture.resolve(Result.Err(err)); - this.toMsg(buildErrorMsg(this.sthis, this.logger, { tid: "internal" } as MsgBase, err)); + this.toMsg(buildErrorMsg(this, { tid: "internal" } as MsgBase, err)); }); }; /* wait for onOpen */ @@ -200,7 +195,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti async request(req: Q, opts: RequestOpts): Promise> { if (!this.opened) { - return buildErrorMsg(this.sthis, this.logger, req, this.logger.Error().Msg("Connection not open").AsError()); + return buildErrorMsg(this, req, this.logger.Error().Msg("Connection not open").AsError()); } const future = new Future(); this.waitForTid.set(req.tid, { tid: req.tid, future, waitFor: opts.waitFor, timeout: opts.timeout }); diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts index 7cc44f97..941fe6db 100644 --- a/src/v2-cloud/ws-sockets.test.ts +++ b/src/v2-cloud/ws-sockets.test.ts @@ -62,7 +62,7 @@ describe("test multiple connections", () => { const rest = [...conns]; for (const c of conns) { - console.log("Sending a chat request", rest.length, conns.length); + // console.log("Sending a chat request", rest.length, conns.length); const act = await c.request(buildReqChat(sthis, c.conn, "Hello"), { waitFor: MsgIsResChat }); if (MsgIsResChat(act)) { expect(act.targets.length).toBe(rest.length); From 8bb45b04e90ebdea83a3ff6f298aa14dcbcc3415 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 10 Mar 2025 11:51:46 +0100 Subject: [PATCH 07/14] WIP --- package.json | 1 + src/v2-cloud/client/cloud-gateway.test.ts | 21 +++- src/v2-cloud/client/gateway.ts | 31 +++-- src/v2-cloud/connection.test.ts | 89 ++++++++++---- src/v2-cloud/hono-server.ts | 8 +- src/v2-cloud/http-connection.ts | 12 +- src/v2-cloud/msg-dispatch.ts | 1 + src/v2-cloud/msg-dispatcher-impl.ts | 3 +- src/v2-cloud/msg-raw-connection-base.ts | 16 ++- src/v2-cloud/msg-type-meta.ts | 23 +++- src/v2-cloud/msg-types-data.ts | 16 +-- src/v2-cloud/msg-types-wal.ts | 14 +-- src/v2-cloud/msg-types.ts | 139 ++++++++++------------ src/v2-cloud/msger.ts | 31 +++-- src/v2-cloud/test-helper.ts | 76 ++++++++++-- src/v2-cloud/ws-connection.ts | 13 +- src/v2-cloud/ws-sockets.test.ts | 31 ++++- 17 files changed, 361 insertions(+), 164 deletions(-) diff --git a/package.json b/package.json index 4c38ecdd..18cc3325 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "esbuild-plugin-replace": "^1.4.0", "esbuild-plugin-resolve": "^2.0.0", "eslint": "^9.22.0", + "jose": "^6.0.8", "netlify": "^13.3.3", "netlify-cli": "^19.0.0", "partyserver": "^0.0.65", diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index fda95050..57609713 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -1,11 +1,12 @@ import { Hono } from "hono"; import { HonoServer } from "../hono-server.js"; import { defaultGestalt } from "../msg-types.js"; -import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle } from "../test-helper.js"; +import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle, mockGetAuthFactory } from "../test-helper.js"; import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; import { defaultMsgParams } from "../msger.js"; import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; import { BuildURI } from "@adviser/cement"; +import { SessionTokenService } from "../../sts-service/sts-service.js"; const sthis = ensureSuperThis(); const msgP = defaultMsgParams(sthis, { hasPersistent: true }); @@ -13,15 +14,29 @@ const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gateway", ({ factory }) => { const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); - const style = wsStyle(sthis, port, msgP, my); + let style; let server: HonoServer; let gw: bs.Gateway; let unregister: () => void; let url: BuildURI; beforeAll(async () => { + const keyPair = await SessionTokenService.generateKeyPair(); + const authFactory = await mockGetAuthFactory( + keyPair.strings.privateKey, + { + userId: "hello", + tenants: [], + ledgers: [], + }, + sthis + ); + // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); + style = wsStyle(sthis, authFactory, port, msgP, my); const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.once(app, port)); + server = await factory(sthis, msgP, style.remoteGestalt, port, keyPair.strings.publicKey).then((srv) => + srv.once(app, port) + ); unregister = registerFireproofCloudStoreProtocol("fireproof:"); gw = new FireproofCloudGateway(sthis); url = BuildURI.from(`fireproof://localhost:${port}/`) diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index d47cbe60..e7525b54 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -11,10 +11,18 @@ import { ReqSignedUrl, MsgWithError, ResSignedUrl, + authType, } from "../msg-types.js"; import { to_uint8 } from "../../coerce-binary.js"; import { MsgConnected, Msger } from "../msger.js"; -import { MsgIsResGetData, MsgIsResPutData, ResDelData, ResGetData, ResPutData } from "../msg-types-data.js"; +import { + MsgIsResDelData, + MsgIsResGetData, + MsgIsResPutData, + ResDelData, + ResGetData, + ResPutData, +} from "../msg-types-data.js"; const VERSION = "v0.1-fp-cloud"; @@ -58,7 +66,7 @@ abstract class BaseGateway { async delete(uri: URI, prConn: Promise>): Promise> { const rConn = await prConn; if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); + return this.logger.Error().Err(rConn).Msg("Error in deleteConn").ResultError(); } const conn = rConn.Ok(); // this.logger.Debug().Any("conn", conn.key).Msg("del"); @@ -77,7 +85,7 @@ abstract class BaseGateway { // return Result.Ok(buildReqSignedUrl(this.sthis, type, sig, conn)) // } - async getResSignedUrl( + async getReqSignedUrl( type: string, method: HttpMethods, store: FPStoreTypes, @@ -102,6 +110,7 @@ abstract class BaseGateway { } const rsu = { tid: this.sthis.nextId().str, + auth: await conn.authFactory(), type, // conn: conn.conn, tenant: { @@ -179,7 +188,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { } async getConn(uri: URI, conn: MsgConnected): Promise> { // type: string, method: HttpMethods, store: FPStoreTypes, waitForFn: - const rResSignedUrl = await this.getResSignedUrl( + const rResSignedUrl = await this.getReqSignedUrl( "reqGetData", "GET", "data", @@ -194,7 +203,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { return this.getObject(uri, downloadUrl); } async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { - const rResSignedUrl = await this.getResSignedUrl( + const rResSignedUrl = await this.getReqSignedUrl( "reqPutData", "PUT", "data", @@ -209,11 +218,11 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { return this.putObject(uri, uploadUrl, body); } async delConn(uri: URI, conn: MsgConnected): Promise> { - const rResSignedUrl = await this.getResSignedUrl( + const rResSignedUrl = await this.getReqSignedUrl( "reqDelData", "DELETE", "data", - MsgIsResPutData, + MsgIsResDelData, uri, conn ); @@ -469,7 +478,13 @@ export class FireproofCloudGateway implements bs.Gateway { // } // tenant = rfingerprint.Ok().fingerPrint; // } - const qOpen = buildReqOpen(this.sthis, {}); + + const authJWK = uri.getParamResult("authJWK"); + if (authJWK.isErr()) { + return this.logger.Error().Err(authJWK).Msg("Missing URI authJWK").ResultError(); + } + + const qOpen = buildReqOpen(this.sthis, authType(authJWK.Ok()), {}); let cUrl = uri.build().protocol(params.protocol).cleanParams().URI(); if (cUrl.pathname === "/") { diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index 00052131..d396373f 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -11,6 +11,7 @@ import { GwCtx, MsgWithError, ResOptionalSignedUrl, + ReqOpen, } from "./msg-types.js"; import { MsgIsResGetData, @@ -32,7 +33,14 @@ import { applyStart, defaultMsgParams, MsgConnected, Msger } from "./msger.js"; import { HonoServer } from "./hono-server.js"; import { Hono } from "hono"; import { calculatePreSignedUrl } from "./pre-signed-url.js"; -import { CFHonoServerFactory, httpStyle, NodeHonoServerFactory, resolveToml, wsStyle } from "./test-helper.js"; +import { + CFHonoServerFactory, + httpStyle, + mockGetAuthFactory, + NodeHonoServerFactory, + resolveToml, + wsStyle, +} from "./test-helper.js"; import { buildReqDelMeta, buildBindGetMeta, @@ -45,6 +53,7 @@ import { MsgIsEventGetMeta, MsgIsResPutMeta, } from "./msg-type-meta.js"; +import { SessionTokenService } from "../sts-service/sts-service.js"; async function refURL(sp: ResOptionalSignedUrl) { const { env } = await resolveToml("D1"); @@ -68,9 +77,14 @@ async function refURL(sp: ResOptionalSignedUrl) { describe("Connection", () => { const sthis = ensureSuperThis(); const msgP = defaultMsgParams(sthis, { hasPersistent: true }); + let pubEnvJWK: string; + // let privEnvJWK: string beforeAll(async () => { sthis.env.sets((await resolveToml("D1")).env as unknown as Record); + const pair = await SessionTokenService.generateKeyPair(); + pubEnvJWK = pair.strings.publicKey; + // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); }); describe.each([ @@ -80,17 +94,39 @@ describe("Connection", () => { CFHonoServerFactory("D1"), ])("$name - Connection", (honoServer) => { const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); - const qOpen = buildReqOpen(sthis, { reqId: "req-open-test" }); const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - describe.each([ - // force multiple lines - httpStyle(sthis, port, msgP, my), - wsStyle(sthis, port, msgP, my), - ])(`${honoServer.name} - $name`, (style) => { + + const styles: (ReturnType | ReturnType)[] = []; + beforeAll(async () => { + const pair = await SessionTokenService.generateKeyPair(); + const authFactory = await mockGetAuthFactory( + pair.strings.privateKey, + { + userId: "hello", + tenants: [], + ledgers: [], + }, + sthis + ); + styles.push( + ...[ + // force multiple lines + httpStyle(sthis, authFactory, port, msgP, my), + wsStyle(sthis, authFactory, port, msgP, my), + ] + ); + }); + + describe.each(styles)(`${honoServer.name} - $name`, (style) => { + const authFactory = style.authFactory; let server: HonoServer; + let qOpen: ReqOpen; beforeAll(async () => { const app = new Hono(); - server = await honoServer.factory(sthis, msgP, style.remoteGestalt, port).then((srv) => srv.once(app, port)); + qOpen = buildReqOpen(sthis, await authFactory(), { reqId: "req-open-test" }); + server = await honoServer + .factory(sthis, msgP, style.remoteGestalt, port, pubEnvJWK) + .then((srv) => srv.once(app, port)); }); afterAll(async () => { // console.log("closing server"); @@ -111,7 +147,9 @@ describe("Connection", () => { describe(`connection`, () => { let c: MsgConnected; beforeEach(async () => { - const rC = await style.ok.open().then((r) => MsgConnected.connect(r, { reqId: "req-open-testx" })); + const rC = await style.ok + .open() + .then((r) => MsgConnected.connect(authFactory, r, { reqId: "req-open-testx" })); expect(rC.isOk()).toBeTruthy(); c = rC.Ok(); expect(c.conn).toEqual({ @@ -127,6 +165,7 @@ describe("Connection", () => { const r = await c.raw.request( { tid: "test", + auth: await authFactory(), type: "kaputt", version: "FP-MSG-1.0", }, @@ -150,7 +189,7 @@ describe("Connection", () => { }); it("gestalt url http", async () => { const msgP = defaultMsgParams(sthis, {}); - const req = buildReqGestalt(sthis, defaultGestalt(msgP, { id: "test" })); + const req = buildReqGestalt(sthis, await authFactory(), defaultGestalt(msgP, { id: "test" })); const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); if (!MsgIsResGestalt(r)) { assert.fail("expected MsgError", JSON.stringify(r)); @@ -159,7 +198,7 @@ describe("Connection", () => { }); it("openConnection", async () => { - const req = buildReqOpen(sthis, { ...c.conn }); + const req = buildReqOpen(sthis, await authFactory(), { ...c.conn }); const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); if (!MsgIsResOpen(r)) { assert.fail(JSON.stringify(r)); @@ -174,7 +213,7 @@ describe("Connection", () => { }); it("open", async () => { - const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, { + const rC = await Msger.connect(sthis, authFactory, URI.from(`http://localhost:${port}/fp`), msgP, { reqId: "req-open-testy", }); expect(rC.isOk()).toBeTruthy(); @@ -194,7 +233,7 @@ describe("Connection", () => { let gwCtx: GwCtx; let conn: MsgConnected; beforeAll(async () => { - const rC = await Msger.connect(sthis, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); + const rC = await Msger.connect(sthis, authFactory, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); expect(rC.isOk()).toBeTruthy(); conn = rC.Ok(); gwCtx = { @@ -209,7 +248,9 @@ describe("Connection", () => { await conn.close(); }); it("Open", async () => { - const res = await conn.raw.request(buildReqOpen(sthis, conn.conn), { waitFor: MsgIsResOpen }); + const res = await conn.raw.request(buildReqOpen(sthis, await authFactory(), conn.conn), { + waitFor: MsgIsResOpen, + }); if (!MsgIsResOpen(res)) { assert.fail("expected MsgResOpen", JSON.stringify(res)); } @@ -260,10 +301,11 @@ describe("Connection", () => { it("bind stop", async () => { const sp = sup(); expect(conn.raw.activeBinds.size).toBe(0); + const auth = await authFactory(); const streams: ReadableStream>[] = Array(5) .fill(0) .map(() => { - return conn.bind(buildBindGetMeta(sthis, sp, gwCtx), { + return conn.bind(buildBindGetMeta(sthis, auth, sp, gwCtx), { waitFor: MsgIsEventGetMeta, }); }); @@ -289,7 +331,9 @@ describe("Connection", () => { it("Get", async () => { const sp = sup(); - const res = await conn.request(buildBindGetMeta(sthis, sp, gwCtx), { waitFor: MsgIsEventGetMeta }); + const res = await conn.request(buildBindGetMeta(sthis, await authFactory(), sp, gwCtx), { + waitFor: MsgIsEventGetMeta, + }); if (MsgIsEventGetMeta(res)) { // expect(res.params).toEqual(sp); expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); @@ -304,7 +348,9 @@ describe("Connection", () => { .map((data) => { return { ...data, cid: sthis.timeOrderedNextId().str }; }); - const res = await conn.request(buildReqPutMeta(sthis, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta }); + const res = await conn.request(buildReqPutMeta(sthis, await authFactory(), sp, metas, gwCtx), { + waitFor: MsgIsResPutMeta, + }); if (MsgIsResPutMeta(res)) { // expect(res.params).toEqual(sp); expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); @@ -314,9 +360,12 @@ describe("Connection", () => { }); it("Del", async () => { const sp = sup(); - const res = await conn.request(buildReqDelMeta(sthis, sp, gwCtx), { - waitFor: MsgIsResDelMeta, - }); + const res = await conn.request( + buildReqDelMeta(sthis, await authFactory(), sp, gwCtx), + { + waitFor: MsgIsResDelMeta, + } + ); if (MsgIsResDelMeta(res)) { // expect(res.params).toEqual(sp); expect(URI.from(res.signedUrl).asObj()).toEqual(await refURL(res)); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index dafcf9fc..8b922ea3 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -10,7 +10,7 @@ import { MsgWithConn, GwCtx, MsgIsError, - SuperThisLogger, + MsgTypesCtx, EnDeCoder, Gestalt, } from "./msg-types.js"; @@ -79,7 +79,7 @@ export interface HonoServerImpl { start(ctx: CFExposeCtxItem): Promise; // gestalt(): Gestalt; // getConnected(): Connected[]; - calculatePreSignedUrl(slogger: SuperThisLogger, p: PreSignedMsg): Promise>; + calculatePreSignedUrl(msgCtx: MsgTypesCtx, p: PreSignedMsg): Promise>; upgradeWebSocket( createEvents: (c: Context) => WSEventsConnId | Promise> ): ConnMiddleware; @@ -158,7 +158,7 @@ export abstract class HonoServerBase implements HonoServerImpl { await metaMerger(ctx).delMeta({ connection: msg, }); - return buildResDelMeta(ctx, msg, rUrl.signedUrl); + return buildResDelMeta(msg, rUrl.signedUrl); } async handleBindGetMeta( @@ -186,7 +186,7 @@ export abstract class HonoServerBase implements HonoServerImpl { return res; } - calculatePreSignedUrl(ctx: SuperThisLogger, p: PreSignedMsg): Promise> { + calculatePreSignedUrl(ctx: MsgTypesCtx, p: PreSignedMsg): Promise> { const rRes = ctx.sthis.env.gets({ STORAGE_URL: param.REQUIRED, ACCESS_KEY_ID: param.REQUIRED, diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts index 8fc4e4dc..7cf21acf 100644 --- a/src/v2-cloud/http-connection.ts +++ b/src/v2-cloud/http-connection.ts @@ -1,6 +1,6 @@ import { HttpHeader, Logger, Result, URI, exception2Result } from "@adviser/cement"; import { SuperThis, ensureLogger } from "@fireproof/core"; -import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError } from "./msg-types.js"; +import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError, AuthFactory } from "./msg-types.js"; import { ActiveStream, ExchangedGestalt, @@ -21,12 +21,20 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec readonly #onMsg = new Map(); - constructor(sthis: SuperThis, uris: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { + readonly auth: AuthFactory; + constructor( + sthis: SuperThis, + auth: AuthFactory, + uris: URI[], + msgP: MsgerParamsWithEnDe, + exGestalt: ExchangedGestalt + ) { super(sthis, exGestalt); this.logger = ensureLogger(sthis, "HttpConnection"); // this.msgParam = msgP; this.baseURIs = uris; this.msgP = msgP; + this.auth = auth; } send(_msg: Q): Promise> { diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts index 0e71e470..72a9d3e3 100644 --- a/src/v2-cloud/msg-dispatch.ts +++ b/src/v2-cloud/msg-dispatch.ts @@ -50,6 +50,7 @@ export interface ConnectionInfo { export interface MsgDispatcherCtx extends ExposeCtxItemWithImpl { readonly id: string; readonly impl: HonoServerImpl; + // readonly auth: AuthFactory; readonly ws: WSContextWithId; } diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts index 256a30b9..9b5c014c 100644 --- a/src/v2-cloud/msg-dispatcher-impl.ts +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -28,7 +28,6 @@ import { MsgIsReqOpen, buildErrorMsg, buildResOpen, - MsgIsReqOpenWithConn, MsgWithConn, ReqGestalt, // Gestalt, @@ -68,7 +67,7 @@ export function buildMsgDispatcher( match: MsgIsReqOpen, isNotConn: true, fn: (ctx, msg) => { - if (!MsgIsReqOpenWithConn(msg)) { + if (!MsgIsReqOpen(msg)) { return buildErrorMsg(ctx, msg, new Error("missing connection")); } if (ctx.wsRoom.isConnected(msg)) { diff --git a/src/v2-cloud/msg-raw-connection-base.ts b/src/v2-cloud/msg-raw-connection-base.ts index e8a29dbd..e75bf4c4 100644 --- a/src/v2-cloud/msg-raw-connection-base.ts +++ b/src/v2-cloud/msg-raw-connection-base.ts @@ -1,6 +1,7 @@ import { SuperThis } from "@fireproof/core"; -import { MsgBase, ErrorMsg, buildErrorMsg, SuperThisLogger } from "./msg-types.js"; +import { MsgBase, ErrorMsg, buildErrorMsg } from "./msg-types.js"; import { ExchangedGestalt, OnErrorFn, UnReg } from "./msger.js"; +import { Logger } from "@adviser/cement"; export class MsgRawConnectionBase { readonly sthis: SuperThis; @@ -18,13 +19,20 @@ export class MsgRawConnectionBase { return () => this.onErrorFns.delete(key); } - buildErrorMsg(slogger: SuperThisLogger, msg: Partial, err: Error): ErrorMsg { + buildErrorMsg( + msgCtx: { + readonly logger: Logger; + readonly sthis: SuperThis; + }, + msg: Partial, + err: Error + ): ErrorMsg { // const logLine = this.sthis.logger.Error().Err(err).Any("msg", msg); const rmsg = Array.from(this.onErrorFns.values()).reduce((msg, fn) => { return fn(msg, err); }, msg); - const emsg = buildErrorMsg(slogger, rmsg, err); - slogger.logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); + const emsg = buildErrorMsg(msgCtx, rmsg, err); + msgCtx.logger.Error().Err(err).Any("msg", rmsg).Msg("connection error"); return emsg; } } diff --git a/src/v2-cloud/msg-type-meta.ts b/src/v2-cloud/msg-type-meta.ts index 71783674..844a14ea 100644 --- a/src/v2-cloud/msg-type-meta.ts +++ b/src/v2-cloud/msg-type-meta.ts @@ -1,6 +1,7 @@ import { VERSION } from "@adviser/cement"; import { CRDTEntry } from "@fireproof/core"; import { + AuthType, GwCtx, MsgBase, MsgWithConn, @@ -9,7 +10,7 @@ import { NextId, ReqSignedUrlParam, ResOptionalSignedUrl, - SuperThisLogger, + MsgTypesCtx, } from "./msg-types.js"; /* Put Meta */ @@ -25,11 +26,13 @@ export interface ResPutMeta extends MsgWithTenantLedger, QSMeta { export function buildReqPutMeta( sthis: NextId, + auth: AuthType, signedUrlParams: ReqSignedUrlParam, metas: CRDTEntry[], gwCtx: GwCtx ): ReqPutMeta { return { + auth, tid: sthis.nextId().str, type: "reqPutMeta", ...gwCtx, @@ -44,7 +47,7 @@ export function MsgIsReqPutMeta(msg: MsgBase): msg is ReqPutMeta { } export function buildResPutMeta( - _slogger: SuperThisLogger, + _msgCtx: MsgTypesCtx, req: MsgWithTenantLedger>, meta: QSMeta ): ResPutMeta { @@ -82,8 +85,9 @@ export interface EventGetMeta extends MsgWithTenantLedger, ResOptio readonly type: "eventGetMeta"; } -export function buildBindGetMeta(sthis: NextId, params: ReqSignedUrlParam, gwCtx: GwCtx): BindGetMeta { +export function buildBindGetMeta(sthis: NextId, auth: AuthType, params: ReqSignedUrlParam, gwCtx: GwCtx): BindGetMeta { return { + auth, tid: sthis.nextId().str, ...gwCtx, type: "bindGetMeta", @@ -93,7 +97,7 @@ export function buildBindGetMeta(sthis: NextId, params: ReqSignedUrlParam, gwCtx } export function buildEventGetMeta( - _slogger: SuperThisLogger, + _msgCtx: MsgTypesCtx, req: MsgWithTenantLedger>, metaParam: QSMeta, gwCtx: GwCtx @@ -118,8 +122,14 @@ export interface ReqDelMeta extends MsgWithTenantLedger { readonly params: ReqSignedUrlParam; } -export function buildReqDelMeta(sthis: NextId, signedUrlParams: ReqSignedUrlParam, gwCtx: GwCtx): ReqDelMeta { +export function buildReqDelMeta( + sthis: NextId, + auth: AuthType, + signedUrlParams: ReqSignedUrlParam, + gwCtx: GwCtx +): ReqDelMeta { return { + auth, tid: sthis.nextId().str, ...gwCtx, type: "reqDelMeta", @@ -137,11 +147,12 @@ export interface ResDelMeta extends MsgWithTenantLedger, ResOptiona } export function buildResDelMeta( - _slogger: SuperThisLogger, + // msgCtx: MsgTypesCtx, req: MsgWithTenantLedger>, signedUrl?: string ): ResDelMeta { return { + auth: req.auth, params: { ...req.params, method: "DELETE", store: "meta" }, signedUrl, tid: req.tid, diff --git a/src/v2-cloud/msg-types-data.ts b/src/v2-cloud/msg-types-data.ts index b8c616a1..c60adcc8 100644 --- a/src/v2-cloud/msg-types-data.ts +++ b/src/v2-cloud/msg-types-data.ts @@ -11,7 +11,7 @@ import { GwCtx, MsgIsTenantLedger, MsgWithConn, - SuperThisLogger, + MsgTypesCtx, } from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; @@ -37,15 +37,15 @@ export function MsgIsResGetData(msg: MsgBase): msg is ResGetData { } export interface CalculatePreSignedUrl { - calculatePreSignedUrl(ctx: SuperThisLogger, p: PreSignedMsg): Promise>; + calculatePreSignedUrl(ctx: MsgTypesCtx, p: PreSignedMsg): Promise>; } export function buildResGetData( - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: MsgWithConn, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResGetData>("GET", "data", "resGetData", slogger, req, ctx); + return buildRes, ResGetData>("GET", "data", "resGetData", msgCtx, req, ctx); } export interface ReqPutData extends ReqSignedUrl { @@ -70,11 +70,11 @@ export function MsgIsResPutData(msg: MsgBase): msg is ResPutData { } export function buildResPutData( - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: MsgWithConn, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResPutData>("PUT", "data", "resPutData", slogger, req, ctx); + return buildRes, ResPutData>("PUT", "data", "resPutData", msgCtx, req, ctx); } export interface ReqDelData extends ReqSignedUrl { @@ -98,9 +98,9 @@ export function MsgIsResDelData(msg: MsgBase): msg is ResDelData { } export function buildResDelData( - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: MsgWithConn, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResDelData>("DELETE", "data", "resDelData", slogger, req, ctx); + return buildRes, ResDelData>("DELETE", "data", "resDelData", msgCtx, req, ctx); } diff --git a/src/v2-cloud/msg-types-wal.ts b/src/v2-cloud/msg-types-wal.ts index 898703c1..1f10f1ef 100644 --- a/src/v2-cloud/msg-types-wal.ts +++ b/src/v2-cloud/msg-types-wal.ts @@ -11,7 +11,7 @@ import { MsgIsTenantLedger, MsgWithTenantLedger, MsgWithConn, - SuperThisLogger, + MsgTypesCtx, } from "./msg-types.js"; import { CalculatePreSignedUrl } from "./msg-types-data.js"; @@ -37,11 +37,11 @@ export function MsgIsResGetWAL(msg: MsgBase): msg is ResGetWAL { } export function buildResGetWAL( - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResGetWAL>("GET", "wal", "resGetWAL", slogger, req, ctx); + return buildRes>, ResGetWAL>("GET", "wal", "resGetWAL", msgCtx, req, ctx); } export interface ReqPutWAL extends Omit { @@ -66,11 +66,11 @@ export function MsgIsResPutWAL(msg: MsgBase): msg is ResPutWAL { } export function buildResPutWAL( - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResPutWAL>("PUT", "wal", "resPutWAL", slogger, req, ctx); + return buildRes>, ResPutWAL>("PUT", "wal", "resPutWAL", msgCtx, req, ctx); } export interface ReqDelWAL extends Omit { @@ -94,7 +94,7 @@ export function MsgIsResDelWAL(msg: MsgBase): msg is ResDelWAL { } export function buildResDelWAL( - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { @@ -102,7 +102,7 @@ export function buildResDelWAL( "DELETE", "wal", "resDelWAL", - slogger, + msgCtx, req, ctx ); diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts index 2b4d3b26..5fe320de 100644 --- a/src/v2-cloud/msg-types.ts +++ b/src/v2-cloud/msg-types.ts @@ -2,6 +2,7 @@ import { Future, Logger } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; import { CalculatePreSignedUrl } from "./msg-types-data.js"; import { PreSignedMsg } from "./pre-signed-url.js"; +import { TokenForParam } from "../sts-service/sts-service.js"; export const VERSION = "FP-MSG-1.0"; @@ -35,15 +36,23 @@ export interface NextId { } export interface AuthType { - readonly type: "ucan"; + readonly type: "ucan" | "error" | "fp-cloud-jwk"; } -export interface UCanAuth { +export interface UCanAuth extends AuthType { readonly type: "ucan"; readonly params: { readonly tbd: string; }; } +export interface FPCloudAuth extends AuthType { + readonly type: "fp-cloud-jwk"; + readonly params: { + readonly jwk: string; + }; +} + +export type AuthFactory = (tp?: Partial) => Promise; export interface TenantLedger { readonly tenant: string; @@ -79,7 +88,7 @@ export interface MsgBase { readonly tid: string; readonly type: string; readonly version: string; - readonly auth?: AuthType; + readonly auth: AuthType; } export function MsgIsTid(msg: MsgBase, tid: string): boolean { @@ -275,20 +284,22 @@ export interface ResChat extends MsgWithConn { readonly targets: QSId[]; } -export function buildReqChat(sthis: NextId, conn: QSId, message: string, targets?: QSId[]): ReqChat { +export function buildReqChat(sthis: NextId, auth: AuthType, conn: QSId, message: string, targets?: QSId[]): ReqChat { return { tid: sthis.nextId().str, type: "reqChat", version: VERSION, + auth, conn, message, targets: targets ?? [], }; } -export function buildResChat(req: ReqChat, conn?: QSId, message?: string, targets?: QSId[]): ResChat { +export function buildResChat(req: ReqChat, conn?: QSId, message?: string, targets?: QSId[], auth?: AuthType): ResChat { return { ...req, + auth: auth || req.auth, conn: conn || req.conn, message: message || req.message, targets: targets || req.targets, @@ -319,9 +330,10 @@ export function MsgIsReqGestalt(msg: MsgBase): msg is ReqGestalt { return msg.type === "reqGestalt"; } -export function buildReqGestalt(sthis: NextId, gestalt: Gestalt, publish?: boolean): ReqGestalt { +export function buildReqGestalt(sthis: NextId, auth: AuthType, gestalt: Gestalt, publish?: boolean): ReqGestalt { return { tid: sthis.nextId().str, + auth, type: "reqGestalt", version: VERSION, gestalt, @@ -341,9 +353,10 @@ export interface ResGestalt extends MsgBase { readonly gestalt: Gestalt; } -export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt): ResGestalt | ErrorMsg { +export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt, auth?: AuthType): ResGestalt | ErrorMsg { return { tid: req.tid, + auth: auth || req.auth, type: "resGestalt", version: VERSION, gestalt, @@ -370,9 +383,10 @@ export interface ReqOpen extends MsgBase { readonly conn: ReqOpenConn; } -export function buildReqOpen(sthis: NextId, conn: ReqOpenConnection): ReqOpen { +export function buildReqOpen(sthis: NextId, auth: AuthType, conn: ReqOpenConnection): ReqOpen { return { tid: sthis.nextId().str, + auth, type: "reqOpen", version: VERSION, conn: { @@ -382,10 +396,10 @@ export function buildReqOpen(sthis: NextId, conn: ReqOpenConnection): ReqOpen { }; } -export function MsgIsReqOpenWithConn(imsg: MsgBase): imsg is MsgWithConn { - const msg = imsg as MsgWithConn; - return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; -} +// export function MsgIsReqOpenWithConn(imsg: MsgBase): imsg is MsgWithConn { +// const msg = imsg as MsgWithConn; +// return msg.type === "reqOpen" && !!msg.conn && !!msg.conn.reqId; +// } export function MsgIsReqOpen(imsg: MsgBase): imsg is MsgWithConn { const msg = imsg as MsgWithConn; @@ -448,9 +462,10 @@ export function buildResClose(req: ReqClose, conn: QSId): ResClose { }; } -export function buildReqClose(sthis: NextId, conn: QSId): ReqClose { +export function buildReqClose(sthis: NextId, auth: AuthType, conn: QSId): ReqClose { return { tid: sthis.nextId().str, + auth, type: "reqClose", version: VERSION, conn, @@ -477,69 +492,18 @@ export interface UpdateReqRes { export type ReqRes = Readonly>; -// export interface ReqOptRes { -// readonly req: Q; -// readonly res?: S; -// } - -// /* Signed URL */ -// export function buildReqSignedUrl(req: ReqSignedUrlParam): ReqSignedUrlParam { -// return { -// tid: req.tid, -// params: { -// // protocol: "wss", -// ...req.params, -// }, -// }; -// } - -// export function MsgIsReqSignedUrl(msg: MsgBase): msg is ReqSignedUrl { -// return msg.type === "reqSignedUrl"; -// } - -// interface StoreAndType { -// readonly store: FPStoreTypes; -// readonly resType: string; -// } -// const reqToRes: Record = { -// reqGetData: { store: "data", resType: "resGetData" }, -// reqPutData: { store: "data", resType: "resPutData" }, -// reqDelData: { store: "data", resType: "resDelData" }, -// reqGetWAL: { store: "wal", resType: "resGetWAL" }, -// reqPutWAL: { store: "wal", resType: "resPutWAL" }, -// reqDelWAL: { store: "wal", resType: "resDelWAL" }, -// }; - -// export function getStoreFromType(req: MsgBase): StoreAndType { -// return ( -// reqToRes[req.type] || -// (() => { -// throw new Error(`unknown req.type=${req.type}`); -// })() -// ); -// } - -// export function buildResSignedUrl(req: ReqSignedUrl, signedUrl: string): ResSignedUrl { -// return { -// tid: req.tid, -// type: getStoreFromType(req).resType, -// version: VERSION, -// params: req.params, -// signedUrl, -// }; -// } - export function buildErrorMsg( - slogger: SuperThisLogger, + msgCtx: { readonly logger: Logger; readonly sthis: SuperThis }, base: Partial, error: Error, body?: string, stack?: string[] ): ErrorMsg { - if (!stack && slogger.sthis.env.get("FP_STACK")) { + if (!stack && msgCtx.sthis.env.get("FP_STACK")) { stack = error.stack?.split("\n"); } const msg = { + auth: base.auth || { type: "error" }, src: base, type: "error", tid: base.tid || "internal", @@ -548,12 +512,10 @@ export function buildErrorMsg( body, stack, } satisfies ErrorMsg; - slogger.logger.Any("ErrorMsg", msg); + msgCtx.logger.Any("ErrorMsg", msg); return msg; } -// export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; - export function MsgIsTenantLedger(msg: T): msg is MsgWithTenantLedger { const t = (msg as MsgWithTenantLedger).tenant; return !!t && !!t.tenant && !!t.ledger; @@ -603,21 +565,50 @@ export interface ResOptionalSignedUrl extends MsgWithTenantLedger { readonly signedUrl?: string; } -export interface SuperThisLogger { +export interface MsgTypesCtx { + readonly sthis: SuperThis; + readonly logger: Logger; + // readonly auth: AuthFactory; +} + +// export async function msgTypesCtxSync(msgCtx: MsgTypesCtx): Promise { +// return { +// sthis: msgCtx.sthis, +// logger: msgCtx.logger, +// auth: await msgCtx.auth(), +// }; +// } + +export function authType(jwk: string): AuthType { + return { + type: "fp-cloud-jwk", + params: { + jwk, + }, + } as FPCloudAuth; +} + +export interface MsgTypesCtxSync { readonly sthis: SuperThis; readonly logger: Logger; + readonly auth: AuthType; +} + +export function resAuth(msg: MsgBase): Promise { + return msg.auth ? Promise.resolve(msg.auth) : Promise.reject(new Error("No Auth")); } export async function buildRes>, S extends ResSignedUrl>( method: SignedUrlParam["method"], store: FPStoreTypes, type: string, - slogger: SuperThisLogger, + msgCtx: MsgTypesCtx, req: Q, ctx: CalculatePreSignedUrl ): Promise> { const psm = { type: "reqSignedUrl", + auth: await resAuth(req), version: req.version, params: { ...req.params, @@ -628,9 +619,9 @@ export async function buildRes>): Promis export class MsgConnected implements MsgRawConnection { static async connect( + authFactory: AuthFactory, mrc: Result | MsgRawConnection, conn: Partial = {} ): Promise> { @@ -129,11 +131,11 @@ export class MsgConnected implements MsgRawConnection { } mrc = mrc.Ok(); } - const res = await mrc.request(buildReqOpen(mrc.sthis, conn), { waitFor: MsgIsResOpen }); + const res = await mrc.request(buildReqOpen(mrc.sthis, await authFactory(), conn), { waitFor: MsgIsResOpen }); if (MsgIsError(res) || !MsgIsResOpen(res)) { return mrc.sthis.logger.Error().Err(res).Msg("unexpected response").ResultError(); } - return Result.Ok(new MsgConnected(mrc, res.conn)); + return Result.Ok(new MsgConnected(mrc, authFactory, res.conn)); } readonly sthis: SuperThis; @@ -142,11 +144,13 @@ export class MsgConnected implements MsgRawConnection { readonly exchangedGestalt: ExchangedGestalt; readonly activeBinds: Map>; readonly id: string; - private constructor(raw: MsgRawConnection, conn: QSId) { + readonly authFactory: AuthFactory; + private constructor(raw: MsgRawConnection, auth: AuthFactory, conn: QSId) { this.sthis = raw.sthis; this.raw = raw; this.exchangedGestalt = raw.exchangedGestalt; this.conn = conn; + this.authFactory = auth; this.activeBinds = raw.activeBinds; this.id = this.sthis.nextId().str; } @@ -162,7 +166,7 @@ export class MsgConnected implements MsgRawConnection { return; } if (MsgIsConnected(chunk, this.conn)) { - if ((opts.waitFor && opts.waitFor(chunk)) || MsgIsError(chunk)) { + if (opts.waitFor?.(chunk) || MsgIsError(chunk)) { controller.enqueue(chunk); } } @@ -187,7 +191,7 @@ export class MsgConnected implements MsgRawConnection { return this.raw.start(); } async close(): Promise> { - await this.request(buildReqClose(this.sthis, this.conn), { waitFor: MsgIsResClose }); + await this.request(buildReqClose(this.sthis, await this.authFactory(), this.conn), { waitFor: MsgIsResClose }); return await this.raw.close(); // return Result.Ok(undefined); } @@ -204,15 +208,17 @@ export class MsgConnected implements MsgRawConnection { export class Msger { static async openHttp( sthis: SuperThis, + auth: AuthFactory, // reqOpen: ReqOpen | undefined, urls: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt ): Promise> { - return Result.Ok(new HttpConnection(sthis, urls, msgP, exGestalt)); + return Result.Ok(new HttpConnection(sthis, auth, urls, msgP, exGestalt)); } static async openWS( sthis: SuperThis, + authFactory: AuthFactory, // qOpen: ReqOpen, url: URI, msgP: MsgerParamsWithEnDe, @@ -228,10 +234,11 @@ export class Msger { } else { ws = new WebSocket(url.toString()); } - return Result.Ok(new WSConnection(sthis, ws, msgP, exGestalt)); + return Result.Ok(new WSConnection(sthis, authFactory, ws, msgP, exGestalt)); } static async open( sthis: SuperThis, + auth: AuthFactory, curl: CoerceURI, imsgP: Partial = {} ): Promise> { @@ -242,12 +249,12 @@ export class Msger { /* * request Gestalt with Http */ - const rHC = await Msger.openHttp(sthis, [url], jsMsgP, { my: gs, remote: gs }); + const rHC = await Msger.openHttp(sthis, auth, [url], jsMsgP, { my: gs, remote: gs }); if (rHC.isErr()) { return rHC; } const hc = rHC.Ok(); - const resGestalt = await hc.request(buildReqGestalt(sthis, gs), { + const resGestalt = await hc.request(buildReqGestalt(sthis, await auth(), gs), { waitFor: MsgIsResGestalt, }); if (!MsgIsResGestalt(resGestalt)) { @@ -260,6 +267,7 @@ export class Msger { return applyStart( Msger.openHttp( sthis, + auth, exGt.remote.httpEndpoints.map((i) => BuildURI.from(url).resolve(i).URI()), msgP, exGt @@ -267,17 +275,18 @@ export class Msger { ); } return applyStart( - Msger.openWS(sthis, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) + Msger.openWS(sthis, auth, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) ); } static connect( sthis: SuperThis, + auth: AuthFactory, curl: CoerceURI, imsgP: Partial = {}, conn: Partial = {} ): Promise> { - return Msger.open(sthis, curl, imsgP).then((srv) => MsgConnected.connect(srv, conn)); + return Msger.open(sthis, auth, curl, imsgP).then((srv) => MsgConnected.connect(auth, srv, conn)); } private constructor() { diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 5c0a6943..dc37bfbb 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -10,6 +10,7 @@ import { MsgIsResGestalt, MsgIsError, MsgBase, + AuthFactory, } from "./msg-types.js"; import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection } from "./msger.js"; import { WSConnection } from "./ws-connection.js"; @@ -19,8 +20,15 @@ import { HonoServer } from "./hono-server.js"; import { NodeHonoFactory } from "./node-hono-server.js"; import { CFHonoFactory } from "./backend/cf-hono-server.js"; import { BetterSQLDatabase } from "./meta-merger/bettersql-abstract-sql.js"; +import { envKeyDefaults, SessionTokenService, TokenForParam } from "../sts-service/sts-service.js"; -export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { +export function httpStyle( + sthis: SuperThis, + authFactory: AuthFactory, + port: number, + msgP: MsgerParamsWithEnDe, + my: Gestalt +) { const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["reqRes"] }), { id: "HTTP-server", }); @@ -28,6 +36,7 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE return { name: "HTTP", remoteGestalt: remote, + authFactory, cInstance: HttpConnection, ok: { url: () => URI.from(`http://127.0.0.1:${port}/fp`), @@ -35,6 +44,7 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE applyStart( Msger.openHttp( sthis, + authFactory, [URI.from(`http://localhost:${port}/fp`)], { ...msgP, @@ -50,6 +60,7 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE open: async (): Promise>> => { const ret = await Msger.openHttp( sthis, + authFactory, [URI.from(`http://localhost:${port - 1}/fp`)], { ...msgP, @@ -62,7 +73,9 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE return ret; } // should fail - const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); + const res = await ret + .Ok() + .request(buildReqGestalt(sthis, await authFactory(), my), { waitFor: MsgIsResGestalt }); if (MsgIsError(res)) { return Result.Err(res.message); } @@ -74,6 +87,7 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE open: async (): Promise>> => { const ret = await Msger.openHttp( sthis, + authFactory, [URI.from(`http://4.7.1.1:${port}/fp`)], { ...msgP, @@ -83,7 +97,9 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE exGt ); // should fail - const res = await ret.Ok().request(buildReqGestalt(sthis, my), { waitFor: MsgIsResGestalt }); + const res = await ret + .Ok() + .request(buildReqGestalt(sthis, await authFactory(), my), { waitFor: MsgIsResGestalt }); if (MsgIsError(res)) { return Result.Err(res.message); } @@ -93,7 +109,13 @@ export function httpStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithE }; } -export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt) { +export function wsStyle( + sthis: SuperThis, + authFactory: AuthFactory, + port: number, + msgP: MsgerParamsWithEnDe, + my: Gestalt +) { const remote = defaultGestalt(defaultMsgParams(sthis, { hasPersistent: true, protocolCapabilities: ["stream"] }), { id: "WS-server", }); @@ -101,6 +123,7 @@ export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnD return { name: "WS", remoteGestalt: remote, + authFactory, cInstance: WSConnection, ok: { url: () => URI.from(`http://127.0.0.1:${port}/ws`), @@ -108,6 +131,7 @@ export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnD applyStart( Msger.openWS( sthis, + authFactory, URI.from(`http://localhost:${port}/ws`), { ...msgP, @@ -123,6 +147,7 @@ export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnD open: () => Msger.openWS( sthis, + authFactory, URI.from(`http://localhost:${port - 1}/ws`), { ...msgP, @@ -137,6 +162,7 @@ export function wsStyle(sthis: SuperThis, port: number, msgP: MsgerParamsWithEnD open: () => Msger.openWS( sthis, + authFactory, URI.from(`http://4.7.1.1:${port - 1}/ws`), { ...msgP, @@ -164,8 +190,9 @@ export async function resolveToml(backend: "D1" | "DO") { export function NodeHonoServerFactory() { return { name: "NodeHonoServer", - factory: async (sthis: SuperThis, msgP: MsgerParams, remoteGestalt: Gestalt, _port: number) => { + factory: async (sthis: SuperThis, msgP: MsgerParams, remoteGestalt: Gestalt, _port: number, pubEnvJWK: string) => { const { env } = await resolveToml("D1"); + sthis.env.set(envKeyDefaults.PUBLIC, pubEnvJWK); sthis.env.sets(env as unknown as Record); const nhf = new NodeHonoFactory(sthis, { msgP, @@ -177,17 +204,27 @@ export function NodeHonoServerFactory() { }; } +async function writeEnvFile(sthis: SuperThis, tomlFile: string, env: string, envJWK: string) { + fs.writeFile( + sthis.pathOps.join(sthis.pathOps.dirname(tomlFile), `dev.vars.${env}`), + `${envKeyDefaults.PUBLIC}=${envJWK}\n` + ); +} + export function CFHonoServerFactory(backend: "D1" | "DO") { return { name: `CFHonoServer(${backend})`, - factory: async (_sthis: SuperThis, _msgP: MsgerParams, remoteGestalt: Gestalt, port: number) => { + factory: async (sthis: SuperThis, _msgP: MsgerParams, remoteGestalt: Gestalt, port: number, pubEnvJWK: string) => { if (process.env.FP_WRANGLER_PORT) { return new HonoServer(new CFHonoFactory()); } const { tomlFile } = await resolveToml(backend); $.verbose = !!process.env.FP_DEBUG; + const envName = `test-${remoteGestalt.protocolCapabilities[0]}-${backend}`; + await writeEnvFile(sthis, tomlFile, envName, pubEnvJWK); + // .dev.vars. const runningWrangler = $` - wrangler dev -c ${tomlFile} --port ${port} --env test-${remoteGestalt.protocolCapabilities[0]}-${backend} --no-show-interactive-dev-session --no-live-reload & + wrangler dev -c ${tomlFile} --port ${port} --env ${envName} --no-show-interactive-dev-session --no-live-reload & waitPid=$! echo "PID:$waitPid" wait $waitPid`; @@ -217,3 +254,28 @@ export function CFHonoServerFactory(backend: "D1" | "DO") { }, }; } + +export async function mockGetAuthFactory(pk: string, factoryTp: TokenForParam, sthis: SuperThis): Promise { + const sts = await SessionTokenService.create( + { + token: pk, + }, + sthis + ); + + return async (tp: Partial = {}) => { + const token = await sts.tokenFor({ + ...factoryTp, + ...tp, + userId: tp.userId || factoryTp.userId, + tenants: tp.tenants || factoryTp.tenants, + ledgers: tp.ledgers || factoryTp.ledgers, + }); + return { + type: "fp-cloud-jwk", + params: { + jwk: token, + }, + }; + }; +} diff --git a/src/v2-cloud/ws-connection.ts b/src/v2-cloud/ws-connection.ts index b8eb2ca6..b05cdc06 100644 --- a/src/v2-cloud/ws-connection.ts +++ b/src/v2-cloud/ws-connection.ts @@ -9,6 +9,7 @@ import { MsgWithError, RequestOpts, MsgIsTid, + AuthFactory, } from "./msg-types.js"; import { ActiveStream, ExchangedGestalt, MsgerParamsWithEnDe, MsgRawConnection, OnMsgFn, UnReg } from "./msger.js"; import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; @@ -32,13 +33,21 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti opened = false; readonly id: string; - - constructor(sthis: SuperThis, ws: WebSocket, msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { + readonly auth: AuthFactory; + + constructor( + sthis: SuperThis, + auth: AuthFactory, + ws: WebSocket, + msgP: MsgerParamsWithEnDe, + exGestalt: ExchangedGestalt + ) { super(sthis, exGestalt); this.id = sthis.nextId().str; this.logger = ensureLogger(sthis, "WSConnection"); this.msgP = msgP; this.ws = ws; + this.auth = auth; // this.wqs = { ...wsq }; } diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts index 941fe6db..e6b2cc40 100644 --- a/src/v2-cloud/ws-sockets.test.ts +++ b/src/v2-cloud/ws-sockets.test.ts @@ -1,11 +1,12 @@ import { ensureSuperThis } from "@fireproof/core"; -import { CFHonoServerFactory, NodeHonoServerFactory, wsStyle } from "./test-helper.js"; +import { CFHonoServerFactory, mockGetAuthFactory, NodeHonoServerFactory, wsStyle } from "./test-helper.js"; import { defaultMsgParams, Msger } from "./msger.js"; -import { buildReqChat, defaultGestalt, MsgIsResChat } from "./msg-types.js"; +import { AuthFactory, buildReqChat, defaultGestalt, MsgIsResChat } from "./msg-types.js"; import { Hono } from "hono"; import { HonoServer } from "./hono-server.js"; import { Future } from "@adviser/cement"; +import { SessionTokenService } from "../sts-service/sts-service.js"; describe("test multiple connections", () => { const sthis = ensureSuperThis(); @@ -19,14 +20,30 @@ describe("test multiple connections", () => { const msgP = defaultMsgParams(sthis, { hasPersistent: true }); const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - const stype = wsStyle(sthis, port, msgP, my); + let stype; const connections = 3; let hserv: HonoServer; + let authFactory: AuthFactory; + beforeAll(async () => { + const pair = await SessionTokenService.generateKeyPair(); + authFactory = await mockGetAuthFactory( + pair.strings.privateKey, + { + userId: "hello", + tenants: [], + ledgers: [], + }, + sthis + ); + stype = wsStyle(sthis, authFactory, port, msgP, my); + const app = new Hono(); - hserv = await factory(sthis, msgP, stype.remoteGestalt, port).then((srv) => srv.once(app, port)); + hserv = await factory(sthis, msgP, stype.remoteGestalt, port, pair.strings.publicKey).then((srv) => + srv.once(app, port) + ); }); afterAll(async () => { await hserv.close(); @@ -37,7 +54,7 @@ describe("test multiple connections", () => { Array(connections) .fill(0) .map(() => { - return Msger.connect(sthis, "http://localhost:" + port + "/fp"); + return Msger.connect(sthis, authFactory, "http://localhost:" + port + "/fp"); }) ).then((cs) => cs.map((c) => c.Ok())); @@ -63,7 +80,9 @@ describe("test multiple connections", () => { const rest = [...conns]; for (const c of conns) { // console.log("Sending a chat request", rest.length, conns.length); - const act = await c.request(buildReqChat(sthis, c.conn, "Hello"), { waitFor: MsgIsResChat }); + const act = await c.request(buildReqChat(sthis, await authFactory(), c.conn, "Hello"), { + waitFor: MsgIsResChat, + }); if (MsgIsResChat(act)) { expect(act.targets.length).toBe(rest.length); } else { From 0070d7dbc2f7c8913440c7e7cf961d32d1f2c092 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 10 Mar 2025 11:51:46 +0100 Subject: [PATCH 08/14] WIP --- src/sts-service/create-key-pair.ts | 63 ++++++++++++++ src/sts-service/sts-service.ts | 131 +++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/sts-service/create-key-pair.ts create mode 100644 src/sts-service/sts-service.ts diff --git a/src/sts-service/create-key-pair.ts b/src/sts-service/create-key-pair.ts new file mode 100644 index 00000000..be9ca1dc --- /dev/null +++ b/src/sts-service/create-key-pair.ts @@ -0,0 +1,63 @@ +import { env2jwk, envKeyDefaults, SessionTokenService } from "./sts-service.js"; + +const { strings, material } = await SessionTokenService.generateKeyPair(); + +// console.log(">", await exportJWK(privateKey)) + +// eslint-disable-next-line no-console +console.log(`${envKeyDefaults.PUBLIC}=${strings.publicKey}`); +// eslint-disable-next-line no-console +console.log(`${envKeyDefaults.SECRET}=${strings.privateKey}`); + +// const txtEncoder = new TextEncoder() +// const inPubKey = await exportJWK(publicKey) +// const publicTxt =base64.encode(txtEncoder.encode(JSON.stringify(inPubKey))) +// console.log("Public:", publicTxt) +// const inPrivKey = await exportJWK(privateKey) +// const privateTxt =base64.encode(txtEncoder.encode(JSON.stringify(inPrivKey))) +// console.log("Private:", privateTxt) + +// const publicJWT = JSON.parse(txtDecoder.decode(base64.decode(publicTxt))) +// console.log("Public=", await exportJWK(await importJWK(publicJWT, "Ed25519")), inPubKey) + +// const privateJWT = JSON.parse(txtDecoder.decode(base64.decode(privateTxt))) +// console.log("Private=", await exportJWK(await importJWK(privateJWT, "Ed25519", { extractable: true})), inPrivKey) + +//const key = await env2jwk(strings.privateKey, "ES256"); +// +//const txtDecoder = new TextDecoder(); +//const keyData = JSON.parse(txtDecoder.decode(base58btc.decode(strings.privateKey))); +//console.log(">>>>>>keydata:", keyData); +// +////const keyData: types.JWK = { ...jwk } +//// delete keyData.alg +//// delete keyData.use +// +//// const sub = await crypto.subtle.importKey( +// "jwk", +// keyData, +// { +// name: "ECDSA", +// namedCurve: "P-256", +// }, +// true, +// ["sign"] +//); +// +//// console.log(">>>>>>", sub[Symbol.toStringTag], sub.constructor.name); +// +//// console.log(">>>>>>", key[Symbol.toStringTag]); +// +//const token = await new SignJWT({ +// userId: "userId", +// tenants: ["tenant"], +// ledgers: ["ledger"], +//}) +// .setProtectedHeader({ alg: "ES256" }) // algorithm +// .setIssuedAt() +// .setIssuer("issuer") // issuer +// .setAudience("audience") // audience +// .setExpirationTime(Date.now() + 3600) // expiration time +// .sign(key); +// +//console.log(`TOKEN=${token}`, material.privateKey); diff --git a/src/sts-service/sts-service.ts b/src/sts-service/sts-service.ts new file mode 100644 index 00000000..f15c077d --- /dev/null +++ b/src/sts-service/sts-service.ts @@ -0,0 +1,131 @@ +import { Result, exception2Result } from "@adviser/cement"; +import { ensureSuperThis, SuperThis } from "@fireproof/core"; +import { exportJWK, importJWK, JWTPayload, JWTVerifyResult, jwtVerify, SignJWT } from "jose"; +import { generateKeyPair, GenerateKeyPairOptions } from "jose/key/generate/keypair"; +import { base58btc } from "multiformats/bases/base58"; + +interface BaseTokenParam { + readonly alg: string; // defaults ES256 + readonly issuer: string; + readonly audience: string; + readonly validFor: number; +} +interface SessionTokenServiceParam extends Partial { + readonly token: string; // env encoded jwk +} + +interface SessionTokenServiceFromEnvParam extends Partial { + readonly privateEnvKey?: string; // defaults CLOUD_SESSION_TOKEN_SECRET + readonly publicEnvKey?: string; // defaults CLOUD_SESSION_TOKEN_PUBLIC +} + +export async function jwk2env(jwk: CryptoKey, sthis = ensureSuperThis()): Promise { + const inPubKey = await exportJWK(jwk); + return base58btc.encode(sthis.txt.encode(JSON.stringify(inPubKey))); +} + +export async function env2jwk(env: string, alg: string, sthis = ensureSuperThis()): Promise { + const inJWT = JSON.parse(sthis.txt.decode(base58btc.decode(env))); + return importJWK(inJWT, alg, { extractable: true }) as Promise; +} + +export interface FPCloudClaim extends JWTPayload { + readonly userId: string; + readonly tenants: { readonly id: string; readonly role: string }[]; + readonly ledgers: { readonly id: string; readonly role: string; readonly right: string }[]; +} + +export type TokenForParam = FPCloudClaim & Partial; + +export const envKeyDefaults = { + SECRET: "CLOUD_SESSION_TOKEN_SECRET", + PUBLIC: "CLOUD_SESSION_TOKEN_PUBLIC", +}; + +export class SessionTokenService { + readonly #key: CryptoKey; + readonly #param: SessionTokenServiceParam; + + static async generateKeyPair( + alg = "ES256", + options: GenerateKeyPairOptions = { extractable: true } + ): Promise<{ material: CryptoKeyPair; strings: { publicKey: string; privateKey: string } }> { + const material = await generateKeyPair(alg, options); + return { + material, + strings: { + publicKey: await jwk2env(material.publicKey), + privateKey: await jwk2env(material.privateKey), + }, + }; + } + + static async createFromEnv(sp: SessionTokenServiceFromEnvParam, sthis: SuperThis = ensureSuperThis()) { + let envToken = sthis.env.get(sp.privateEnvKey ?? envKeyDefaults.SECRET); + if (!envToken) { + envToken = sthis.env.get(sp.publicEnvKey ?? envKeyDefaults.PUBLIC); + } + if (!envToken) { + throw new Error( + `env not found for: ${sp.privateEnvKey ?? envKeyDefaults.SECRET} or ${sp.publicEnvKey ?? envKeyDefaults.PUBLIC}` + ); + } + return SessionTokenService.create({ token: envToken }, sthis); + } + + static async create(stsparam: SessionTokenServiceParam, sthis: SuperThis = ensureSuperThis()) { + const key = await env2jwk(stsparam.token, stsparam.alg ?? "ES256", sthis); + return new SessionTokenService(key, stsparam); + } + + private constructor(key: CryptoKey, stsparam: SessionTokenServiceParam) { + this.#key = key; + this.#param = stsparam; + } + + get validFor() { + let validFor = this.#param.validFor ?? 3600; + if (!(0 <= validFor && validFor <= 3600000)) { + validFor = 3600000; + } + return validFor; + } + + get alg() { + return this.#param.alg ?? "ES256"; + } + + get isssuer() { + return this.#param.issuer ?? "fireproof"; + } + + get audience() { + return this.#param.audience ?? "fireproof"; + } + + async validate(token: string): Promise>> { + return exception2Result(() => jwtVerify(token, this.#key)); + } + + // async getEnvKey(): Promise { + // return jwk2env(ensureSuperThis(), this.#key); + // } + + async tokenFor(p: TokenForParam): Promise { + if (this.#key.type !== "private") { + throw new Error("key must be private"); + } + const token = await new SignJWT({ + userId: p.userId, + tenants: p.tenants, + ledgers: p.ledgers, + } satisfies FPCloudClaim) + .setProtectedHeader({ alg: this.alg }) // algorithm + .setIssuedAt() + .setIssuer(p.issuer ?? this.isssuer) // issuer + .setAudience(p.audience ?? this.audience) // audience + .setExpirationTime(Date.now() + (p.validFor ?? this.validFor)) // expiration time + .sign(this.#key); + return token; + } +} From efa4800ae4ffb32eaf8f2de3709049765b45d006 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 11 Mar 2025 08:13:34 +0100 Subject: [PATCH 09/14] WIP --- src/sts-service/sts-service.ts | 9 +- src/v2-cloud/backend/cf-hono-server.ts | 4 +- src/v2-cloud/client/gateway.ts | 188 ++++++++++++++++--------- src/v2-cloud/connection.test.ts | 24 ++-- src/v2-cloud/hono-server.ts | 8 +- src/v2-cloud/http-connection.ts | 5 +- src/v2-cloud/msg-dispatch.ts | 4 +- src/v2-cloud/msg-dispatcher-impl.ts | 24 ++-- src/v2-cloud/msg-type-meta.ts | 22 +-- src/v2-cloud/msg-types-data.ts | 14 +- src/v2-cloud/msg-types-wal.ts | 14 +- src/v2-cloud/msg-types.ts | 30 ++-- src/v2-cloud/msger.ts | 123 ++++++++++++---- src/v2-cloud/node-hono-server.ts | 4 +- src/v2-cloud/pre-signed-url.ts | 4 +- src/v2-cloud/test-helper.ts | 101 +++++++------ src/v2-cloud/ws-connection.ts | 4 - src/v2-cloud/ws-room.ts | 4 +- 18 files changed, 354 insertions(+), 232 deletions(-) diff --git a/src/sts-service/sts-service.ts b/src/sts-service/sts-service.ts index f15c077d..42d0ca90 100644 --- a/src/sts-service/sts-service.ts +++ b/src/sts-service/sts-service.ts @@ -42,6 +42,11 @@ export const envKeyDefaults = { PUBLIC: "CLOUD_SESSION_TOKEN_PUBLIC", }; +export interface KeysResult { + readonly material: CryptoKeyPair; + readonly strings: { readonly publicKey: string; readonly privateKey: string }; +} + export class SessionTokenService { readonly #key: CryptoKey; readonly #param: SessionTokenServiceParam; @@ -49,7 +54,7 @@ export class SessionTokenService { static async generateKeyPair( alg = "ES256", options: GenerateKeyPairOptions = { extractable: true } - ): Promise<{ material: CryptoKeyPair; strings: { publicKey: string; privateKey: string } }> { + ): Promise { const material = await generateKeyPair(alg, options); return { material, @@ -60,7 +65,7 @@ export class SessionTokenService { }; } - static async createFromEnv(sp: SessionTokenServiceFromEnvParam, sthis: SuperThis = ensureSuperThis()) { + static async createFromEnv(sp: SessionTokenServiceFromEnvParam = {}, sthis: SuperThis = ensureSuperThis()) { let envToken = sthis.env.get(sp.privateEnvKey ?? envKeyDefaults.SECRET); if (!envToken) { envToken = sthis.env.get(sp.publicEnvKey ?? envKeyDefaults.PUBLIC); diff --git a/src/v2-cloud/backend/cf-hono-server.ts b/src/v2-cloud/backend/cf-hono-server.ts index 3f1998e4..3db65f0c 100644 --- a/src/v2-cloud/backend/cf-hono-server.ts +++ b/src/v2-cloud/backend/cf-hono-server.ts @@ -17,7 +17,7 @@ import { Gestalt, MsgBase, MsgIsWithConn, - MsgWithConn, + MsgWithConnAuth, QSId, qsidEqual, } from "../msg-types.js"; @@ -185,7 +185,7 @@ class CFWSRoom implements WSRoom { // console.log("addConn", this.id, conn); return conn; } - isConnected(msg: MsgBase): msg is MsgWithConn { + isConnected(msg: MsgBase): msg is MsgWithConnAuth { if (!MsgIsWithConn(msg)) { return false; } diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index e7525b54..69918fa5 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -1,5 +1,14 @@ // import PartySocket, { PartySocketOptions } from "partysocket"; -import { Result, URI, KeyedResolvOnce, exception2Result, Logger, param } from "@adviser/cement"; +import { + Result, + URI, + KeyedResolvOnce, + exception2Result, + Logger, + param, + MatchResult, + ResolveOnce, +} from "@adviser/cement"; import { bs, ensureLogger, NotFoundError, SuperThis } from "@fireproof/core"; import { buildErrorMsg, @@ -11,10 +20,9 @@ import { ReqSignedUrl, MsgWithError, ResSignedUrl, - authType, } from "../msg-types.js"; import { to_uint8 } from "../../coerce-binary.js"; -import { MsgConnected, Msger } from "../msger.js"; +import { MsgConnected, MsgConnectedAuth, Msger, authTypeFromUri } from "../msger.js"; import { MsgIsResDelData, MsgIsResGetData, @@ -27,9 +35,9 @@ import { const VERSION = "v0.1-fp-cloud"; export interface StoreTypeGateway { - get(uri: URI, conn: Promise>): Promise>; - put(uri: URI, body: Uint8Array, conn: Promise>): Promise>; - delete(uri: URI, conn: Promise>): Promise>; + get(uri: URI, conn: Promise>): Promise>; + put(uri: URI, body: Uint8Array, conn: Promise>): Promise>; + delete(uri: URI, conn: Promise>): Promise>; } abstract class BaseGateway { @@ -40,8 +48,8 @@ abstract class BaseGateway { this.logger = ensureLogger(sthis, module); } - abstract getConn(uri: URI, conn: MsgConnected): Promise>; - async get(uri: URI, prConn: Promise>): Promise> { + abstract getConn(uri: URI, conn: MsgConnectedAuth): Promise>; + async get(uri: URI, prConn: Promise>): Promise> { const rConn = await prConn; if (rConn.isErr()) { return this.logger.Error().Err(rConn).Msg("Error in getConn").ResultError(); @@ -51,8 +59,8 @@ abstract class BaseGateway { return this.getConn(uri, conn); } - abstract putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise>; - async put(uri: URI, body: Uint8Array, prConn: Promise>): Promise> { + abstract putConn(uri: URI, body: Uint8Array, conn: MsgConnectedAuth): Promise>; + async put(uri: URI, body: Uint8Array, prConn: Promise>): Promise> { const rConn = await prConn; if (rConn.isErr()) { return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); @@ -62,8 +70,8 @@ abstract class BaseGateway { return this.putConn(uri, body, conn); } - abstract delConn(uri: URI, conn: MsgConnected): Promise>; - async delete(uri: URI, prConn: Promise>): Promise> { + abstract delConn(uri: URI, conn: MsgConnectedAuth): Promise>; + async delete(uri: URI, prConn: Promise>): Promise> { const rConn = await prConn; if (rConn.isErr()) { return this.logger.Error().Err(rConn).Msg("Error in deleteConn").ResultError(); @@ -91,7 +99,7 @@ abstract class BaseGateway { store: FPStoreTypes, waitForFn: (msg: MsgBase) => boolean, uri: URI, - conn: MsgConnected + conn: MsgConnectedAuth ): Promise> { const rParams = uri.getParamsResult({ key: param.REQUIRED, @@ -108,9 +116,13 @@ abstract class BaseGateway { if (store !== params.store) { return buildErrorMsg(this, {} as MsgBase, new Error("store mismatch")); } + const rAuth = await authTypeFromUri(this.logger, uri); + if (rAuth.isErr()) { + return buildErrorMsg(this, {} as MsgBase, rAuth.Err()); + } const rsu = { tid: this.sthis.nextId().str, - auth: await conn.authFactory(), + auth: rAuth.Ok(), type, // conn: conn.conn, tenant: { @@ -186,7 +198,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { constructor(sthis: SuperThis) { super(sthis, "DataGateway"); } - async getConn(uri: URI, conn: MsgConnected): Promise> { + async getConn(uri: URI, conn: MsgConnectedAuth): Promise> { // type: string, method: HttpMethods, store: FPStoreTypes, waitForFn: const rResSignedUrl = await this.getReqSignedUrl( "reqGetData", @@ -202,7 +214,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { const { signedUrl: downloadUrl } = rResSignedUrl; return this.getObject(uri, downloadUrl); } - async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { + async putConn(uri: URI, body: Uint8Array, conn: MsgConnectedAuth): Promise> { const rResSignedUrl = await this.getReqSignedUrl( "reqPutData", "PUT", @@ -217,7 +229,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { const { signedUrl: uploadUrl } = rResSignedUrl; return this.putObject(uri, uploadUrl, body); } - async delConn(uri: URI, conn: MsgConnected): Promise> { + async delConn(uri: URI, conn: MsgConnectedAuth): Promise> { const rResSignedUrl = await this.getReqSignedUrl( "reqDelData", "DELETE", @@ -240,7 +252,7 @@ class MetaGateway extends BaseGateway implements StoreTypeGateway { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getConn(uri: URI, conn: MsgConnected): Promise> { + async getConn(uri: URI, conn: MsgConnectedAuth): Promise> { // const rkey = uri.getParamResult("key"); // if (rkey.isErr()) { // return Result.Err(rkey.Err()); @@ -268,7 +280,7 @@ class MetaGateway extends BaseGateway implements StoreTypeGateway { return Result.Ok(new Uint8Array()); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async putConn(uri: URI, body: Uint8Array, conn: MsgConnected): Promise> { + async putConn(uri: URI, body: Uint8Array, conn: MsgConnectedAuth): Promise> { // const bodyRes = Result.Ok(body); // await bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body); // if (bodyRes.isErr()) { // return this.logger.Error().Err(bodyRes).Msg("Error in addCryptoKeyToGatewayMetaPayload").ResultError(); @@ -291,7 +303,7 @@ class MetaGateway extends BaseGateway implements StoreTypeGateway { return Result.Ok(undefined); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async delConn(uri: URI, conn: MsgConnected): Promise> { + async delConn(uri: URI, conn: MsgConnectedAuth): Promise> { // const rsu = this.prepareReqSignedUrl(uri, "DELETE", conn.key); // if (rsu.isErr()) { // return Result.Err(rsu.Err()); @@ -376,6 +388,12 @@ function getStoreTypeGateway(sthis: SuperThis, uri: URI): StoreTypeGateway { } } +interface ConnectionItem { + readonly uri: URI; + readonly matchRes: MatchResult; + readonly connection: ResolveOnce>; + readonly trackPuts: Set; +} // const keyedConnections = new KeyedResolvOnce(); interface Subscription { readonly sid: string; @@ -383,12 +401,17 @@ interface Subscription { readonly callback: (msg: Uint8Array) => void; readonly unsub: () => void; } +function connectionURI(uri: URI): URI { + return uri.build().delParam("authJWK").URI(); +} + + const subscriptions = new Map(); // const doServerSubscribe = new KeyedResolvOnce(); -const trackPuts = new Set(); export class FireproofCloudGateway implements bs.Gateway { readonly logger: Logger; readonly sthis: SuperThis; + readonly #connectionURIs = new Map< string, ConnectionItem >(); constructor(sthis: SuperThis) { this.sthis = sthis; @@ -409,26 +432,36 @@ export class FireproofCloudGateway implements bs.Gateway { return this.logger.Error().Err(rName).Msg("name not found").ResultError(); } ret.defParam("protocol", "wss"); - return Result.Ok(ret.URI()); + const retURI = ret.URI(); + const matchURI = connectionURI(retURI); + this.#connectionURIs.set(matchURI.toString(), { + uri: matchURI, + matchRes: matchURI.match(uri), + connection: new ResolveOnce>(), + trackPuts: new Set(), + }); + return Result.Ok(retURI); } - async get(uri: URI): Promise { - return getStoreTypeGateway(this.sthis, uri).get(uri, this.getCloudConnection(uri)); + async get(uri: URI, sthis: SuperThis): Promise { + return getStoreTypeGateway(sthis, uri).get(uri, this.getCloudConnection(uri)); } - async put(uri: URI, body: Uint8Array): Promise> { - const ret = await getStoreTypeGateway(this.sthis, uri).put(uri, body, this.getCloudConnection(uri)); + async put(uri: URI, body: Uint8Array, sthis: SuperThis): Promise> { + const item = await this.getCloudConnectionItem(uri); + const ret = await getStoreTypeGateway(sthis, uri).put(uri, body, Promise.resolve(item.conn)); if (ret.isOk()) { if (uri.getParam("testMode")) { - trackPuts.add(uri.toString()); + item.citem.trackPuts.add(uri.toString()); } } return ret; } - async delete(uri: URI): Promise { - trackPuts.delete(uri.toString()); - return getStoreTypeGateway(this.sthis, uri).delete(uri, this.getCloudConnection(uri)); + async delete(uri: URI, sthis: SuperThis): Promise { + const item = await this.getCloudConnectionItem(uri); + item.citem.trackPuts.delete(uri.toString()); + return getStoreTypeGateway(sthis, uri).delete(uri, this.getCloudConnection(uri)); } async close(uri: URI): Promise { @@ -446,51 +479,69 @@ export class FireproofCloudGateway implements bs.Gateway { return this.logger.Error().Err(rConn).Msg("Error in getCloudConnection").ResultError(); } const conn = rConn.Ok(); - await conn.close(); + const rAuth = await conn.msgConnAuth(); + await conn.close(rAuth.Ok()); + this.#connectionURIs.delete(connectionURI(uri).toString()); return Result.Ok(undefined); } // fireproof://localhost:1999/?name=test-public-api&protocol=ws&store=meta - async getCloudConnection(uri: URI): Promise> { - const rParams = uri.getParamsResult({ - name: param.REQUIRED, - protocol: "https", - store: param.REQUIRED, - storekey: param.OPTIONAL, - tenant: param.REQUIRED, + async getCloudConnection(uri: URI): Promise> { + return this.getCloudConnectionItem(uri).then((r) => { + return r.conn; }); - if (rParams.isErr()) { - return this.logger.Error().Url(uri).Err(rParams).Msg("getCloudConnection:err").ResultError(); - } - const params = rParams.Ok(); - // let tenant: string; - // if (params.tenant) { - // tenant = params.tenant; - // } else { - // if (!params.storekey) { - // return this.logger.Error().Url(uri).Msg("no tendant or storekey given").ResultError(); - // } - // const dataKey = params.storekey.replace(/:(meta|wal)@$/, `:data@`); - // const kb = await rt.kb.getKeyBag(this.sthis); - // const rfingerprint = await kb.getNamedKey(dataKey); - // if (rfingerprint.isErr()) { - // return this.logger.Error().Err(rfingerprint).Msg("Error in getNamedKey").ResultError(); - // } - // tenant = rfingerprint.Ok().fingerPrint; - // } - - const authJWK = uri.getParamResult("authJWK"); - if (authJWK.isErr()) { - return this.logger.Error().Err(authJWK).Msg("Missing URI authJWK").ResultError(); + } + async getCloudConnectionItem(uri: URI): Promise<{ conn: Result; citem: ConnectionItem }> { + const matchURI = connectionURI(uri); + const rConn = this.#connectionURIs.get(matchURI.toString()); + if (!rConn) { + return { conn: this.logger.Error().Url(uri).Msg("No connection found").ResultError(), citem: {} as ConnectionItem }; } + const conn = await rConn.connection.once(async () => { + const rParams = uri.getParamsResult({ + name: param.REQUIRED, + protocol: "https", + store: param.REQUIRED, + storekey: param.OPTIONAL, + tenant: param.REQUIRED, + }); + if (rParams.isErr()) { + return this.logger.Error().Url(uri).Err(rParams).Msg("getCloudConnection:err").ResultError(); + } + const params = rParams.Ok(); + // let tenant: string; + // if (params.tenant) { + // tenant = params.tenant; + // } else { + // if (!params.storekey) { + // return this.logger.Error().Url(uri).Msg("no tendant or storekey given").ResultError(); + // } + // const dataKey = params.storekey.replace(/:(meta|wal)@$/, `:data@`); + // const kb = await rt.kb.getKeyBag(this.sthis); + // const rfingerprint = await kb.getNamedKey(dataKey); + // if (rfingerprint.isErr()) { + // return this.logger.Error().Err(rfingerprint).Msg("Error in getNamedKey").ResultError(); + // } + // tenant = rfingerprint.Ok().fingerPrint; + // } + + const rAuth = await authTypeFromUri(this.logger, uri); + if (rAuth.isErr()) { + return Result.Err(rAuth); + } - const qOpen = buildReqOpen(this.sthis, authType(authJWK.Ok()), {}); + const qOpen = buildReqOpen(this.sthis, rAuth.Ok(), {}); - let cUrl = uri.build().protocol(params.protocol).cleanParams().URI(); - if (cUrl.pathname === "/") { - cUrl = cUrl.build().pathname("/fp").URI(); + let cUrl = uri.build().protocol(params.protocol).cleanParams().URI(); + if (cUrl.pathname === "/") { + cUrl = cUrl.build().pathname("/fp").URI(); + } + return Msger.connect(this.sthis, cUrl, qOpen); + }); + if (conn.isErr()) { + return { conn: Result.Err(conn), citem: rConn }; } - return Msger.connect(this.sthis, cUrl, qOpen); + return { conn: Result.Ok(conn.Ok().attachAuth(() => authTypeFromUri(this.logger, uri))), citem: rConn }; // keyedConnections.get(keyTenantLedger(qOpen.conn.key)).once(async () => Msger.open(this.sthis, cUrl, qOpen)); } @@ -566,8 +617,9 @@ export class FireproofCloudGateway implements bs.Gateway { // return Result.Ok(unsub); } - async destroy(_uri: URI): Promise> { - await Promise.all(Array.from(trackPuts).map(async (k) => this.delete(URI.from(k)))); + async destroy(uri: URI, sthis: SuperThis): Promise> { + const item = await this.getCloudConnectionItem(uri); + await Promise.all(Array.from(item.citem.trackPuts).map(async (k) => this.delete(URI.from(k), sthis))); return Result.Ok(undefined); } diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index d396373f..94830060 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -36,7 +36,8 @@ import { calculatePreSignedUrl } from "./pre-signed-url.js"; import { CFHonoServerFactory, httpStyle, - mockGetAuthFactory, + mockJWK, + MockJWK, NodeHonoServerFactory, resolveToml, wsStyle, @@ -77,13 +78,10 @@ async function refURL(sp: ResOptionalSignedUrl) { describe("Connection", () => { const sthis = ensureSuperThis(); const msgP = defaultMsgParams(sthis, { hasPersistent: true }); - let pubEnvJWK: string; // let privEnvJWK: string beforeAll(async () => { sthis.env.sets((await resolveToml("D1")).env as unknown as Record); - const pair = await SessionTokenService.generateKeyPair(); - pubEnvJWK = pair.strings.publicKey; // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); }); @@ -97,22 +95,16 @@ describe("Connection", () => { const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); const styles: (ReturnType | ReturnType)[] = []; + let auth: MockJWK + beforeAll(async () => { - const pair = await SessionTokenService.generateKeyPair(); - const authFactory = await mockGetAuthFactory( - pair.strings.privateKey, - { - userId: "hello", - tenants: [], - ledgers: [], - }, - sthis - ); + auth = await mockJWK(); + styles.push( ...[ // force multiple lines - httpStyle(sthis, authFactory, port, msgP, my), - wsStyle(sthis, authFactory, port, msgP, my), + httpStyle(sthis, auth.applyAuthToURI, port, msgP, my), + wsStyle(sthis, auth.applyAuthToURI, port, msgP, my), ] ); }); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index 8b922ea3..f50b4668 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -7,12 +7,12 @@ import { ErrorMsg, MsgWithError, buildRes, - MsgWithConn, GwCtx, MsgIsError, MsgTypesCtx, EnDeCoder, Gestalt, + MsgWithConnAuth, } from "./msg-types.js"; import { MsgDispatcher, MsgDispatcherCtx, Promisable, WSConnection } from "./msg-dispatch.js"; import { WSContext, WSContextInit, WSMessageReceive } from "hono/ws"; @@ -138,7 +138,7 @@ export abstract class HonoServerBase implements HonoServerImpl { // return this._gs; // } - async handleReqPutMeta(ctx: MsgDispatcherCtx, msg: MsgWithConn): Promise> { + async handleReqPutMeta(ctx: MsgDispatcherCtx, msg: MsgWithConnAuth): Promise> { const rUrl = await buildRes("PUT", "meta", "resPutMeta", ctx, msg, this); if (MsgIsError(rUrl)) { return rUrl; @@ -150,7 +150,7 @@ export abstract class HonoServerBase implements HonoServerImpl { return buildResPutMeta(ctx, msg, { ...rUrl, metas: await metaMerger(ctx).metaToSend(msg) }); } - async handleReqDelMeta(ctx: MsgDispatcherCtx, msg: MsgWithConn): Promise> { + async handleReqDelMeta(ctx: MsgDispatcherCtx, msg: MsgWithConnAuth): Promise> { const rUrl = await buildRes("DELETE", "meta", "resDelMeta", ctx, msg, this); if (MsgIsError(rUrl)) { return rUrl; @@ -163,7 +163,7 @@ export abstract class HonoServerBase implements HonoServerImpl { async handleBindGetMeta( ctx: MsgDispatcherCtx, - msg: MsgWithConn, + msg: MsgWithConnAuth, gwCtx: GwCtx = msg ): Promise> { const rMsg = await buildRes("GET", "meta", "eventGetMeta", ctx, msg, this); diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts index 7cf21acf..62db2486 100644 --- a/src/v2-cloud/http-connection.ts +++ b/src/v2-cloud/http-connection.ts @@ -1,6 +1,6 @@ import { HttpHeader, Logger, Result, URI, exception2Result } from "@adviser/cement"; import { SuperThis, ensureLogger } from "@fireproof/core"; -import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError, AuthFactory } from "./msg-types.js"; +import { MsgBase, buildErrorMsg, MsgWithError, RequestOpts, MsgIsError } from "./msg-types.js"; import { ActiveStream, ExchangedGestalt, @@ -21,10 +21,8 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec readonly #onMsg = new Map(); - readonly auth: AuthFactory; constructor( sthis: SuperThis, - auth: AuthFactory, uris: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt @@ -34,7 +32,6 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec // this.msgParam = msgP; this.baseURIs = uris; this.msgP = msgP; - this.auth = auth; } send(_msg: Q): Promise> { diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts index 72a9d3e3..4c1bd0a7 100644 --- a/src/v2-cloud/msg-dispatch.ts +++ b/src/v2-cloud/msg-dispatch.ts @@ -1,5 +1,5 @@ import { SuperThis } from "@fireproof/core"; -import { MsgBase, buildErrorMsg, MsgWithError, MsgWithConn, QSId } from "./msg-types.js"; +import { MsgBase, buildErrorMsg, MsgWithError, QSId, MsgWithConnAuth } from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; import { ExposeCtxItemWithImpl, HonoServerImpl, WSContextWithId } from "./hono-server.js"; @@ -112,7 +112,7 @@ export class MsgDispatcher { async dispatch(ctx: MsgDispatcherCtx, msg: MsgBase): Promise { const validateConn = async ( msg: T, - fn: (msg: MsgWithConn) => Promisable> + fn: (msg: MsgWithConnAuth) => Promisable> ): Promise => { if (!ctx.wsRoom.isConnected(msg)) { return this.send(ctx, buildErrorMsg(ctx, { ...msg }, new Error("dispatch missing connection"))); diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts index 9b5c014c..f96daf43 100644 --- a/src/v2-cloud/msg-dispatcher-impl.ts +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -28,7 +28,6 @@ import { MsgIsReqOpen, buildErrorMsg, buildResOpen, - MsgWithConn, ReqGestalt, // Gestalt, // EnDeCoder, @@ -39,6 +38,7 @@ import { MsgIsReqClose, buildResClose, ReqClose, + MsgWithConnAuth, } from "./msg-types.js"; import { BindGetMeta, @@ -82,14 +82,14 @@ export function buildMsgDispatcher( }, { match: MsgIsReqClose, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { ctx.wsRoom.removeConn(msg.conn); return buildResClose(msg, msg.conn); }, }, { match: MsgIsReqChat, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { const conns = ctx.wsRoom.getConns(msg.conn); const ci = conns.map((c) => c.conn); for (const conn of conns) { @@ -109,56 +109,56 @@ export function buildMsgDispatcher( }, { match: MsgIsReqGetData, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return buildResGetData(ctx, msg, ctx.impl); }, }, { match: MsgIsReqPutData, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return buildResPutData(ctx, msg, ctx.impl); }, }, { match: MsgIsReqDelData, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return buildResDelData(ctx, msg, ctx.impl); }, }, { match: MsgIsReqGetWAL, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return buildResGetWAL(ctx, msg, ctx.impl); }, }, { match: MsgIsReqPutWAL, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return buildResPutWAL(ctx, msg, ctx.impl); }, }, { match: MsgIsReqDelWAL, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return buildResDelWAL(ctx, msg, ctx.impl); }, }, { match: MsgIsBindGetMeta, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { // console.log("MsgIsBindGetMeta", msg); return ctx.impl.handleBindGetMeta(ctx, msg); }, }, { match: MsgIsReqPutMeta, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return ctx.impl.handleReqPutMeta(ctx, msg); }, }, { match: MsgIsReqDelMeta, - fn: (ctx, msg: MsgWithConn) => { + fn: (ctx, msg: MsgWithConnAuth) => { return ctx.impl.handleReqDelMeta(ctx, msg); }, } diff --git a/src/v2-cloud/msg-type-meta.ts b/src/v2-cloud/msg-type-meta.ts index 844a14ea..82fd38c7 100644 --- a/src/v2-cloud/msg-type-meta.ts +++ b/src/v2-cloud/msg-type-meta.ts @@ -4,23 +4,23 @@ import { AuthType, GwCtx, MsgBase, - MsgWithConn, - MsgWithOptionalConn, MsgWithTenantLedger, NextId, ReqSignedUrlParam, ResOptionalSignedUrl, MsgTypesCtx, + MsgWithOptionalConnAuth, + MsgWithConnAuth, } from "./msg-types.js"; /* Put Meta */ -export interface ReqPutMeta extends MsgWithTenantLedger { +export interface ReqPutMeta extends MsgWithTenantLedger { readonly type: "reqPutMeta"; readonly params: ReqSignedUrlParam; readonly metas: CRDTEntry[]; } -export interface ResPutMeta extends MsgWithTenantLedger, QSMeta { +export interface ResPutMeta extends MsgWithTenantLedger, QSMeta { readonly type: "resPutMeta"; } @@ -48,7 +48,7 @@ export function MsgIsReqPutMeta(msg: MsgBase): msg is ReqPutMeta { export function buildResPutMeta( _msgCtx: MsgTypesCtx, - req: MsgWithTenantLedger>, + req: MsgWithTenantLedger>, meta: QSMeta ): ResPutMeta { return { @@ -67,7 +67,7 @@ export function MsgIsResPutMeta(qs: MsgBase): qs is ResPutMeta { } /* Bind Meta */ -export interface BindGetMeta extends MsgWithTenantLedger { +export interface BindGetMeta extends MsgWithTenantLedger { readonly type: "bindGetMeta"; readonly params: ReqSignedUrlParam; } @@ -81,7 +81,7 @@ export interface QSMeta extends ResOptionalSignedUrl { readonly keys?: string[]; } -export interface EventGetMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { +export interface EventGetMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { readonly type: "eventGetMeta"; } @@ -98,7 +98,7 @@ export function buildBindGetMeta(sthis: NextId, auth: AuthType, params: ReqSigne export function buildEventGetMeta( _msgCtx: MsgTypesCtx, - req: MsgWithTenantLedger>, + req: MsgWithTenantLedger>, metaParam: QSMeta, gwCtx: GwCtx ): EventGetMeta { @@ -117,7 +117,7 @@ export function MsgIsEventGetMeta(qs: MsgBase): qs is EventGetMeta { } /* Del Meta */ -export interface ReqDelMeta extends MsgWithTenantLedger { +export interface ReqDelMeta extends MsgWithTenantLedger { readonly type: "reqDelMeta"; readonly params: ReqSignedUrlParam; } @@ -142,13 +142,13 @@ export function MsgIsReqDelMeta(msg: MsgBase): msg is ReqDelMeta { return msg.type === "reqDelMeta"; } -export interface ResDelMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { +export interface ResDelMeta extends MsgWithTenantLedger, ResOptionalSignedUrl { readonly type: "resDelMeta"; } export function buildResDelMeta( // msgCtx: MsgTypesCtx, - req: MsgWithTenantLedger>, + req: MsgWithTenantLedger>, signedUrl?: string ): ResDelMeta { return { diff --git a/src/v2-cloud/msg-types-data.ts b/src/v2-cloud/msg-types-data.ts index c60adcc8..f03b697b 100644 --- a/src/v2-cloud/msg-types-data.ts +++ b/src/v2-cloud/msg-types-data.ts @@ -10,8 +10,8 @@ import { buildReqSignedUrl, GwCtx, MsgIsTenantLedger, - MsgWithConn, MsgTypesCtx, + MsgWithConnAuth, } from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; @@ -42,10 +42,10 @@ export interface CalculatePreSignedUrl { export function buildResGetData( msgCtx: MsgTypesCtx, - req: MsgWithConn, + req: MsgWithConnAuth, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResGetData>("GET", "data", "resGetData", msgCtx, req, ctx); + return buildRes, ResGetData>("GET", "data", "resGetData", msgCtx, req, ctx); } export interface ReqPutData extends ReqSignedUrl { @@ -71,10 +71,10 @@ export function MsgIsResPutData(msg: MsgBase): msg is ResPutData { export function buildResPutData( msgCtx: MsgTypesCtx, - req: MsgWithConn, + req: MsgWithConnAuth, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResPutData>("PUT", "data", "resPutData", msgCtx, req, ctx); + return buildRes, ResPutData>("PUT", "data", "resPutData", msgCtx, req, ctx); } export interface ReqDelData extends ReqSignedUrl { @@ -99,8 +99,8 @@ export function MsgIsResDelData(msg: MsgBase): msg is ResDelData { export function buildResDelData( msgCtx: MsgTypesCtx, - req: MsgWithConn, + req: MsgWithConnAuth, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResDelData>("DELETE", "data", "resDelData", msgCtx, req, ctx); + return buildRes, ResDelData>("DELETE", "data", "resDelData", msgCtx, req, ctx); } diff --git a/src/v2-cloud/msg-types-wal.ts b/src/v2-cloud/msg-types-wal.ts index 1f10f1ef..07c8c06c 100644 --- a/src/v2-cloud/msg-types-wal.ts +++ b/src/v2-cloud/msg-types-wal.ts @@ -10,8 +10,8 @@ import { GwCtx, MsgIsTenantLedger, MsgWithTenantLedger, - MsgWithConn, MsgTypesCtx, + MsgWithConnAuth, } from "./msg-types.js"; import { CalculatePreSignedUrl } from "./msg-types-data.js"; @@ -38,10 +38,10 @@ export function MsgIsResGetWAL(msg: MsgBase): msg is ResGetWAL { export function buildResGetWAL( msgCtx: MsgTypesCtx, - req: MsgWithTenantLedger>, + req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResGetWAL>("GET", "wal", "resGetWAL", msgCtx, req, ctx); + return buildRes>, ResGetWAL>("GET", "wal", "resGetWAL", msgCtx, req, ctx); } export interface ReqPutWAL extends Omit { @@ -67,10 +67,10 @@ export function MsgIsResPutWAL(msg: MsgBase): msg is ResPutWAL { export function buildResPutWAL( msgCtx: MsgTypesCtx, - req: MsgWithTenantLedger>, + req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResPutWAL>("PUT", "wal", "resPutWAL", msgCtx, req, ctx); + return buildRes>, ResPutWAL>("PUT", "wal", "resPutWAL", msgCtx, req, ctx); } export interface ReqDelWAL extends Omit { @@ -95,10 +95,10 @@ export function MsgIsResDelWAL(msg: MsgBase): msg is ResDelWAL { export function buildResDelWAL( msgCtx: MsgTypesCtx, - req: MsgWithTenantLedger>, + req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResDelWAL>( + return buildRes>, ResDelWAL>( "DELETE", "wal", "resDelWAL", diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts index 5fe320de..eb4d77e7 100644 --- a/src/v2-cloud/msg-types.ts +++ b/src/v2-cloud/msg-types.ts @@ -1,4 +1,4 @@ -import { Future, Logger } from "@adviser/cement"; +import { Future, Logger, Result } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; import { CalculatePreSignedUrl } from "./msg-types-data.js"; import { PreSignedMsg } from "./pre-signed-url.js"; @@ -36,7 +36,7 @@ export interface NextId { } export interface AuthType { - readonly type: "ucan" | "error" | "fp-cloud-jwk"; + readonly type: "ucan" | "error" | "fp-cloud-jwk" | "fp-cloud"; } export interface UCanAuth extends AuthType { @@ -45,14 +45,19 @@ export interface UCanAuth extends AuthType { readonly tbd: string; }; } -export interface FPCloudAuth extends AuthType { +export interface FPJWKCloudAuthType extends AuthType { readonly type: "fp-cloud-jwk"; readonly params: { readonly jwk: string; }; } -export type AuthFactory = (tp?: Partial) => Promise; +export interface FPCloudAuthType extends AuthType { + readonly type: "fp-cloud"; + readonly params: TokenForParam; +} + +export type AuthFactory = (tp?: Partial) => Promise>; export interface TenantLedger { readonly tenant: string; @@ -95,9 +100,13 @@ export function MsgIsTid(msg: MsgBase, tid: string): boolean { return msg.tid === tid; } -export type MsgWithConn = T & { readonly conn: QSId }; +type MsgWithConn = T & { readonly conn: QSId }; + +export type MsgWithConnAuth = MsgWithConn & { readonly auth: AuthType }; -export type MsgWithOptionalConn = T & { readonly conn?: QSId }; +type MsgWithOptionalConn = T & { readonly conn?: QSId }; + +export type MsgWithOptionalConnAuth = MsgWithOptionalConn & { readonly auth: AuthType }; export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; @@ -579,14 +588,7 @@ export interface MsgTypesCtx { // }; // } -export function authType(jwk: string): AuthType { - return { - type: "fp-cloud-jwk", - params: { - jwk, - }, - } as FPCloudAuth; -} + export interface MsgTypesCtxSync { readonly sthis: SuperThis; diff --git a/src/v2-cloud/msger.ts b/src/v2-cloud/msger.ts index 0699b055..52a1705b 100644 --- a/src/v2-cloud/msger.ts +++ b/src/v2-cloud/msger.ts @@ -1,4 +1,4 @@ -import { BuildURI, CoerceURI, Result, runtimeFn, URI } from "@adviser/cement"; +import { BuildURI, CoerceURI, Logger, Result, runtimeFn, URI } from "@adviser/cement"; import { buildReqGestalt, defaultGestalt, @@ -10,22 +10,25 @@ import { RequestOpts, ResGestalt, MsgWithError, - MsgWithConn, + MsgWithConnAuth, buildReqOpen, MsgIsConnected, MsgIsError, MsgIsResOpen, - MsgWithOptionalConn, + MsgWithOptionalConnAuth, QSId, MsgIsTid, ReqGestalt, buildReqClose, MsgIsResClose, AuthFactory, + FPCloudAuthType, + AuthType, } from "./msg-types.js"; import { SuperThis } from "@fireproof/core"; import { HttpConnection } from "./http-connection.js"; import { WSConnection } from "./ws-connection.js"; +import { SessionTokenService } from "../sts-service/sts-service.js"; // const headers = { // "Content-Type": "application/json", @@ -79,7 +82,7 @@ export interface MsgRawConnection { request(req: Q, opts: RequestOpts): Promise>; send(msg: Q): Promise>; start(): Promise>; - close(): Promise>; + close(o: T): Promise>; onMsg(msg: OnMsgFn): UnReg; } @@ -119,9 +122,29 @@ export async function applyStart(prC: Promise>): Promis return rC; } -export class MsgConnected implements MsgRawConnection { +export async function authTypeFromUri(logger: Logger, curi: CoerceURI): Promise> { + const uri = URI.from(curi); + const authJWK = uri.getParam("authJWK"); + if (!authJWK) { + return logger.Error().Url(uri).Msg("authJWK is required").ResultError(); + } + const sts = await SessionTokenService.createFromEnv() + const fpc = await sts.validate(authJWK) + if (fpc.isErr()) { + return logger.Error().Err(fpc).Msg("Invalid authJWK").ResultError(); + } + return Result.Ok({ + type: "fp-cloud", + params: { + ...fpc.Ok().payload, + jwk: authJWK, + }, + } satisfies FPCloudAuthType) +} + +export class MsgConnected { static async connect( - authFactory: AuthFactory, + uri: CoerceURI, mrc: Result | MsgRawConnection, conn: Partial = {} ): Promise> { @@ -131,31 +154,57 @@ export class MsgConnected implements MsgRawConnection { } mrc = mrc.Ok(); } - const res = await mrc.request(buildReqOpen(mrc.sthis, await authFactory(), conn), { waitFor: MsgIsResOpen }); + const rAuthType = await authTypeFromUri(mrc.sthis.logger, uri); + if (rAuthType.isErr()) { + return Result.Err(rAuthType) + } + const res = await mrc.request(buildReqOpen(mrc.sthis, rAuthType.Ok(), conn), { waitFor: MsgIsResOpen }); if (MsgIsError(res) || !MsgIsResOpen(res)) { return mrc.sthis.logger.Error().Err(res).Msg("unexpected response").ResultError(); } - return Result.Ok(new MsgConnected(mrc, authFactory, res.conn)); + return Result.Ok(new MsgConnected(mrc, res.conn)); } readonly sthis: SuperThis; readonly conn: QSId; readonly raw: MsgRawConnection; readonly exchangedGestalt: ExchangedGestalt; - readonly activeBinds: Map>; + readonly activeBinds: Map>; readonly id: string; - readonly authFactory: AuthFactory; - private constructor(raw: MsgRawConnection, auth: AuthFactory, conn: QSId) { + private constructor(raw: MsgRawConnection, conn: QSId) { this.sthis = raw.sthis; this.raw = raw; this.exchangedGestalt = raw.exchangedGestalt; this.conn = conn; - this.authFactory = auth; this.activeBinds = raw.activeBinds; this.id = this.sthis.nextId().str; } - bind( + attachAuth(auth: AuthFactory): MsgConnectedAuth { + return new MsgConnectedAuth(this, auth); + } +} + +export class MsgConnectedAuth implements MsgRawConnection { + readonly sthis: SuperThis; + readonly conn: QSId; + readonly raw: MsgRawConnection; + readonly exchangedGestalt: ExchangedGestalt; + readonly activeBinds: Map>; + readonly id: string; + readonly authFactory: AuthFactory; + + constructor(conn: MsgConnected, authFactory: AuthFactory) { + this.id = conn.id; + this.raw = conn.raw; + this.conn = conn.conn; + this.sthis = conn.sthis; + this.authFactory = authFactory; + this.exchangedGestalt = conn.exchangedGestalt; + this.activeBinds = conn.activeBinds; + } + + bind( req: Q, opts: RequestOpts ): ReadableStream> { @@ -179,23 +228,36 @@ export class MsgConnected implements MsgRawConnection { return ts.readable; } - request(req: Q, opts: RequestOpts): Promise> { + authType(): Promise> { + return this.authFactory(); + } + + msgConnAuth(): Promise< Result> { + return this.authType().then((r) => { + if (r.isErr()) { + return Result.Err(r); + } + return Result.Ok({ conn: this.conn, auth: r.Ok() } as MsgWithConnAuth); + }); + } + + request(req: Q, opts: RequestOpts): Promise> { return this.raw.request({ ...req, conn: req.conn || this.conn }, opts); } - send(msg: Q): Promise> { + send(msg: Q): Promise> { return this.raw.send({ ...msg, conn: msg.conn || this.conn }); } start(): Promise> { return this.raw.start(); } - async close(): Promise> { - await this.request(buildReqClose(this.sthis, await this.authFactory(), this.conn), { waitFor: MsgIsResClose }); - return await this.raw.close(); + async close(t: MsgWithConnAuth): Promise> { + await this.request(buildReqClose(this.sthis, t.auth, this.conn), { waitFor: MsgIsResClose }); + return await this.raw.close(t); // return Result.Ok(undefined); } - onMsg(msgFn: OnMsgFn): UnReg { + onMsg(msgFn: OnMsgFn): UnReg { return this.raw.onMsg((msg) => { if (MsgIsConnected(msg, this.conn)) { msgFn(msg); @@ -208,17 +270,15 @@ export class MsgConnected implements MsgRawConnection { export class Msger { static async openHttp( sthis: SuperThis, - auth: AuthFactory, // reqOpen: ReqOpen | undefined, urls: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt ): Promise> { - return Result.Ok(new HttpConnection(sthis, auth, urls, msgP, exGestalt)); + return Result.Ok(new HttpConnection(sthis, urls, msgP, exGestalt)); } static async openWS( sthis: SuperThis, - authFactory: AuthFactory, // qOpen: ReqOpen, url: URI, msgP: MsgerParamsWithEnDe, @@ -234,11 +294,10 @@ export class Msger { } else { ws = new WebSocket(url.toString()); } - return Result.Ok(new WSConnection(sthis, authFactory, ws, msgP, exGestalt)); + return Result.Ok(new WSConnection(sthis, ws, msgP, exGestalt)); } static async open( sthis: SuperThis, - auth: AuthFactory, curl: CoerceURI, imsgP: Partial = {} ): Promise> { @@ -249,25 +308,28 @@ export class Msger { /* * request Gestalt with Http */ - const rHC = await Msger.openHttp(sthis, auth, [url], jsMsgP, { my: gs, remote: gs }); + const rHC = await Msger.openHttp(sthis, [url], jsMsgP, { my: gs, remote: gs }); if (rHC.isErr()) { return rHC; } const hc = rHC.Ok(); - const resGestalt = await hc.request(buildReqGestalt(sthis, await auth(), gs), { + const rAuth = await authTypeFromUri(sthis.logger, url); + if (rAuth.isErr()) { + return Result.Err(rAuth) + } + const resGestalt = await hc.request(buildReqGestalt(sthis, rAuth.Ok(), gs), { waitFor: MsgIsResGestalt, }); if (!MsgIsResGestalt(resGestalt)) { return Result.Err(new Error("Invalid Gestalt")); } - await hc.close(); + await hc.close(resGestalt /* as MsgWithConnAuth */); const exGt = { my: gs, remote: resGestalt.gestalt } satisfies ExchangedGestalt; const msgP = defaultMsgParams(sthis, imsgP); if (exGt.remote.protocolCapabilities.includes("reqRes") && !exGt.remote.protocolCapabilities.includes("stream")) { return applyStart( Msger.openHttp( sthis, - auth, exGt.remote.httpEndpoints.map((i) => BuildURI.from(url).resolve(i).URI()), msgP, exGt @@ -275,18 +337,17 @@ export class Msger { ); } return applyStart( - Msger.openWS(sthis, auth, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) + Msger.openWS(sthis, BuildURI.from(url).resolve(selectRandom(exGt.remote.wsEndpoints)).URI(), msgP, exGt) ); } static connect( sthis: SuperThis, - auth: AuthFactory, curl: CoerceURI, imsgP: Partial = {}, conn: Partial = {} ): Promise> { - return Msger.open(sthis, auth, curl, imsgP).then((srv) => MsgConnected.connect(auth, srv, conn)); + return Msger.open(sthis, curl, imsgP).then((srv) => MsgConnected.connect(curl, srv, conn)); } private constructor() { diff --git a/src/v2-cloud/node-hono-server.ts b/src/v2-cloud/node-hono-server.ts index 4349232c..def1b208 100644 --- a/src/v2-cloud/node-hono-server.ts +++ b/src/v2-cloud/node-hono-server.ts @@ -19,7 +19,7 @@ import { MsgBase, MsgerParams, MsgIsWithConn, - MsgWithConn, + MsgWithConnAuth, QSId, qsidKey, } from "./msg-types.js"; @@ -68,7 +68,7 @@ class NodeWSRoom implements WSRoom { return ci.conn; } - isConnected(msg: MsgBase): msg is MsgWithConn { + isConnected(msg: MsgBase): msg is MsgWithConnAuth { if (!MsgIsWithConn(msg)) { return false; } diff --git a/src/v2-cloud/pre-signed-url.ts b/src/v2-cloud/pre-signed-url.ts index a5f25de7..2abcaf83 100644 --- a/src/v2-cloud/pre-signed-url.ts +++ b/src/v2-cloud/pre-signed-url.ts @@ -1,8 +1,8 @@ import { Result, URI } from "@adviser/cement"; import { AwsClient } from "aws4fetch"; -import { MsgWithConn, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; +import { MsgWithConnAuth, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; -export interface PreSignedMsg extends MsgWithTenantLedger { +export interface PreSignedMsg extends MsgWithTenantLedger { readonly params: SignedUrlParam; } diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index dc37bfbb..897bb44c 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -1,4 +1,4 @@ -import { Future, Result, URI } from "@adviser/cement"; +import { BuildURI, CoerceURI, Future, Result, URI } from "@adviser/cement"; import { SuperThis } from "@fireproof/core"; import { $, fs, sleep } from "zx"; import { HttpConnection } from "./http-connection.js"; @@ -10,9 +10,8 @@ import { MsgIsResGestalt, MsgIsError, MsgBase, - AuthFactory, } from "./msg-types.js"; -import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection } from "./msger.js"; +import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection, authTypeFromUri } from "./msger.js"; import { WSConnection } from "./ws-connection.js"; import * as toml from "smol-toml"; import { Env } from "./backend/env.js"; @@ -20,11 +19,34 @@ import { HonoServer } from "./hono-server.js"; import { NodeHonoFactory } from "./node-hono-server.js"; import { CFHonoFactory } from "./backend/cf-hono-server.js"; import { BetterSQLDatabase } from "./meta-merger/bettersql-abstract-sql.js"; -import { envKeyDefaults, SessionTokenService, TokenForParam } from "../sts-service/sts-service.js"; +import { envKeyDefaults, KeysResult, SessionTokenService, TokenForParam } from "../sts-service/sts-service.js"; + +export interface MockJWK { + keys: KeysResult; + applyAuthToURI: (uri: CoerceURI) => URI; +} +export async function mockJWK(claim: Partial = {}): Promise { + const keys = await SessionTokenService.generateKeyPair() + + const sts = await SessionTokenService.create({ + token: keys.strings.privateKey + }) + const jwk = await sts.tokenFor({ + userId: "hello", + tenants: [], + ledgers: [], + ...claim + }) + + return { + keys, + applyAuthToURI: (uri: CoerceURI) => BuildURI.from(uri).setParam("authJWK", jwk).URI() + } +} export function httpStyle( sthis: SuperThis, - authFactory: AuthFactory, + applyAuthToURI: (uri: CoerceURI) => URI, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt @@ -36,7 +58,6 @@ export function httpStyle( return { name: "HTTP", remoteGestalt: remote, - authFactory, cInstance: HttpConnection, ok: { url: () => URI.from(`http://127.0.0.1:${port}/fp`), @@ -44,7 +65,6 @@ export function httpStyle( applyStart( Msger.openHttp( sthis, - authFactory, [URI.from(`http://localhost:${port}/fp`)], { ...msgP, @@ -60,7 +80,6 @@ export function httpStyle( open: async (): Promise>> => { const ret = await Msger.openHttp( sthis, - authFactory, [URI.from(`http://localhost:${port - 1}/fp`)], { ...msgP, @@ -72,10 +91,12 @@ export function httpStyle( if (ret.isErr()) { return ret; } + + const rAuth = await authTypeFromUri(sthis.logger, applyAuthToURI(`http://localhost:${port - 1}/fp`)); // should fail const res = await ret .Ok() - .request(buildReqGestalt(sthis, await authFactory(), my), { waitFor: MsgIsResGestalt }); + .request(buildReqGestalt(sthis, rAuth.Ok(), my), { waitFor: MsgIsResGestalt }); if (MsgIsError(res)) { return Result.Err(res.message); } @@ -87,7 +108,6 @@ export function httpStyle( open: async (): Promise>> => { const ret = await Msger.openHttp( sthis, - authFactory, [URI.from(`http://4.7.1.1:${port}/fp`)], { ...msgP, @@ -97,9 +117,10 @@ export function httpStyle( exGt ); // should fail + const rAuth = await authTypeFromUri(sthis.logger, applyAuthToURI(`http://4.7.1.1:${port}/fp`)); const res = await ret .Ok() - .request(buildReqGestalt(sthis, await authFactory(), my), { waitFor: MsgIsResGestalt }); + .request(buildReqGestalt(sthis, rAuth.Ok(), my), { waitFor: MsgIsResGestalt }); if (MsgIsError(res)) { return Result.Err(res.message); } @@ -111,7 +132,7 @@ export function httpStyle( export function wsStyle( sthis: SuperThis, - authFactory: AuthFactory, + applyAuthToURI: (uri: CoerceURI) => URI, port: number, msgP: MsgerParamsWithEnDe, my: Gestalt @@ -123,7 +144,6 @@ export function wsStyle( return { name: "WS", remoteGestalt: remote, - authFactory, cInstance: WSConnection, ok: { url: () => URI.from(`http://127.0.0.1:${port}/ws`), @@ -131,8 +151,7 @@ export function wsStyle( applyStart( Msger.openWS( sthis, - authFactory, - URI.from(`http://localhost:${port}/ws`), + applyAuthToURI(URI.from(`http://localhost:${port}/ws`)), { ...msgP, // protocol: "ws", @@ -147,8 +166,7 @@ export function wsStyle( open: () => Msger.openWS( sthis, - authFactory, - URI.from(`http://localhost:${port - 1}/ws`), + applyAuthToURI(URI.from(`http://localhost:${port - 1}/ws`)), { ...msgP, // protocol: "ws", @@ -162,8 +180,7 @@ export function wsStyle( open: () => Msger.openWS( sthis, - authFactory, - URI.from(`http://4.7.1.1:${port - 1}/ws`), + applyAuthToURI(URI.from(`http://4.7.1.1:${port - 1}/ws`)), { ...msgP, // protocol: "ws", @@ -255,27 +272,27 @@ export function CFHonoServerFactory(backend: "D1" | "DO") { }; } -export async function mockGetAuthFactory(pk: string, factoryTp: TokenForParam, sthis: SuperThis): Promise { - const sts = await SessionTokenService.create( - { - token: pk, - }, - sthis - ); +// export async function mockGetAuthFactory(pk: string, factoryTp: TokenForParam, sthis: SuperThis): Promise { +// const sts = await SessionTokenService.create( +// { +// token: pk, +// }, +// sthis +// ); - return async (tp: Partial = {}) => { - const token = await sts.tokenFor({ - ...factoryTp, - ...tp, - userId: tp.userId || factoryTp.userId, - tenants: tp.tenants || factoryTp.tenants, - ledgers: tp.ledgers || factoryTp.ledgers, - }); - return { - type: "fp-cloud-jwk", - params: { - jwk: token, - }, - }; - }; -} +// return async (tp: Partial = {}) => { +// const token = await sts.tokenFor({ +// ...factoryTp, +// ...tp, +// userId: tp.userId || factoryTp.userId, +// tenants: tp.tenants || factoryTp.tenants, +// ledgers: tp.ledgers || factoryTp.ledgers, +// }); +// return { +// type: "fp-cloud-jwk", +// params: { +// jwk: token, +// }, +// }; +// }; +// } diff --git a/src/v2-cloud/ws-connection.ts b/src/v2-cloud/ws-connection.ts index b05cdc06..1cd7e4f5 100644 --- a/src/v2-cloud/ws-connection.ts +++ b/src/v2-cloud/ws-connection.ts @@ -9,7 +9,6 @@ import { MsgWithError, RequestOpts, MsgIsTid, - AuthFactory, } from "./msg-types.js"; import { ActiveStream, ExchangedGestalt, MsgerParamsWithEnDe, MsgRawConnection, OnMsgFn, UnReg } from "./msger.js"; import { MsgRawConnectionBase } from "./msg-raw-connection-base.js"; @@ -33,11 +32,9 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti opened = false; readonly id: string; - readonly auth: AuthFactory; constructor( sthis: SuperThis, - auth: AuthFactory, ws: WebSocket, msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt @@ -47,7 +44,6 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti this.logger = ensureLogger(sthis, "WSConnection"); this.msgP = msgP; this.ws = ws; - this.auth = auth; // this.wqs = { ...wsq }; } diff --git a/src/v2-cloud/ws-room.ts b/src/v2-cloud/ws-room.ts index 02cd4c60..9a502500 100644 --- a/src/v2-cloud/ws-room.ts +++ b/src/v2-cloud/ws-room.ts @@ -1,5 +1,5 @@ import { WSContextWithId } from "./hono-server.js"; -import { MsgBase, MsgWithConn, QSId } from "./msg-types.js"; +import { MsgBase, MsgWithConnAuth, QSId } from "./msg-types.js"; import { ConnItem } from "./msg-dispatch.js"; export interface WSRoom { @@ -8,7 +8,7 @@ export interface WSRoom { getConns(conn: QSId): ConnItem[]; removeConn(conn: QSId): void; addConn(ws: WSContextWithId, conn: QSId): QSId; - isConnected(msg: MsgBase): msg is MsgWithConn; + isConnected(msg: MsgBase): msg is MsgWithConnAuth; } // class ConnectionManager { From 2e37e7ab0f160be899789243c625b38a65b609a3 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Tue, 11 Mar 2025 15:59:53 +0100 Subject: [PATCH 10/14] chore: replace coerce-binary --- package.json | 6 +- pnpm-lock.yaml | 145 ++++++++++++++++-- src/aws/gateway.ts | 3 +- src/coerce-binary.ts | 42 ----- src/netlify/backend/server.ts | 2 +- src/ucan/common.ts | 2 +- src/v2-cloud/client/cloud-gateway.test.ts | 19 +-- src/v2-cloud/client/gateway.ts | 118 +++++++------- src/v2-cloud/connection.test.ts | 49 +++--- src/v2-cloud/hono-server.ts | 3 +- src/v2-cloud/test-helper.ts | 8 + src/v2-cloud/ws-sockets.test.ts | 43 +++--- .../app/netlify/edge-functions/fireproof.ts | 2 +- 13 files changed, 265 insertions(+), 177 deletions(-) delete mode 100644 src/coerce-binary.ts diff --git a/package.json b/package.json index 18cc3325..5a2b4254 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.8", "wait-on": "^8.0.2", - "wrangler": "^3.112.0", + "wrangler": "3.112.0", "zx": "^8.4.0" }, "repository": { @@ -95,10 +95,10 @@ }, "homepage": "https://github.com/fireproof-storage/connect#readme", "peerDependencies": { - "@adviser/cement": "^0.4.0" + "@adviser/cement": "^0.4.1" }, "dependencies": { - "@adviser/cement": "^0.4.0", + "@adviser/cement": "^0.4.1", "@aws-sdk/client-dynamodb": "^3.758.0", "@aws-sdk/client-lambda": "^3.758.0", "@aws-sdk/client-s3": "^3.758.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 723ce34a..0a7f5609 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@adviser/cement': - specifier: ^0.4.0 - version: 0.4.0(typescript@5.7.3) + specifier: ^0.4.1 + version: 0.4.1(typescript@5.7.3) '@aws-sdk/client-dynamodb': specifier: ^3.758.0 version: 3.758.0 @@ -34,7 +34,7 @@ importers: version: 4.20250303.0 '@fireproof/core': specifier: 0.20.0-dev-preview-53 - version: 0.20.0-dev-preview-53(@adviser/cement@0.4.0(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1) + version: 0.20.0-dev-preview-53(@adviser/cement@0.4.1(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1) '@fireproof/vendor': specifier: ~2.0.0 version: 2.0.1 @@ -244,16 +244,16 @@ importers: specifier: ^8.0.2 version: 8.0.2 wrangler: - specifier: ^3.112.0 - version: 3.114.0(@cloudflare/workers-types@4.20250303.0) + specifier: 3.112.0 + version: 3.112.0(@cloudflare/workers-types@4.20250303.0) zx: specifier: ^8.4.0 version: 8.4.0 packages: - '@adviser/cement@0.4.0': - resolution: {integrity: sha512-AH/bfC0nTA/aBEtrS1hjB4E04YgkoBl2/YN/ZFFpUWuPuACwUGqtAGxXvCjuI6hDkUqlFCWodrwZ7/sU09MbKA==} + '@adviser/cement@0.4.1': + resolution: {integrity: sha512-32Pa5mDXnIqFX5CV42dM785YHMEZ6Vc2c0PIN+f2tvthV4lRe8Nw5YiD72diiheEmxrTzUG0P708LH1yFE1w9Q==} engines: {node: '>=20'} '@aws-crypto/crc32@5.2.0': @@ -524,6 +524,12 @@ packages: cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20250214.0': + resolution: {integrity: sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-64@1.20250224.0': resolution: {integrity: sha512-sBbaAF2vgQ9+T50ik1ihekdepStBp0w4fvNghBfXIw1iWqfNWnypcjDMmi/7JhXJt2uBxBrSlXCvE5H7Gz+kbw==} engines: {node: '>=16'} @@ -536,6 +542,12 @@ packages: cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20250214.0': + resolution: {integrity: sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20250224.0': resolution: {integrity: sha512-naetGefgjAaDbEacpwaVruJXNwxmRRL7v3ppStgEiqAlPmTpQ/Edjn2SQ284QwOw3MvaVPHrWcaTBupUpkqCyg==} engines: {node: '>=16'} @@ -548,6 +560,12 @@ packages: cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20250214.0': + resolution: {integrity: sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-64@1.20250224.0': resolution: {integrity: sha512-BtUvuj91rgB06TUAkLYvedghUA8nDFiLcY3GC7MXmWhxCxGmY4PWkrKq/+uHjrhwknCcXrE4aFsM28ja8EcAGA==} engines: {node: '>=16'} @@ -560,6 +578,12 @@ packages: cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20250214.0': + resolution: {integrity: sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20250224.0': resolution: {integrity: sha512-Gr4MPNi+BvwjfWF7clx0dJY2Vm4suaW5FtAQwrfqJmPtN5zb/BP16VZxxnFRMy377dP7ycoxpKfZZ6Q8RVGvbA==} engines: {node: '>=16'} @@ -572,6 +596,12 @@ packages: cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20250214.0': + resolution: {integrity: sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workerd-windows-64@1.20250224.0': resolution: {integrity: sha512-x2iF1CsmYmmPEorWb1GRpAAouX5rRjmhuHMC259ojIlozR4G0LarlB9XfmeLEvtw537Ea0kJ6SOhjvUcWzxSvA==} engines: {node: '>=16'} @@ -5375,6 +5405,11 @@ packages: engines: {node: '>=16.13'} hasBin: true + miniflare@3.20250214.2: + resolution: {integrity: sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA==} + engines: {node: '>=16.13'} + hasBin: true + miniflare@3.20250224.0: resolution: {integrity: sha512-DyLxzhHCQ9UWDceqEsT7tmw8ZTSAhb1yKUqUi5VDmSxsIocKi4y5kvMijw9ELK8+tq/CiCp/RQxwRNZRJD8Xbg==} engines: {node: '>=16.13'} @@ -5678,6 +5713,9 @@ packages: ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + ohash@1.1.6: + resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -7113,6 +7151,9 @@ packages: resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} engines: {node: '>=14.0'} + unenv@2.0.0-rc.1: + resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==} + unenv@2.0.0-rc.8: resolution: {integrity: sha512-wj/lN45LvZ4Uz95rti6DC5wg56eocAwA8KYzExk2SN01iuAb9ayzMwN13Kd3EG2eBXu27PzgMIXLTwWfSczKfw==} @@ -7446,11 +7487,26 @@ packages: engines: {node: '>=16'} hasBin: true + workerd@1.20250214.0: + resolution: {integrity: sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g==} + engines: {node: '>=16'} + hasBin: true + workerd@1.20250224.0: resolution: {integrity: sha512-NntMg1d9SSkbS4vGdjV5NZxe6FUrvJXY7UiQD7fBtCRVpoPpqz9bVgTq86zalMm+vz64lftzabKT4ka4Y9hejQ==} engines: {node: '>=16'} hasBin: true + wrangler@3.112.0: + resolution: {integrity: sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20250214.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrangler@3.114.0: resolution: {integrity: sha512-cY0HxgU5yuc24tE1Y4KD2n9UzYYEx+9lSL7p/Sqj18SgDfwyiMPY/FryXQAPYLuD/S+dxArRQyeEkFSokIr75Q==} engines: {node: '>=16.17.0'} @@ -7591,7 +7647,7 @@ packages: snapshots: - '@adviser/cement@0.4.0(typescript@5.7.3)': + '@adviser/cement@0.4.1(typescript@5.7.3)': dependencies: ts-essentials: 10.0.4(typescript@5.7.3) yaml: 2.7.0 @@ -8311,30 +8367,45 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20240718.0': optional: true + '@cloudflare/workerd-darwin-64@1.20250214.0': + optional: true + '@cloudflare/workerd-darwin-64@1.20250224.0': optional: true '@cloudflare/workerd-darwin-arm64@1.20240718.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20250214.0': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20250224.0': optional: true '@cloudflare/workerd-linux-64@1.20240718.0': optional: true + '@cloudflare/workerd-linux-64@1.20250214.0': + optional: true + '@cloudflare/workerd-linux-64@1.20250224.0': optional: true '@cloudflare/workerd-linux-arm64@1.20240718.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20250214.0': + optional: true + '@cloudflare/workerd-linux-arm64@1.20250224.0': optional: true '@cloudflare/workerd-windows-64@1.20240718.0': optional: true + '@cloudflare/workerd-windows-64@1.20250214.0': + optional: true + '@cloudflare/workerd-windows-64@1.20250224.0': optional: true @@ -8803,9 +8874,9 @@ snapshots: fastq: 1.19.0 glob: 10.4.5 - '@fireproof/core@0.20.0-dev-preview-53(@adviser/cement@0.4.0(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1)': + '@fireproof/core@0.20.0-dev-preview-53(@adviser/cement@0.4.1(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1)': dependencies: - '@adviser/cement': 0.4.0(typescript@5.7.3) + '@adviser/cement': 0.4.1(typescript@5.7.3) '@fireproof/vendor': 2.0.1 '@ipld/car': 5.4.0 '@ipld/dag-cbor': 9.2.2 @@ -13747,6 +13818,23 @@ snapshots: - supports-color - utf-8-validate + miniflare@3.20250214.2: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.5 + workerd: 1.20250214.0 + ws: 8.18.0 + youch: 3.2.3 + zod: 3.22.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + miniflare@3.20250224.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -14199,6 +14287,8 @@ snapshots: node-fetch-native: 1.6.6 ufo: 1.5.4 + ohash@1.1.6: {} + ohash@2.0.11: {} omit.js@2.0.2: {} @@ -15757,6 +15847,14 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 + unenv@2.0.0-rc.1: + dependencies: + defu: 6.1.4 + mlly: 1.7.4 + ohash: 1.1.6 + pathe: 1.1.2 + ufo: 1.5.4 + unenv@2.0.0-rc.8: dependencies: defu: 6.1.4 @@ -16084,6 +16182,14 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20240718.0 '@cloudflare/workerd-windows-64': 1.20240718.0 + workerd@1.20250214.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250214.0 + '@cloudflare/workerd-darwin-arm64': 1.20250214.0 + '@cloudflare/workerd-linux-64': 1.20250214.0 + '@cloudflare/workerd-linux-arm64': 1.20250214.0 + '@cloudflare/workerd-windows-64': 1.20250214.0 + workerd@1.20250224.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20250224.0 @@ -16092,6 +16198,25 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20250224.0 '@cloudflare/workerd-windows-64': 1.20250224.0 + wrangler@3.112.0(@cloudflare/workers-types@4.20250303.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + esbuild: 0.17.19 + miniflare: 3.20250214.2 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.1 + workerd: 1.20250214.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20250303.0 + fsevents: 2.3.3 + sharp: 0.33.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrangler@3.114.0(@cloudflare/workers-types@4.20250303.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 diff --git a/src/aws/gateway.ts b/src/aws/gateway.ts index 7c6bfadf..9774aca3 100644 --- a/src/aws/gateway.ts +++ b/src/aws/gateway.ts @@ -1,7 +1,6 @@ -import { BuildURI, CoerceURI, exception2Result, KeyedResolvOnce, Logger, param, Result, URI } from "@adviser/cement"; +import { BuildURI, CoerceURI, exception2Result, KeyedResolvOnce, Logger, param, Result, to_uint8, URI } from "@adviser/cement"; import { bs, getStore, NotFoundError, SuperThis, ensureSuperLog } from "@fireproof/core"; import { AddKeyToDbMetaGateway } from "../meta-key-hack.js"; -import { to_uint8 } from "../coerce-binary.js"; async function resultFetch(logger: Logger, curl: CoerceURI, init?: RequestInit): Promise> { const url = URI.from(curl); diff --git a/src/coerce-binary.ts b/src/coerce-binary.ts deleted file mode 100644 index e9106db7..00000000 --- a/src/coerce-binary.ts +++ /dev/null @@ -1,42 +0,0 @@ -export async function top_uint8( - input: string | ArrayBufferLike | ArrayBufferView | Uint8Array | SharedArrayBuffer | Blob -): Promise { - if (input instanceof Blob) { - return new Uint8Array(await input.arrayBuffer()); - } - return to_uint8(input); -} - -export function to_uint8( - input: string | ArrayBufferLike | ArrayBufferView | Uint8Array | SharedArrayBuffer -): Uint8Array { - if (typeof input === "string") { - // eslint-disable-next-line no-restricted-globals - return new TextEncoder().encode(input); - } - if (input instanceof ArrayBuffer || input instanceof SharedArrayBuffer) { - return new Uint8Array(input); - } - - if (input instanceof Uint8Array) { - return input; - } - // not nice but we make the cloudflare types happy - return new Uint8Array(input as unknown as ArrayBufferLike); -} - -export function to_blob(input: ArrayBufferLike | ArrayBufferView | Uint8Array | Blob): Blob { - if (input instanceof Blob) { - return input; - } - return new Blob([to_uint8(input)]); -} - -export function to_arraybuf(input: ArrayBufferLike | ArrayBufferView | Uint8Array): ArrayBuffer { - if (input instanceof ArrayBuffer) { - return input; - } - const u8 = to_uint8(input); - return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; - // return to_uint8(input).buffer; // as ArrayBuffer; -} diff --git a/src/netlify/backend/server.ts b/src/netlify/backend/server.ts index 33405f59..96b49aae 100644 --- a/src/netlify/backend/server.ts +++ b/src/netlify/backend/server.ts @@ -1,5 +1,5 @@ +import { to_blob } from "@adviser/cement"; import { getStore } from "@netlify/blobs"; -import { to_blob } from "../../coerce-binary.js"; // eslint-disable-next-line no-console console.log("fireproof edge function loaded netlify"); diff --git a/src/ucan/common.ts b/src/ucan/common.ts index 1f0a1e83..67b1690f 100644 --- a/src/ucan/common.ts +++ b/src/ucan/common.ts @@ -5,7 +5,7 @@ import { Block } from "multiformats/block"; import { CID } from "multiformats"; import type { Service } from "./types.js"; -import { to_arraybuf } from "../coerce-binary.js"; +import { to_arraybuf } from "@adviser/cement"; export function agentProofs( agent: Agent, diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index 57609713..15747a80 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -1,12 +1,11 @@ import { Hono } from "hono"; import { HonoServer } from "../hono-server.js"; import { defaultGestalt } from "../msg-types.js"; -import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle, mockGetAuthFactory } from "../test-helper.js"; +import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle, MockJWK, mockJWK } from "../test-helper.js"; import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; import { defaultMsgParams } from "../msger.js"; import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; import { BuildURI } from "@adviser/cement"; -import { SessionTokenService } from "../../sts-service/sts-service.js"; const sthis = ensureSuperThis(); const msgP = defaultMsgParams(sthis, { hasPersistent: true }); @@ -20,21 +19,13 @@ describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gat let gw: bs.Gateway; let unregister: () => void; let url: BuildURI; + let auth: MockJWK; beforeAll(async () => { - const keyPair = await SessionTokenService.generateKeyPair(); - const authFactory = await mockGetAuthFactory( - keyPair.strings.privateKey, - { - userId: "hello", - tenants: [], - ledgers: [], - }, - sthis - ); + auth = await mockJWK(); // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); - style = wsStyle(sthis, authFactory, port, msgP, my); + style = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port, keyPair.strings.publicKey).then((srv) => + server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => srv.once(app, port) ); unregister = registerFireproofCloudStoreProtocol("fireproof:"); diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index 69918fa5..16996032 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -8,6 +8,7 @@ import { param, MatchResult, ResolveOnce, + to_uint8, } from "@adviser/cement"; import { bs, ensureLogger, NotFoundError, SuperThis } from "@fireproof/core"; import { @@ -21,7 +22,6 @@ import { MsgWithError, ResSignedUrl, } from "../msg-types.js"; -import { to_uint8 } from "../../coerce-binary.js"; import { MsgConnected, MsgConnectedAuth, Msger, authTypeFromUri } from "../msger.js"; import { MsgIsResDelData, @@ -35,9 +35,9 @@ import { const VERSION = "v0.1-fp-cloud"; export interface StoreTypeGateway { - get(uri: URI, conn: Promise>): Promise>; - put(uri: URI, body: Uint8Array, conn: Promise>): Promise>; - delete(uri: URI, conn: Promise>): Promise>; + get(uri: URI, conn: AuthedConnection): Promise>; + put(uri: URI, body: Uint8Array, conn: AuthedConnection): Promise>; + delete(uri: URI, conn: AuthedConnection): Promise>; } abstract class BaseGateway { @@ -48,37 +48,33 @@ abstract class BaseGateway { this.logger = ensureLogger(sthis, module); } - abstract getConn(uri: URI, conn: MsgConnectedAuth): Promise>; - async get(uri: URI, prConn: Promise>): Promise> { - const rConn = await prConn; - if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in getConn").ResultError(); + abstract getConn(uri: URI, conn: AuthedConnection): Promise>; + async get(uri: URI, rConn: AuthedConnection): Promise> { + if (rConn.conn.isErr()) { + return this.logger.Error().Err(rConn.conn).Msg("Error in getConn").ResultError(); } - const conn = rConn.Ok(); // this.logger.Debug().Any("conn", conn.key).Msg("get"); - return this.getConn(uri, conn); + return this.getConn(uri, rConn); } - abstract putConn(uri: URI, body: Uint8Array, conn: MsgConnectedAuth): Promise>; - async put(uri: URI, body: Uint8Array, prConn: Promise>): Promise> { - const rConn = await prConn; - if (rConn.isErr()) { - return this.logger.Error().Err(rConn).Msg("Error in putConn").ResultError(); + abstract putConn(uri: URI, body: Uint8Array, conn: AuthedConnection): Promise>; + async put(uri: URI, body: Uint8Array, rConn: AuthedConnection): Promise> { + if (rConn.conn.isErr()) { + return this.logger.Error().Err(rConn.conn).Msg("Error in putConn").ResultError(); } - const conn = rConn.Ok(); // this.logger.Debug().Any("conn", conn.key).Msg("put"); - return this.putConn(uri, body, conn); + return this.putConn(uri, body, rConn); } - abstract delConn(uri: URI, conn: MsgConnectedAuth): Promise>; - async delete(uri: URI, prConn: Promise>): Promise> { - const rConn = await prConn; - if (rConn.isErr()) { + abstract delConn(uri: URI, conn: AuthedConnection): Promise>; + async delete(uri: URI, rConn: AuthedConnection): Promise> { + // const rConn = await prConn; + if (rConn.conn.isErr()) { return this.logger.Error().Err(rConn).Msg("Error in deleteConn").ResultError(); } - const conn = rConn.Ok(); + // const conn = rConn.Ok(); // this.logger.Debug().Any("conn", conn.key).Msg("del"); - return this.delConn(uri, conn); + return this.delConn(uri, rConn); } // prepareReqSignedUrl(type: string, method: HttpMethods, store: FPStoreTypes, uri: URI, conn: Connection): Result { @@ -99,7 +95,7 @@ abstract class BaseGateway { store: FPStoreTypes, waitForFn: (msg: MsgBase) => boolean, uri: URI, - conn: MsgConnectedAuth + conn: AuthedConnection ): Promise> { const rParams = uri.getParamsResult({ key: param.REQUIRED, @@ -138,10 +134,10 @@ abstract class BaseGateway { }, version: VERSION, } as ReqSignedUrl; - return conn.request(rsu, { waitFor: waitForFn }); + return conn.conn.Ok().request(rsu, { waitFor: waitForFn }); } - async putObject(uri: URI, uploadUrl: string, body: Uint8Array): Promise> { + async putObject(uri: URI, uploadUrl: string, body: Uint8Array, conn: AuthedConnection): Promise> { this.logger.Debug().Any("url", { uploadUrl, uri }).Msg("put-fetch-url"); const rUpload = await exception2Result(async () => fetch(uploadUrl, { method: "PUT", body })); if (rUpload.isErr()) { @@ -151,12 +147,12 @@ abstract class BaseGateway { return this.logger.Error().Url(uploadUrl, "uploadUrl").Http(rUpload.Ok()).Msg("Error in put fetch").ResultError(); } if (uri.getParam("testMode")) { - trackPuts.add(uri.toString()); + conn.citem.trackPuts.add(uri.toString()); } return Result.Ok(undefined); } - async getObject(uri: URI, downloadUrl: string): Promise> { + async getObject(uri: URI, downloadUrl: string, _conn: AuthedConnection): Promise> { this.logger.Debug().Any("url", { downloadUrl, uri }).Msg("get-fetch-url"); const rDownload = await exception2Result(async () => fetch(downloadUrl.toString(), { method: "GET" })); if (rDownload.isErr()) { @@ -177,7 +173,7 @@ abstract class BaseGateway { return Result.Ok(to_uint8(await download.arrayBuffer())); } - async delObject(uri: URI, deleteUrl: string): Promise> { + async delObject(uri: URI, deleteUrl: string, _conn: AuthedConnection): Promise> { this.logger.Debug().Any("url", { deleteUrl, uri }).Msg("get-fetch-url"); const rDelete = await exception2Result(async () => fetch(deleteUrl.toString(), { method: "DELETE" })); if (rDelete.isErr()) { @@ -198,7 +194,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { constructor(sthis: SuperThis) { super(sthis, "DataGateway"); } - async getConn(uri: URI, conn: MsgConnectedAuth): Promise> { + async getConn(uri: URI, conn: AuthedConnection): Promise> { // type: string, method: HttpMethods, store: FPStoreTypes, waitForFn: const rResSignedUrl = await this.getReqSignedUrl( "reqGetData", @@ -212,9 +208,9 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); } const { signedUrl: downloadUrl } = rResSignedUrl; - return this.getObject(uri, downloadUrl); + return this.getObject(uri, downloadUrl, conn); } - async putConn(uri: URI, body: Uint8Array, conn: MsgConnectedAuth): Promise> { + async putConn(uri: URI, body: Uint8Array, conn: AuthedConnection): Promise> { const rResSignedUrl = await this.getReqSignedUrl( "reqPutData", "PUT", @@ -227,9 +223,9 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); } const { signedUrl: uploadUrl } = rResSignedUrl; - return this.putObject(uri, uploadUrl, body); + return this.putObject(uri, uploadUrl, body, conn); } - async delConn(uri: URI, conn: MsgConnectedAuth): Promise> { + async delConn(uri: URI, conn: AuthedConnection): Promise> { const rResSignedUrl = await this.getReqSignedUrl( "reqDelData", "DELETE", @@ -242,7 +238,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); } const { signedUrl: deleteUrl } = rResSignedUrl; - return this.delObject(uri, deleteUrl); + return this.delObject(uri, deleteUrl, conn); } } @@ -252,7 +248,7 @@ class MetaGateway extends BaseGateway implements StoreTypeGateway { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getConn(uri: URI, conn: MsgConnectedAuth): Promise> { + async getConn(uri: URI, conn: AuthedConnection): Promise> { // const rkey = uri.getParamResult("key"); // if (rkey.isErr()) { // return Result.Err(rkey.Err()); @@ -280,7 +276,7 @@ class MetaGateway extends BaseGateway implements StoreTypeGateway { return Result.Ok(new Uint8Array()); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async putConn(uri: URI, body: Uint8Array, conn: MsgConnectedAuth): Promise> { + async putConn(uri: URI, body: Uint8Array, conn: AuthedConnection): Promise> { // const bodyRes = Result.Ok(body); // await bs.addCryptoKeyToGatewayMetaPayload(uri, this.sthis, body); // if (bodyRes.isErr()) { // return this.logger.Error().Err(bodyRes).Msg("Error in addCryptoKeyToGatewayMetaPayload").ResultError(); @@ -303,7 +299,7 @@ class MetaGateway extends BaseGateway implements StoreTypeGateway { return Result.Ok(undefined); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async delConn(uri: URI, conn: MsgConnectedAuth): Promise> { + async delConn(uri: URI, conn: AuthedConnection): Promise> { // const rsu = this.prepareReqSignedUrl(uri, "DELETE", conn.key); // if (rsu.isErr()) { // return Result.Err(rsu.Err()); @@ -394,6 +390,12 @@ interface ConnectionItem { readonly connection: ResolveOnce>; readonly trackPuts: Set; } + +interface AuthedConnection { + readonly conn: Result; + readonly citem: ConnectionItem +} + // const keyedConnections = new KeyedResolvOnce(); interface Subscription { readonly sid: string; @@ -436,7 +438,7 @@ export class FireproofCloudGateway implements bs.Gateway { const matchURI = connectionURI(retURI); this.#connectionURIs.set(matchURI.toString(), { uri: matchURI, - matchRes: matchURI.match(uri), + matchRes: matchURI.match(matchURI), connection: new ResolveOnce>(), trackPuts: new Set(), }); @@ -444,12 +446,12 @@ export class FireproofCloudGateway implements bs.Gateway { } async get(uri: URI, sthis: SuperThis): Promise { - return getStoreTypeGateway(sthis, uri).get(uri, this.getCloudConnection(uri)); + return getStoreTypeGateway(sthis, uri).get(uri, await this.getCloudConnectionItem(uri)); } async put(uri: URI, body: Uint8Array, sthis: SuperThis): Promise> { const item = await this.getCloudConnectionItem(uri); - const ret = await getStoreTypeGateway(sthis, uri).put(uri, body, Promise.resolve(item.conn)); + const ret = await getStoreTypeGateway(sthis, uri).put(uri, body, item); if (ret.isOk()) { if (uri.getParam("testMode")) { item.citem.trackPuts.add(uri.toString()); @@ -461,7 +463,7 @@ export class FireproofCloudGateway implements bs.Gateway { async delete(uri: URI, sthis: SuperThis): Promise { const item = await this.getCloudConnectionItem(uri); item.citem.trackPuts.delete(uri.toString()); - return getStoreTypeGateway(sthis, uri).delete(uri, this.getCloudConnection(uri)); + return getStoreTypeGateway(sthis, uri).delete(uri, item); } async close(uri: URI): Promise { @@ -474,14 +476,14 @@ export class FireproofCloudGateway implements bs.Gateway { } } } - const rConn = await this.getCloudConnection(uri); - if (rConn.isErr()) { + const rConn = await this.getCloudConnectionItem(uri); + if (rConn.conn.isErr()) { return this.logger.Error().Err(rConn).Msg("Error in getCloudConnection").ResultError(); } - const conn = rConn.Ok(); + const conn = rConn.conn.Ok(); const rAuth = await conn.msgConnAuth(); await conn.close(rAuth.Ok()); - this.#connectionURIs.delete(connectionURI(uri).toString()); + this.#connectionURIs.delete(rConn.citem.uri.toString()); return Result.Ok(undefined); } @@ -491,13 +493,21 @@ export class FireproofCloudGateway implements bs.Gateway { return r.conn; }); } - async getCloudConnectionItem(uri: URI): Promise<{ conn: Result; citem: ConnectionItem }> { + + async getCloudConnectionItem(uri: URI): Promise { const matchURI = connectionURI(uri); - const rConn = this.#connectionURIs.get(matchURI.toString()); - if (!rConn) { - return { conn: this.logger.Error().Url(uri).Msg("No connection found").ResultError(), citem: {} as ConnectionItem }; + let bestMatch: ConnectionItem | undefined; + for (const ci of this.#connectionURIs.values()) { + const mci = ci.uri.match(matchURI); + if (mci.score >= ci.matchRes.score) { + bestMatch = ci; + break; + } + } + if (!bestMatch) { + return { conn: this.logger.Error().Url(matchURI).Msg("No connection found").ResultError(), citem: {} as ConnectionItem }; } - const conn = await rConn.connection.once(async () => { + const conn = await bestMatch.connection.once(async () => { const rParams = uri.getParamsResult({ name: param.REQUIRED, protocol: "https", @@ -539,9 +549,9 @@ export class FireproofCloudGateway implements bs.Gateway { return Msger.connect(this.sthis, cUrl, qOpen); }); if (conn.isErr()) { - return { conn: Result.Err(conn), citem: rConn }; + return { conn: Result.Err(conn), citem: bestMatch }; } - return { conn: Result.Ok(conn.Ok().attachAuth(() => authTypeFromUri(this.logger, uri))), citem: rConn }; + return { conn: Result.Ok(conn.Ok().attachAuth(() => authTypeFromUri(this.logger, uri))), citem: bestMatch }; // keyedConnections.get(keyTenantLedger(qOpen.conn.key)).once(async () => Msger.open(this.sthis, cUrl, qOpen)); } diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index 94830060..b0439d7b 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -1,5 +1,5 @@ import { ensureSuperThis } from "@fireproof/core"; -import { URI } from "@adviser/cement"; +import { Result, URI } from "@adviser/cement"; import { buildReqGestalt, buildReqOpen, @@ -29,7 +29,7 @@ import { MsgIsResPutWAL, MsgIsResDelWAL, } from "./msg-types-wal.js"; -import { applyStart, defaultMsgParams, MsgConnected, Msger } from "./msger.js"; +import { applyStart, defaultMsgParams, MsgConnected, MsgConnectedAuth, Msger } from "./msger.js"; import { HonoServer } from "./hono-server.js"; import { Hono } from "hono"; import { calculatePreSignedUrl } from "./pre-signed-url.js"; @@ -54,7 +54,6 @@ import { MsgIsEventGetMeta, MsgIsResPutMeta, } from "./msg-type-meta.js"; -import { SessionTokenService } from "../sts-service/sts-service.js"; async function refURL(sp: ResOptionalSignedUrl) { const { env } = await resolveToml("D1"); @@ -110,14 +109,13 @@ describe("Connection", () => { }); describe.each(styles)(`${honoServer.name} - $name`, (style) => { - const authFactory = style.authFactory; let server: HonoServer; let qOpen: ReqOpen; beforeAll(async () => { const app = new Hono(); - qOpen = buildReqOpen(sthis, await authFactory(), { reqId: "req-open-test" }); + qOpen = buildReqOpen(sthis, auth.authType, { reqId: "req-open-test" }); server = await honoServer - .factory(sthis, msgP, style.remoteGestalt, port, pubEnvJWK) + .factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey) .then((srv) => srv.once(app, port)); }); afterAll(async () => { @@ -137,27 +135,27 @@ describe("Connection", () => { }); describe(`connection`, () => { - let c: MsgConnected; + let c: MsgConnectedAuth; beforeEach(async () => { const rC = await style.ok .open() - .then((r) => MsgConnected.connect(authFactory, r, { reqId: "req-open-testx" })); + .then((r) => MsgConnected.connect(auth.applyAuthToURI('http://test'), r, { reqId: "req-open-testx" })); expect(rC.isOk()).toBeTruthy(); - c = rC.Ok(); + c = rC.Ok().attachAuth(() => Promise.resolve(Result.Ok(auth.authType))); expect(c.conn).toEqual({ reqId: "req-open-testx", resId: c.conn.resId, }); }); afterEach(async () => { - await c.close(); + await c.close((await c.msgConnAuth()).Ok()); }); it("kaputt url http", async () => { const r = await c.raw.request( { tid: "test", - auth: await authFactory(), + auth: auth.authType, type: "kaputt", version: "FP-MSG-1.0", }, @@ -181,7 +179,7 @@ describe("Connection", () => { }); it("gestalt url http", async () => { const msgP = defaultMsgParams(sthis, {}); - const req = buildReqGestalt(sthis, await authFactory(), defaultGestalt(msgP, { id: "test" })); + const req = buildReqGestalt(sthis, auth.authType, defaultGestalt(msgP, { id: "test" })); const r = await c.raw.request(req, { waitFor: MsgIsResGestalt }); if (!MsgIsResGestalt(r)) { assert.fail("expected MsgError", JSON.stringify(r)); @@ -190,7 +188,7 @@ describe("Connection", () => { }); it("openConnection", async () => { - const req = buildReqOpen(sthis, await authFactory(), { ...c.conn }); + const req = buildReqOpen(sthis, auth.authType, { ...c.conn }); const r = await c.raw.request(req, { waitFor: MsgIsResOpen }); if (!MsgIsResOpen(r)) { assert.fail(JSON.stringify(r)); @@ -205,11 +203,11 @@ describe("Connection", () => { }); it("open", async () => { - const rC = await Msger.connect(sthis, authFactory, URI.from(`http://localhost:${port}/fp`), msgP, { + const rC = await Msger.connect(sthis, auth.applyAuthToURI(`http://localhost:${port}/fp`), msgP, { reqId: "req-open-testy", }); expect(rC.isOk()).toBeTruthy(); - const c = rC.Ok(); + const c = rC.Ok().attachAuth(() => Promise.resolve(Result.Ok(auth.authType))); expect(c.conn).toEqual({ reqId: "req-open-testy", resId: c.conn.resId, @@ -219,15 +217,15 @@ describe("Connection", () => { my, remote: style.remoteGestalt, }); - await c.close(); + await c.close((await c.msgConnAuth()).Ok()); }); describe(`${honoServer.name} - Msgs`, () => { let gwCtx: GwCtx; - let conn: MsgConnected; + let conn: MsgConnectedAuth; beforeAll(async () => { - const rC = await Msger.connect(sthis, authFactory, URI.from(`http://localhost:${port}/fp`), msgP, qOpen.conn); + const rC = await Msger.connect(sthis, auth.applyAuthToURI(`http://localhost:${port}/fp`), msgP, qOpen.conn); expect(rC.isOk()).toBeTruthy(); - conn = rC.Ok(); + conn = rC.Ok().attachAuth(() => Promise.resolve(Result.Ok(auth.authType))); gwCtx = { conn: conn.conn, tenant: { @@ -237,10 +235,10 @@ describe("Connection", () => { }; }); afterAll(async () => { - await conn.close(); + await conn.close((await conn.msgConnAuth()).Ok()); }); it("Open", async () => { - const res = await conn.raw.request(buildReqOpen(sthis, await authFactory(), conn.conn), { + const res = await conn.raw.request(buildReqOpen(sthis, auth.authType, conn.conn), { waitFor: MsgIsResOpen, }); if (!MsgIsResOpen(res)) { @@ -293,11 +291,10 @@ describe("Connection", () => { it("bind stop", async () => { const sp = sup(); expect(conn.raw.activeBinds.size).toBe(0); - const auth = await authFactory(); const streams: ReadableStream>[] = Array(5) .fill(0) .map(() => { - return conn.bind(buildBindGetMeta(sthis, auth, sp, gwCtx), { + return conn.bind(buildBindGetMeta(sthis, auth.authType, sp, gwCtx), { waitFor: MsgIsEventGetMeta, }); }); @@ -323,7 +320,7 @@ describe("Connection", () => { it("Get", async () => { const sp = sup(); - const res = await conn.request(buildBindGetMeta(sthis, await authFactory(), sp, gwCtx), { + const res = await conn.request(buildBindGetMeta(sthis, auth.authType, sp, gwCtx), { waitFor: MsgIsEventGetMeta, }); if (MsgIsEventGetMeta(res)) { @@ -340,7 +337,7 @@ describe("Connection", () => { .map((data) => { return { ...data, cid: sthis.timeOrderedNextId().str }; }); - const res = await conn.request(buildReqPutMeta(sthis, await authFactory(), sp, metas, gwCtx), { + const res = await conn.request(buildReqPutMeta(sthis, auth.authType, sp, metas, gwCtx), { waitFor: MsgIsResPutMeta, }); if (MsgIsResPutMeta(res)) { @@ -353,7 +350,7 @@ describe("Connection", () => { it("Del", async () => { const sp = sup(); const res = await conn.request( - buildReqDelMeta(sthis, await authFactory(), sp, gwCtx), + buildReqDelMeta(sthis, auth.authType, sp, gwCtx), { waitFor: MsgIsResDelMeta, } diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index f50b4668..8eebeb0e 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -1,6 +1,5 @@ -import { exception2Result, HttpHeader, Logger, param, Result, URI } from "@adviser/cement"; +import { exception2Result, HttpHeader, Logger, param, Result, top_uint8, URI } from "@adviser/cement"; import { Context, Hono, Next } from "hono"; -import { top_uint8 } from "../coerce-binary.js"; import { buildErrorMsg, MsgBase, diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 897bb44c..1a11da49 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -10,6 +10,7 @@ import { MsgIsResGestalt, MsgIsError, MsgBase, + FPJWKCloudAuthType, } from "./msg-types.js"; import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection, authTypeFromUri } from "./msger.js"; import { WSConnection } from "./ws-connection.js"; @@ -23,6 +24,7 @@ import { envKeyDefaults, KeysResult, SessionTokenService, TokenForParam } from " export interface MockJWK { keys: KeysResult; + authType: FPJWKCloudAuthType applyAuthToURI: (uri: CoerceURI) => URI; } export async function mockJWK(claim: Partial = {}): Promise { @@ -40,6 +42,12 @@ export async function mockJWK(claim: Partial = {}): Promise BuildURI.from(uri).setParam("authJWK", jwk).URI() } } diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts index e6b2cc40..b2dd8ff3 100644 --- a/src/v2-cloud/ws-sockets.test.ts +++ b/src/v2-cloud/ws-sockets.test.ts @@ -1,12 +1,11 @@ import { ensureSuperThis } from "@fireproof/core"; -import { CFHonoServerFactory, mockGetAuthFactory, NodeHonoServerFactory, wsStyle } from "./test-helper.js"; +import { CFHonoServerFactory, MockJWK, mockJWK, NodeHonoServerFactory, wsStyle } from "./test-helper.js"; import { defaultMsgParams, Msger } from "./msger.js"; -import { AuthFactory, buildReqChat, defaultGestalt, MsgIsResChat } from "./msg-types.js"; +import { buildReqChat, defaultGestalt, MsgIsResChat } from "./msg-types.js"; import { Hono } from "hono"; import { HonoServer } from "./hono-server.js"; -import { Future } from "@adviser/cement"; -import { SessionTokenService } from "../sts-service/sts-service.js"; +import { Future, Result } from "@adviser/cement"; describe("test multiple connections", () => { const sthis = ensureSuperThis(); @@ -25,23 +24,24 @@ describe("test multiple connections", () => { let hserv: HonoServer; - let authFactory: AuthFactory; + let auth: MockJWK beforeAll(async () => { - const pair = await SessionTokenService.generateKeyPair(); - authFactory = await mockGetAuthFactory( - pair.strings.privateKey, - { - userId: "hello", - tenants: [], - ledgers: [], - }, - sthis - ); - stype = wsStyle(sthis, authFactory, port, msgP, my); + auth = await mockJWK(); + // const pair = await SessionTokenService.generateKeyPair(); + // authFactory = await mockGetAuthFactory( + // pair.strings.privateKey, + // { + // userId: "hello", + // tenants: [], + // ledgers: [], + // }, + // sthis + // ); + stype = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); const app = new Hono(); - hserv = await factory(sthis, msgP, stype.remoteGestalt, port, pair.strings.publicKey).then((srv) => + hserv = await factory(sthis, msgP, stype.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => srv.once(app, port) ); }); @@ -54,9 +54,9 @@ describe("test multiple connections", () => { Array(connections) .fill(0) .map(() => { - return Msger.connect(sthis, authFactory, "http://localhost:" + port + "/fp"); + return Msger.connect(sthis, auth.applyAuthToURI("http://localhost:" + port + "/fp")); }) - ).then((cs) => cs.map((c) => c.Ok())); + ).then((cs) => cs.map((c) => c.Ok().attachAuth((() => Promise.resolve(Result.Ok(auth.authType)))))); const ready = new Future(); let total = (connections * (connections + 1)) / 2; @@ -79,8 +79,9 @@ describe("test multiple connections", () => { const rest = [...conns]; for (const c of conns) { + // console.log("Sending a chat request", rest.length, conns.length); - const act = await c.request(buildReqChat(sthis, await authFactory(), c.conn, "Hello"), { + const act = await c.request(buildReqChat(sthis, auth.authType, c.conn, "Hello"), { waitFor: MsgIsResChat, }); if (MsgIsResChat(act)) { @@ -88,7 +89,7 @@ describe("test multiple connections", () => { } else { assert.fail("Expected a response"); } - await c.close(); + await c.close((await c.msgConnAuth()).Ok()); rest.shift(); } diff --git a/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts b/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts index 50049f45..5476529f 100644 --- a/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts +++ b/tests/connect-netlify/app/netlify/edge-functions/fireproof.ts @@ -1,5 +1,5 @@ +import { to_blob } from "@adviser/cement"; import { getStore } from "@netlify/blobs"; -import { to_blob } from "../../../../../src/coerce-binary.js"; // eslint-disable-next-line no-console console.log("fireproof edge function loaded netlify"); From 1de0dc0353c3368ccf06e3467eeb7eaeb2c7832e Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Wed, 12 Mar 2025 10:24:56 +0100 Subject: [PATCH 11/14] chore: added auth transfer between client - backend --- .gitignore | 1 + package.json | 4 +- pnpm-lock.yaml | 11 -- src/aws/gateway.ts | 12 +- src/sts-service/create-key-pair.ts | 5 +- src/v2-cloud/client/cloud-gateway.test.ts | 225 ++++++++++++---------- src/v2-cloud/client/gateway.ts | 24 ++- src/v2-cloud/connection.test.ts | 76 ++++---- src/v2-cloud/hono-server.ts | 6 +- src/v2-cloud/http-connection.ts | 7 +- src/v2-cloud/msg-type-meta.ts | 26 ++- src/v2-cloud/msg-types-data.ts | 24 ++- src/v2-cloud/msg-types-wal.ts | 19 +- src/v2-cloud/msg-types.ts | 45 +++-- src/v2-cloud/msger.ts | 37 ++-- src/v2-cloud/pre-signed-url.ts | 7 +- src/v2-cloud/test-helper.ts | 44 +++-- src/v2-cloud/ws-connection.ts | 7 +- src/v2-cloud/ws-sockets.test.ts | 7 +- 19 files changed, 327 insertions(+), 260 deletions(-) diff --git a/.gitignore b/.gitignore index 738c10ab..2d5c460e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ smoke/react/package.json # Local PartyKit folder .partykit +.dev.vars* diff --git a/package.json b/package.json index 5a2b4254..bb56050b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "private": true, "description": "Live database connector for the web", "type": "module", + "engines": { + "node": ">=20" + }, "scripts": { "xpublish": "pnpm run '/publish:/'", "xpublish:aws": "tsx ./publish-package.ts ./dist/aws/package.json", @@ -58,7 +61,6 @@ "@smithy/types": "^4.1.0", "@types/aws-lambda": "^8.10.147", "@types/better-sqlite3": "^7.6.12", - "@types/eslint__js": "^9.14.0", "@types/node": "^22.13.10", "@types/wait-on": "^5.3.4", "@types/ws": "^8.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a7f5609..94cd5394 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,9 +177,6 @@ importers: '@types/better-sqlite3': specifier: ^7.6.12 version: 7.6.12 - '@types/eslint__js': - specifier: ^9.14.0 - version: 9.14.0 '@types/node': specifier: ^22.13.10 version: 22.13.10 @@ -2462,10 +2459,6 @@ packages: '@types/better-sqlite3@7.6.12': resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} - '@types/eslint__js@9.14.0': - resolution: {integrity: sha512-s0jepCjOJWB/GKcuba4jISaVpBudw3ClXJ3fUK4tugChUMQsp6kSwuA8Dcx6wFd/JsJqcY8n4rEpa5RTHs5ypA==} - deprecated: This is a stub types definition. @eslint/js provides its own type definitions, so you do not need this installed. - '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -10209,10 +10202,6 @@ snapshots: dependencies: '@types/node': 22.13.10 - '@types/eslint__js@9.14.0': - dependencies: - '@eslint/js': 9.22.0 - '@types/estree@1.0.6': {} '@types/http-cache-semantics@4.0.4': {} diff --git a/src/aws/gateway.ts b/src/aws/gateway.ts index 9774aca3..ba99cc3c 100644 --- a/src/aws/gateway.ts +++ b/src/aws/gateway.ts @@ -1,4 +1,14 @@ -import { BuildURI, CoerceURI, exception2Result, KeyedResolvOnce, Logger, param, Result, to_uint8, URI } from "@adviser/cement"; +import { + BuildURI, + CoerceURI, + exception2Result, + KeyedResolvOnce, + Logger, + param, + Result, + to_uint8, + URI, +} from "@adviser/cement"; import { bs, getStore, NotFoundError, SuperThis, ensureSuperLog } from "@fireproof/core"; import { AddKeyToDbMetaGateway } from "../meta-key-hack.js"; diff --git a/src/sts-service/create-key-pair.ts b/src/sts-service/create-key-pair.ts index be9ca1dc..dd5367ad 100644 --- a/src/sts-service/create-key-pair.ts +++ b/src/sts-service/create-key-pair.ts @@ -1,6 +1,7 @@ -import { env2jwk, envKeyDefaults, SessionTokenService } from "./sts-service.js"; +// deno run --allow-env --unstable-sloppy-imports /Users/menabe/Software/fproof/connect/src/sts-service/create-key-pair.ts +import { envKeyDefaults, SessionTokenService } from "./sts-service.js"; -const { strings, material } = await SessionTokenService.generateKeyPair(); +const { strings } = await SessionTokenService.generateKeyPair(); // console.log(">", await exportJWK(privateKey)) diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index 15747a80..b14eb518 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -5,125 +5,136 @@ import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle, MockJWK, mockJWK } import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; import { defaultMsgParams } from "../msger.js"; import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; -import { BuildURI } from "@adviser/cement"; +import { BuildURI, URI } from "@adviser/cement"; -const sthis = ensureSuperThis(); -const msgP = defaultMsgParams(sthis, { hasPersistent: true }); -const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - -describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1")])("$name - Gateway", ({ factory }) => { - const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); - let style; - - let server: HonoServer; - let gw: bs.Gateway; - let unregister: () => void; - let url: BuildURI; +describe("test multiple connections", () => { + const sthis = ensureSuperThis(); + const msgP = defaultMsgParams(sthis, { hasPersistent: true }); + const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); let auth: MockJWK; + beforeAll(async () => { auth = await mockJWK(); - // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); - style = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); - const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => - srv.once(app, port) - ); - unregister = registerFireproofCloudStoreProtocol("fireproof:"); - gw = new FireproofCloudGateway(sthis); - url = BuildURI.from(`fireproof://localhost:${port}/`) - .setParam("protocol", "http") - .setParam("name", "ledger-name") - .setParam("tenant", "tendant"); - }); - afterAll(async () => { - await server.close(); - unregister(); }); - describe("data", () => { - it("get not found", async () => { - await Promise.all( - Array(20) - .fill(async () => { - url.setParam("store", "data"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - it("put - get - del - get", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const resStart = await gw.start(url.URI(), sthis); - expect(resStart.isOk()).toBeTruthy(); + describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1"), CFHonoServerFactory("DO")])( + "$name - Gateway", + ({ factory }) => { + const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); + let style; - url.setParam("store", "data"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); + let server: HonoServer; + let gw: bs.Gateway; + let unregister: () => void; + let url: URI; + beforeAll(async () => { + // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); + style = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); + const app = new Hono(); + server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => + srv.once(app, port) + ); + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + gw = new FireproofCloudGateway(sthis); + const lurl = auth.applyAuthToURI( + BuildURI.from(`fireproof://localhost:${port}/`) + .setParam("protocol", "http") + .setParam("name", "ledger-name") + .setParam("tenant", "tendant") + ); + url = (await gw.start(lurl, sthis)).Ok(); + }); + afterAll(async () => { + await server.close(); + unregister(); + }); + describe("data", () => { + it("get not found", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const my = url.build().setParam("store", "data").URI(); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my, key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl, sthis); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl, sthis); - expect(resDel.isOk()).toBeTruthy(); + it("put - get - del - get", async () => { + await Promise.all( + Array(1) + .fill(async () => { + const resStart = await gw.start(url, sthis); + expect(resStart.isOk()).toBeTruthy(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - }); + const my = url.build().setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - describe("WAL", () => { - it("get not found", async () => { - await Promise.all( - Array(20) - .fill(async () => { - url.setParam("store", "wal"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl, sthis); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl, sthis); + expect(resDel.isOk()).toBeTruthy(); - it("put - get - del - get", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const resStart = await gw.start(url.URI(), sthis); - expect(resStart.isOk()).toBeTruthy(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + }); - url.setParam("store", "wal"); - const key = `theWALKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(url.URI(), key, sthis)).Ok(); + describe("WAL", () => { + it("get not found", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const my = url.build().setParam("store", "wal"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl, sthis); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl, sthis); - expect(resDel.isOk()).toBeTruthy(); + it("put - get - del - get", async () => { + await Promise.all( + Array(20) + .fill(async () => { + const resStart = await gw.start(url, sthis); + expect(resStart.isOk()).toBeTruthy(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - }); + const my = url.build().setParam("store", "wal"); + const key = `theWALKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl, sthis); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl, sthis); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + }); + } + ); }); diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index 16996032..0825f51c 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -126,9 +126,11 @@ abstract class BaseGateway { ledger: params.name, }, // tenant: conn.tenant, - params: { + methodParams: { method, store, + }, + params: { ...params, key: params.key, }, @@ -392,8 +394,8 @@ interface ConnectionItem { } interface AuthedConnection { - readonly conn: Result; - readonly citem: ConnectionItem + readonly conn: Result; + readonly citem: ConnectionItem; } // const keyedConnections = new KeyedResolvOnce(); @@ -404,16 +406,15 @@ interface Subscription { readonly unsub: () => void; } function connectionURI(uri: URI): URI { - return uri.build().delParam("authJWK").URI(); + return uri.build().delParam("authJWK").delParam("key").delParam("store").URI(); } - const subscriptions = new Map(); // const doServerSubscribe = new KeyedResolvOnce(); export class FireproofCloudGateway implements bs.Gateway { readonly logger: Logger; readonly sthis: SuperThis; - readonly #connectionURIs = new Map< string, ConnectionItem >(); + readonly #connectionURIs = new Map(); constructor(sthis: SuperThis) { this.sthis = sthis; @@ -498,14 +499,17 @@ export class FireproofCloudGateway implements bs.Gateway { const matchURI = connectionURI(uri); let bestMatch: ConnectionItem | undefined; for (const ci of this.#connectionURIs.values()) { - const mci = ci.uri.match(matchURI); + const mci = ci.uri.match(matchURI); if (mci.score >= ci.matchRes.score) { - bestMatch = ci; + bestMatch = ci; break; } } if (!bestMatch) { - return { conn: this.logger.Error().Url(matchURI).Msg("No connection found").ResultError(), citem: {} as ConnectionItem }; + return { + conn: this.logger.Error().Url(matchURI).Msg("No connection found").ResultError(), + citem: {} as ConnectionItem, + }; } const conn = await bestMatch.connection.once(async () => { const rParams = uri.getParamsResult({ @@ -546,7 +550,7 @@ export class FireproofCloudGateway implements bs.Gateway { if (cUrl.pathname === "/") { cUrl = cUrl.build().pathname("/fp").URI(); } - return Msger.connect(this.sthis, cUrl, qOpen); + return Msger.connect(this.sthis, rAuth.Ok(), cUrl, qOpen); }); if (conn.isErr()) { return { conn: Result.Err(conn), citem: bestMatch }; diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index b0439d7b..9750678f 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -12,6 +12,7 @@ import { MsgWithError, ResOptionalSignedUrl, ReqOpen, + MethodSignedUrlParam, } from "./msg-types.js"; import { MsgIsResGetData, @@ -77,10 +78,12 @@ async function refURL(sp: ResOptionalSignedUrl) { describe("Connection", () => { const sthis = ensureSuperThis(); const msgP = defaultMsgParams(sthis, { hasPersistent: true }); + let auth: MockJWK; // let privEnvJWK: string beforeAll(async () => { sthis.env.sets((await resolveToml("D1")).env as unknown as Record); + auth = await mockJWK(); // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); }); @@ -93,25 +96,18 @@ describe("Connection", () => { const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - const styles: (ReturnType | ReturnType)[] = []; - let auth: MockJWK + const styles: { name: string; action: () => ReturnType | ReturnType }[] = [ + // force multiple lines + { name: "http", action: () => httpStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, + { name: "ws", action: () => wsStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, + ]; - beforeAll(async () => { - auth = await mockJWK(); - - styles.push( - ...[ - // force multiple lines - httpStyle(sthis, auth.applyAuthToURI, port, msgP, my), - wsStyle(sthis, auth.applyAuthToURI, port, msgP, my), - ] - ); - }); - - describe.each(styles)(`${honoServer.name} - $name`, (style) => { + describe.each(styles)(`${honoServer.name} - $name`, (styleFn) => { + let style: ReturnType | ReturnType; let server: HonoServer; let qOpen: ReqOpen; beforeAll(async () => { + style = styleFn.action(); const app = new Hono(); qOpen = buildReqOpen(sthis, auth.authType, { reqId: "req-open-test" }); server = await honoServer @@ -122,6 +118,7 @@ describe("Connection", () => { // console.log("closing server"); await server.close(); }); + it(`conn refused`, async () => { const rC = await applyStart(style.connRefused.open()); expect(rC.isErr()).toBeTruthy(); @@ -139,7 +136,7 @@ describe("Connection", () => { beforeEach(async () => { const rC = await style.ok .open() - .then((r) => MsgConnected.connect(auth.applyAuthToURI('http://test'), r, { reqId: "req-open-testx" })); + .then((r) => MsgConnected.connect(auth.authType, r, { reqId: "req-open-testx" })); expect(rC.isOk()).toBeTruthy(); c = rC.Ok().attachAuth(() => Promise.resolve(Result.Ok(auth.authType))); expect(c.conn).toEqual({ @@ -167,11 +164,13 @@ describe("Connection", () => { } expect(r).toEqual({ message: "unexpected message", + auth: auth.authType, tid: "test", type: "error", version: "FP-MSG-1.0", src: { tid: "test", + auth: auth.authType, type: "kaputt", version: "FP-MSG-1.0", }, @@ -195,6 +194,7 @@ describe("Connection", () => { } expect(r).toEqual({ conn: { ...c.conn, resId: r.conn?.resId }, + auth: auth.authType, tid: req.tid, type: "resOpen", version: "FP-MSG-1.0", @@ -203,7 +203,7 @@ describe("Connection", () => { }); it("open", async () => { - const rC = await Msger.connect(sthis, auth.applyAuthToURI(`http://localhost:${port}/fp`), msgP, { + const rC = await Msger.connect(sthis, auth.authType, `http://localhost:${port}/fp`, msgP, { reqId: "req-open-testy", }); expect(rC.isOk()).toBeTruthy(); @@ -217,13 +217,13 @@ describe("Connection", () => { my, remote: style.remoteGestalt, }); - await c.close((await c.msgConnAuth()).Ok()); + await c.close((await c.msgConnAuth()).Ok()); }); describe(`${honoServer.name} - Msgs`, () => { let gwCtx: GwCtx; let conn: MsgConnectedAuth; beforeAll(async () => { - const rC = await Msger.connect(sthis, auth.applyAuthToURI(`http://localhost:${port}/fp`), msgP, qOpen.conn); + const rC = await Msger.connect(sthis, auth.authType, `http://localhost:${port}/fp`, msgP, qOpen.conn); expect(rC.isOk()).toBeTruthy(); conn = rC.Ok().attachAuth(() => Promise.resolve(Result.Ok(auth.authType))); gwCtx = { @@ -248,15 +248,19 @@ describe("Connection", () => { expect(res.conn).toEqual({ ...qOpen.conn, resId: res.conn.resId }); }); - function sup() { + function sup(mp: MethodSignedUrlParam) { return { - path: "test/me", - key: "key-test", + auth: auth.authType, + methodParam: mp, + params: { + path: "test/me", + key: "key-test", + }, } satisfies ReqSignedUrlParam; } describe("Data", async () => { it("Get", async () => { - const sp = sup(); + const sp = sup({ method: "GET", store: "data" }); const res = await conn.request(buildReqGetData(sthis, sp, gwCtx), { waitFor: MsgIsResGetData }); if (MsgIsResGetData(res)) { // expect(res.params).toEqual(sp); @@ -266,7 +270,7 @@ describe("Connection", () => { } }); it("Put", async () => { - const sp = sup(); + const sp = sup({ method: "PUT", store: "data" }); const res = await conn.request(buildReqPutData(sthis, sp, gwCtx), { waitFor: MsgIsResPutData }); if (MsgIsResPutData(res)) { // expect(res.params).toEqual(sp); @@ -276,7 +280,7 @@ describe("Connection", () => { } }); it("Del", async () => { - const sp = sup(); + const sp = sup({ method: "DELETE", store: "data" }); const res = await conn.request(buildReqDelData(sthis, sp, gwCtx), { waitFor: MsgIsResDelData }); if (MsgIsResDelData(res)) { // expect(res.params).toEqual(sp); @@ -289,12 +293,12 @@ describe("Connection", () => { describe("Meta", async () => { it("bind stop", async () => { - const sp = sup(); + const sp = sup({ method: "GET", store: "meta" }); expect(conn.raw.activeBinds.size).toBe(0); const streams: ReadableStream>[] = Array(5) .fill(0) .map(() => { - return conn.bind(buildBindGetMeta(sthis, auth.authType, sp, gwCtx), { + return conn.bind(buildBindGetMeta(sthis, auth.authType, sp.params, gwCtx), { waitFor: MsgIsEventGetMeta, }); }); @@ -319,8 +323,8 @@ describe("Connection", () => { }); it("Get", async () => { - const sp = sup(); - const res = await conn.request(buildBindGetMeta(sthis, auth.authType, sp, gwCtx), { + const sp = sup({ method: "GET", store: "meta" }); + const res = await conn.request(buildBindGetMeta(sthis, auth.authType, sp.params, gwCtx), { waitFor: MsgIsEventGetMeta, }); if (MsgIsEventGetMeta(res)) { @@ -331,13 +335,13 @@ describe("Connection", () => { } }); it("Put", async () => { - const sp = sup(); + const sp = sup({ method: "PUT", store: "meta" }); const metas = Array(5) .fill({ cid: "x", parents: [], data: "MomRkYXRho" }) .map((data) => { return { ...data, cid: sthis.timeOrderedNextId().str }; }); - const res = await conn.request(buildReqPutMeta(sthis, auth.authType, sp, metas, gwCtx), { + const res = await conn.request(buildReqPutMeta(sthis, auth.authType, sp.params, metas, gwCtx), { waitFor: MsgIsResPutMeta, }); if (MsgIsResPutMeta(res)) { @@ -348,9 +352,9 @@ describe("Connection", () => { } }); it("Del", async () => { - const sp = sup(); + const sp = sup({ method: "DELETE", store: "meta" }); const res = await conn.request( - buildReqDelMeta(sthis, auth.authType, sp, gwCtx), + buildReqDelMeta(sthis, auth.authType, sp.params, gwCtx), { waitFor: MsgIsResDelMeta, } @@ -365,7 +369,7 @@ describe("Connection", () => { }); describe("WAL", async () => { it("Get", async () => { - const sp = sup(); + const sp = sup({ method: "GET", store: "wal" }); const res = await conn.request(buildReqGetWAL(sthis, sp, gwCtx), { waitFor: MsgIsResGetWAL }); if (MsgIsResGetWAL(res)) { // expect(res.params).toEqual(sp); @@ -375,7 +379,7 @@ describe("Connection", () => { } }); it("Put", async () => { - const sp = sup(); + const sp = sup({ method: "PUT", store: "wal" }); const res = await conn.request(buildReqPutWAL(sthis, sp, gwCtx), { waitFor: MsgIsResPutWAL }); if (MsgIsResPutWAL(res)) { // expect(res.params).toEqual(sp); @@ -385,7 +389,7 @@ describe("Connection", () => { } }); it("Del", async () => { - const sp = sup(); + const sp = sup({ method: "DELETE", store: "wal" }); const res = await conn.request(buildReqDelWAL(sthis, sp, gwCtx), { waitFor: MsgIsResDelWAL }); if (MsgIsResDelWAL(res)) { // expect(res.params).toEqual(sp); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index 8eebeb0e..577ad466 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -138,7 +138,7 @@ export abstract class HonoServerBase implements HonoServerImpl { // } async handleReqPutMeta(ctx: MsgDispatcherCtx, msg: MsgWithConnAuth): Promise> { - const rUrl = await buildRes("PUT", "meta", "resPutMeta", ctx, msg, this); + const rUrl = await buildRes({ method: "PUT", store: "meta" }, "resPutMeta", ctx, msg, this); if (MsgIsError(rUrl)) { return rUrl; } @@ -150,7 +150,7 @@ export abstract class HonoServerBase implements HonoServerImpl { } async handleReqDelMeta(ctx: MsgDispatcherCtx, msg: MsgWithConnAuth): Promise> { - const rUrl = await buildRes("DELETE", "meta", "resDelMeta", ctx, msg, this); + const rUrl = await buildRes({ method: "DELETE", store: "meta" }, "resDelMeta", ctx, msg, this); if (MsgIsError(rUrl)) { return rUrl; } @@ -165,7 +165,7 @@ export abstract class HonoServerBase implements HonoServerImpl { msg: MsgWithConnAuth, gwCtx: GwCtx = msg ): Promise> { - const rMsg = await buildRes("GET", "meta", "eventGetMeta", ctx, msg, this); + const rMsg = await buildRes({ method: "GET", store: "meta" }, "eventGetMeta", ctx, msg, this); if (MsgIsError(rMsg)) { return rMsg; } diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts index 62db2486..8fc4e4dc 100644 --- a/src/v2-cloud/http-connection.ts +++ b/src/v2-cloud/http-connection.ts @@ -21,12 +21,7 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec readonly #onMsg = new Map(); - constructor( - sthis: SuperThis, - uris: URI[], - msgP: MsgerParamsWithEnDe, - exGestalt: ExchangedGestalt - ) { + constructor(sthis: SuperThis, uris: URI[], msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { super(sthis, exGestalt); this.logger = ensureLogger(sthis, "HttpConnection"); // this.msgParam = msgP; diff --git a/src/v2-cloud/msg-type-meta.ts b/src/v2-cloud/msg-type-meta.ts index 82fd38c7..09fa03a7 100644 --- a/src/v2-cloud/msg-type-meta.ts +++ b/src/v2-cloud/msg-type-meta.ts @@ -6,17 +6,19 @@ import { MsgBase, MsgWithTenantLedger, NextId, - ReqSignedUrlParam, ResOptionalSignedUrl, MsgTypesCtx, MsgWithOptionalConnAuth, MsgWithConnAuth, + SignedUrlParam, + MethodSignedUrlParam, } from "./msg-types.js"; /* Put Meta */ export interface ReqPutMeta extends MsgWithTenantLedger { readonly type: "reqPutMeta"; - readonly params: ReqSignedUrlParam; + readonly methodParams: MethodSignedUrlParam; + readonly params: SignedUrlParam; readonly metas: CRDTEntry[]; } @@ -27,7 +29,7 @@ export interface ResPutMeta extends MsgWithTenantLedger, QSMeta export function buildReqPutMeta( sthis: NextId, auth: AuthType, - signedUrlParams: ReqSignedUrlParam, + signedUrlParams: SignedUrlParam, metas: CRDTEntry[], gwCtx: GwCtx ): ReqPutMeta { @@ -37,6 +39,10 @@ export function buildReqPutMeta( type: "reqPutMeta", ...gwCtx, version: VERSION, + methodParams: { + method: "PUT", + store: "meta", + }, params: signedUrlParams, metas, }; @@ -69,7 +75,7 @@ export function MsgIsResPutMeta(qs: MsgBase): qs is ResPutMeta { /* Bind Meta */ export interface BindGetMeta extends MsgWithTenantLedger { readonly type: "bindGetMeta"; - readonly params: ReqSignedUrlParam; + readonly params: SignedUrlParam; } export function MsgIsBindGetMeta(msg: MsgBase): msg is BindGetMeta { @@ -85,7 +91,7 @@ export interface EventGetMeta extends MsgWithTenantLedger, ResO readonly type: "eventGetMeta"; } -export function buildBindGetMeta(sthis: NextId, auth: AuthType, params: ReqSignedUrlParam, gwCtx: GwCtx): BindGetMeta { +export function buildBindGetMeta(sthis: NextId, auth: AuthType, params: SignedUrlParam, gwCtx: GwCtx): BindGetMeta { return { auth, tid: sthis.nextId().str, @@ -107,7 +113,8 @@ export function buildEventGetMeta( ...gwCtx, tid: req.tid, type: "eventGetMeta", - params: { ...req.params, method: "GET", store: "meta" }, + params: req.params, + methodParams: { method: "GET", store: "meta" }, version: VERSION, }; } @@ -119,13 +126,13 @@ export function MsgIsEventGetMeta(qs: MsgBase): qs is EventGetMeta { /* Del Meta */ export interface ReqDelMeta extends MsgWithTenantLedger { readonly type: "reqDelMeta"; - readonly params: ReqSignedUrlParam; + readonly params: SignedUrlParam; } export function buildReqDelMeta( sthis: NextId, auth: AuthType, - signedUrlParams: ReqSignedUrlParam, + signedUrlParams: SignedUrlParam, gwCtx: GwCtx ): ReqDelMeta { return { @@ -153,7 +160,8 @@ export function buildResDelMeta( ): ResDelMeta { return { auth: req.auth, - params: { ...req.params, method: "DELETE", store: "meta" }, + params: req.params, + methodParams: { method: "DELETE", store: "meta" }, signedUrl, tid: req.tid, conn: req.conn, diff --git a/src/v2-cloud/msg-types-data.ts b/src/v2-cloud/msg-types-data.ts index f03b697b..36b3728f 100644 --- a/src/v2-cloud/msg-types-data.ts +++ b/src/v2-cloud/msg-types-data.ts @@ -45,7 +45,13 @@ export function buildResGetData( req: MsgWithConnAuth, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResGetData>("GET", "data", "resGetData", msgCtx, req, ctx); + return buildRes, ResGetData>( + { method: "GET", store: "data" }, + "resGetData", + msgCtx, + req, + ctx + ); } export interface ReqPutData extends ReqSignedUrl { @@ -74,7 +80,13 @@ export function buildResPutData( req: MsgWithConnAuth, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResPutData>("PUT", "data", "resPutData", msgCtx, req, ctx); + return buildRes, ResPutData>( + { method: "PUT", store: "data" }, + "resPutData", + msgCtx, + req, + ctx + ); } export interface ReqDelData extends ReqSignedUrl { @@ -102,5 +114,11 @@ export function buildResDelData( req: MsgWithConnAuth, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes, ResDelData>("DELETE", "data", "resDelData", msgCtx, req, ctx); + return buildRes, ResDelData>( + { method: "DELETE", store: "data" }, + "resDelData", + msgCtx, + req, + ctx + ); } diff --git a/src/v2-cloud/msg-types-wal.ts b/src/v2-cloud/msg-types-wal.ts index 07c8c06c..4b777499 100644 --- a/src/v2-cloud/msg-types-wal.ts +++ b/src/v2-cloud/msg-types-wal.ts @@ -41,7 +41,13 @@ export function buildResGetWAL( req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResGetWAL>("GET", "wal", "resGetWAL", msgCtx, req, ctx); + return buildRes>, ResGetWAL>( + { method: "GET", store: "wal" }, + "resGetWAL", + msgCtx, + req, + ctx + ); } export interface ReqPutWAL extends Omit { @@ -70,7 +76,13 @@ export function buildResPutWAL( req: MsgWithTenantLedger>, ctx: CalculatePreSignedUrl ): Promise> { - return buildRes>, ResPutWAL>("PUT", "wal", "resPutWAL", msgCtx, req, ctx); + return buildRes>, ResPutWAL>( + { method: "PUT", store: "wal" }, + "resPutWAL", + msgCtx, + req, + ctx + ); } export interface ReqDelWAL extends Omit { @@ -99,8 +111,7 @@ export function buildResDelWAL( ctx: CalculatePreSignedUrl ): Promise> { return buildRes>, ResDelWAL>( - "DELETE", - "wal", + { method: "DELETE", store: "wal" }, "resDelWAL", msgCtx, req, diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts index eb4d77e7..a2fb24fc 100644 --- a/src/v2-cloud/msg-types.ts +++ b/src/v2-cloud/msg-types.ts @@ -108,7 +108,7 @@ type MsgWithOptionalConn = T & { readonly conn?: QS export type MsgWithOptionalConnAuth = MsgWithOptionalConn & { readonly auth: AuthType }; -export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; +export type MsgWithTenantLedger = T & { readonly tenant: TenantLedger }; export interface ErrorMsg extends MsgBase { readonly type: "error"; @@ -482,8 +482,6 @@ export function buildReqClose(sthis: NextId, auth: AuthType, conn: QSId): ReqClo } export interface SignedUrlParam { - readonly method: HttpMethods; - readonly store: FPStoreTypes; // base path readonly path?: string; // name of the file @@ -492,7 +490,17 @@ export interface SignedUrlParam { readonly index?: string; } -export type ReqSignedUrlParam = Omit; +export interface MethodSignedUrlParam { + readonly method: HttpMethods; + readonly store: FPStoreTypes; +} + +// export type ReqSignedUrlParam = Omit; +export interface ReqSignedUrlParam { + readonly auth: AuthType; + readonly methodParam: MethodSignedUrlParam; + readonly params: SignedUrlParam; +} export interface UpdateReqRes { req: Q; @@ -530,9 +538,13 @@ export function MsgIsTenantLedger(msg: T): msg is MsgWithTena return !!t && !!t.tenant && !!t.ledger; } -export interface ReqSignedUrl extends MsgWithTenantLedger { +export interface ReqSignedUrl extends ReqSignedUrlWithoutMethodParams { // readonly type: "reqSignedUrl"; - readonly params: ReqSignedUrlParam; + readonly methodParams: MethodSignedUrlParam; +} + +export interface ReqSignedUrlWithoutMethodParams extends MsgWithTenantLedger { + readonly params: SignedUrlParam; } export interface GwCtx { @@ -550,20 +562,22 @@ export interface GwCtxConn { export function buildReqSignedUrl( sthis: NextId, type: string, - params: ReqSignedUrlParam, + rparam: ReqSignedUrlParam, gwCtx: GwCtx ): T { return { tid: sthis.nextId().str, type, + auth: rparam.auth, version: VERSION, ...gwCtx, - params, + params: rparam.params, } as T; } export interface ResSignedUrl extends MsgWithTenantLedger { // readonly type: "resSignedUrl"; + readonly methodParams: MethodSignedUrlParam; readonly params: SignedUrlParam; readonly signedUrl: string; } @@ -571,6 +585,7 @@ export interface ResSignedUrl extends MsgWithTenantLedger { export interface ResOptionalSignedUrl extends MsgWithTenantLedger { // readonly type: "resSignedUrl"; readonly params: SignedUrlParam; + readonly methodParams: MethodSignedUrlParam; readonly signedUrl?: string; } @@ -588,8 +603,6 @@ export interface MsgTypesCtx { // }; // } - - export interface MsgTypesCtxSync { readonly sthis: SuperThis; readonly logger: Logger; @@ -600,9 +613,11 @@ export function resAuth(msg: MsgBase): Promise { return msg.auth ? Promise.resolve(msg.auth) : Promise.reject(new Error("No Auth")); } -export async function buildRes>, S extends ResSignedUrl>( - method: SignedUrlParam["method"], - store: FPStoreTypes, +export async function buildRes< + Q extends MsgWithTenantLedger>, + S extends ResSignedUrl, +>( + methodParams: MethodSignedUrlParam, type: string, msgCtx: MsgTypesCtx, req: Q, @@ -612,10 +627,9 @@ export async function buildRes; diff --git a/src/v2-cloud/msger.ts b/src/v2-cloud/msger.ts index 52a1705b..6cc32579 100644 --- a/src/v2-cloud/msger.ts +++ b/src/v2-cloud/msger.ts @@ -128,23 +128,23 @@ export async function authTypeFromUri(logger: Logger, curi: CoerceURI): Promise< if (!authJWK) { return logger.Error().Url(uri).Msg("authJWK is required").ResultError(); } - const sts = await SessionTokenService.createFromEnv() - const fpc = await sts.validate(authJWK) + const sts = await SessionTokenService.createFromEnv(); + const fpc = await sts.validate(authJWK); if (fpc.isErr()) { return logger.Error().Err(fpc).Msg("Invalid authJWK").ResultError(); - } + } return Result.Ok({ type: "fp-cloud", params: { ...fpc.Ok().payload, jwk: authJWK, }, - } satisfies FPCloudAuthType) + } satisfies FPCloudAuthType); } export class MsgConnected { static async connect( - uri: CoerceURI, + auth: AuthType, mrc: Result | MsgRawConnection, conn: Partial = {} ): Promise> { @@ -154,11 +154,7 @@ export class MsgConnected { } mrc = mrc.Ok(); } - const rAuthType = await authTypeFromUri(mrc.sthis.logger, uri); - if (rAuthType.isErr()) { - return Result.Err(rAuthType) - } - const res = await mrc.request(buildReqOpen(mrc.sthis, rAuthType.Ok(), conn), { waitFor: MsgIsResOpen }); + const res = await mrc.request(buildReqOpen(mrc.sthis, auth, conn), { waitFor: MsgIsResOpen }); if (MsgIsError(res) || !MsgIsResOpen(res)) { return mrc.sthis.logger.Error().Err(res).Msg("unexpected response").ResultError(); } @@ -232,7 +228,7 @@ export class MsgConnectedAuth implements MsgRawConnection { return this.authFactory(); } - msgConnAuth(): Promise< Result> { + msgConnAuth(): Promise> { return this.authType().then((r) => { if (r.isErr()) { return Result.Err(r); @@ -241,7 +237,10 @@ export class MsgConnectedAuth implements MsgRawConnection { }); } - request(req: Q, opts: RequestOpts): Promise> { + request( + req: Q, + opts: RequestOpts + ): Promise> { return this.raw.request({ ...req, conn: req.conn || this.conn }, opts); } @@ -298,6 +297,7 @@ export class Msger { } static async open( sthis: SuperThis, + auth: AuthType, curl: CoerceURI, imsgP: Partial = {} ): Promise> { @@ -313,11 +313,11 @@ export class Msger { return rHC; } const hc = rHC.Ok(); - const rAuth = await authTypeFromUri(sthis.logger, url); - if (rAuth.isErr()) { - return Result.Err(rAuth) - } - const resGestalt = await hc.request(buildReqGestalt(sthis, rAuth.Ok(), gs), { + // const rAuth = await authTypeFromUri(sthis.logger, url); + // if (rAuth.isErr()) { + // return Result.Err(rAuth) + // } + const resGestalt = await hc.request(buildReqGestalt(sthis, auth, gs), { waitFor: MsgIsResGestalt, }); if (!MsgIsResGestalt(resGestalt)) { @@ -343,11 +343,12 @@ export class Msger { static connect( sthis: SuperThis, + auth: AuthType, curl: CoerceURI, imsgP: Partial = {}, conn: Partial = {} ): Promise> { - return Msger.open(sthis, curl, imsgP).then((srv) => MsgConnected.connect(curl, srv, conn)); + return Msger.open(sthis, auth, curl, imsgP).then((srv) => MsgConnected.connect(auth, srv, conn)); } private constructor() { diff --git a/src/v2-cloud/pre-signed-url.ts b/src/v2-cloud/pre-signed-url.ts index 2abcaf83..eba056f3 100644 --- a/src/v2-cloud/pre-signed-url.ts +++ b/src/v2-cloud/pre-signed-url.ts @@ -1,8 +1,9 @@ import { Result, URI } from "@adviser/cement"; import { AwsClient } from "aws4fetch"; -import { MsgWithConnAuth, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; +import { MethodSignedUrlParam, MsgWithConnAuth, MsgWithTenantLedger, SignedUrlParam } from "./msg-types.js"; export interface PreSignedMsg extends MsgWithTenantLedger { + readonly methodParams: MethodSignedUrlParam; readonly params: SignedUrlParam; } @@ -31,7 +32,7 @@ export async function calculatePreSignedUrl(psm: PreSignedMsg, env: PreSignedEnv // const psm = ipsm as PreSignedConnMsg; // verify if you are not overriding - let store: string = psm.params.store; + let store: string = psm.methodParams.store; if (psm.params.index?.length) { store = `${store}-${psm.params.index}`; } @@ -65,7 +66,7 @@ export async function calculatePreSignedUrl(psm: PreSignedMsg, env: PreSignedEnv const signedUrl = await a4f .sign( new Request(opUrl.toString(), { - method: psm.params.method, + method: psm.methodParams.method, }), { aws: { diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 1a11da49..9c053d54 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -12,7 +12,14 @@ import { MsgBase, FPJWKCloudAuthType, } from "./msg-types.js"; -import { defaultMsgParams, applyStart, Msger, MsgerParamsWithEnDe, MsgRawConnection, authTypeFromUri } from "./msger.js"; +import { + defaultMsgParams, + applyStart, + Msger, + MsgerParamsWithEnDe, + MsgRawConnection, + authTypeFromUri, +} from "./msger.js"; import { WSConnection } from "./ws-connection.js"; import * as toml from "smol-toml"; import { Env } from "./backend/env.js"; @@ -24,32 +31,32 @@ import { envKeyDefaults, KeysResult, SessionTokenService, TokenForParam } from " export interface MockJWK { keys: KeysResult; - authType: FPJWKCloudAuthType + authType: FPJWKCloudAuthType; applyAuthToURI: (uri: CoerceURI) => URI; } export async function mockJWK(claim: Partial = {}): Promise { - const keys = await SessionTokenService.generateKeyPair() + const keys = await SessionTokenService.generateKeyPair(); const sts = await SessionTokenService.create({ - token: keys.strings.privateKey - }) + token: keys.strings.privateKey, + }); const jwk = await sts.tokenFor({ userId: "hello", tenants: [], ledgers: [], - ...claim - }) + ...claim, + }); return { keys, authType: { type: "fp-cloud-jwk", params: { - jwk - } + jwk, + }, }, - applyAuthToURI: (uri: CoerceURI) => BuildURI.from(uri).setParam("authJWK", jwk).URI() - } + applyAuthToURI: (uri: CoerceURI) => BuildURI.from(uri).setParam("authJWK", jwk).URI(), + }; } export function httpStyle( @@ -102,9 +109,7 @@ export function httpStyle( const rAuth = await authTypeFromUri(sthis.logger, applyAuthToURI(`http://localhost:${port - 1}/fp`)); // should fail - const res = await ret - .Ok() - .request(buildReqGestalt(sthis, rAuth.Ok(), my), { waitFor: MsgIsResGestalt }); + const res = await ret.Ok().request(buildReqGestalt(sthis, rAuth.Ok(), my), { waitFor: MsgIsResGestalt }); if (MsgIsError(res)) { return Result.Err(res.message); } @@ -126,9 +131,7 @@ export function httpStyle( ); // should fail const rAuth = await authTypeFromUri(sthis.logger, applyAuthToURI(`http://4.7.1.1:${port}/fp`)); - const res = await ret - .Ok() - .request(buildReqGestalt(sthis, rAuth.Ok(), my), { waitFor: MsgIsResGestalt }); + const res = await ret.Ok().request(buildReqGestalt(sthis, rAuth.Ok(), my), { waitFor: MsgIsResGestalt }); if (MsgIsError(res)) { return Result.Err(res.message); } @@ -230,10 +233,9 @@ export function NodeHonoServerFactory() { } async function writeEnvFile(sthis: SuperThis, tomlFile: string, env: string, envJWK: string) { - fs.writeFile( - sthis.pathOps.join(sthis.pathOps.dirname(tomlFile), `dev.vars.${env}`), - `${envKeyDefaults.PUBLIC}=${envJWK}\n` - ); + const fname = sthis.pathOps.join(sthis.pathOps.dirname(tomlFile), `.dev.vars.${env}`); + // console.log("Writing to", fname); + await fs.writeFile(fname, `${envKeyDefaults.PUBLIC}=${envJWK}\n`); } export function CFHonoServerFactory(backend: "D1" | "DO") { diff --git a/src/v2-cloud/ws-connection.ts b/src/v2-cloud/ws-connection.ts index 1cd7e4f5..b8eb2ca6 100644 --- a/src/v2-cloud/ws-connection.ts +++ b/src/v2-cloud/ws-connection.ts @@ -33,12 +33,7 @@ export class WSConnection extends MsgRawConnectionBase implements MsgRawConnecti readonly id: string; - constructor( - sthis: SuperThis, - ws: WebSocket, - msgP: MsgerParamsWithEnDe, - exGestalt: ExchangedGestalt - ) { + constructor(sthis: SuperThis, ws: WebSocket, msgP: MsgerParamsWithEnDe, exGestalt: ExchangedGestalt) { super(sthis, exGestalt); this.id = sthis.nextId().str; this.logger = ensureLogger(sthis, "WSConnection"); diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts index b2dd8ff3..a5e0b2a3 100644 --- a/src/v2-cloud/ws-sockets.test.ts +++ b/src/v2-cloud/ws-sockets.test.ts @@ -24,7 +24,7 @@ describe("test multiple connections", () => { let hserv: HonoServer; - let auth: MockJWK + let auth: MockJWK; beforeAll(async () => { auth = await mockJWK(); @@ -54,9 +54,9 @@ describe("test multiple connections", () => { Array(connections) .fill(0) .map(() => { - return Msger.connect(sthis, auth.applyAuthToURI("http://localhost:" + port + "/fp")); + return Msger.connect(sthis, auth.authType, `http://localhost:${port}/fp`); }) - ).then((cs) => cs.map((c) => c.Ok().attachAuth((() => Promise.resolve(Result.Ok(auth.authType)))))); + ).then((cs) => cs.map((c) => c.Ok().attachAuth(() => Promise.resolve(Result.Ok(auth.authType))))); const ready = new Future(); let total = (connections * (connections + 1)) / 2; @@ -79,7 +79,6 @@ describe("test multiple connections", () => { const rest = [...conns]; for (const c of conns) { - // console.log("Sending a chat request", rest.length, conns.length); const act = await c.request(buildReqChat(sthis, auth.authType, c.conn, "Hello"), { waitFor: MsgIsResChat, From 3ac347bddf5c085434dcba7cce0b2cfdea7bd265 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Thu, 13 Mar 2025 10:27:56 +0100 Subject: [PATCH 12/14] chore: now there is only backend process for all tests the auth is passed down and converts into a Claim Object --- globalSetup.v2-cloud.ts | 17 ++ package.json | 6 +- pnpm-lock.yaml | 185 ++++++++++--------- src/sts-service/sts-service.ts | 10 +- src/v2-cloud/backend/cf-hono-server.ts | 31 +++- src/v2-cloud/backend/wrangler.toml | 8 +- src/v2-cloud/client/cloud-gateway.test.ts | 214 +++++++++++----------- src/v2-cloud/client/gateway.ts | 1 + src/v2-cloud/connection.test.ts | 51 ++++-- src/v2-cloud/hono-server.ts | 36 +++- src/v2-cloud/http-connection.ts | 33 ++-- src/v2-cloud/msg-dispatch.ts | 87 +++++++-- src/v2-cloud/msg-dispatcher-impl.ts | 3 +- src/v2-cloud/msg-types.ts | 20 +- src/v2-cloud/msger.ts | 23 ++- src/v2-cloud/node-hono-server.ts | 17 +- src/v2-cloud/pre-signed-url.ts | 1 + src/v2-cloud/test-helper.ts | 160 ++++++++++------ src/v2-cloud/test-utils.ts | 24 +++ src/v2-cloud/ws-sockets.test.ts | 17 +- vitest.v2-cloud.config.ts | 1 + 21 files changed, 601 insertions(+), 344 deletions(-) create mode 100644 globalSetup.v2-cloud.ts create mode 100644 src/v2-cloud/test-utils.ts diff --git a/globalSetup.v2-cloud.ts b/globalSetup.v2-cloud.ts new file mode 100644 index 00000000..02231d79 --- /dev/null +++ b/globalSetup.v2-cloud.ts @@ -0,0 +1,17 @@ +import { ensureSuperThis } from "@fireproof/core"; +import { setupBackend } from "./src/v2-cloud/test-helper.js"; +import { wranglerParams } from "./src/v2-cloud/test-utils.js"; + +const sthis = ensureSuperThis(); +export async function setup() { + const r = await setupBackend(sthis); + process.env[`FP_TEST_CF_BACKEND`] = JSON.stringify(r); + // eslint-disable-next-line no-console + console.log("Started wrangler process - ", wranglerParams(sthis).pid); +} + +export async function teardown() { + // eslint-disable-next-line no-console + console.log("Stopping wrangler process - ", wranglerParams(sthis).pid); + process.kill(wranglerParams(sthis).pid); +} diff --git a/package.json b/package.json index bb56050b..25c3c42d 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.8", "wait-on": "^8.0.2", - "wrangler": "3.112.0", + "wrangler": "^3.114.1", "zx": "^8.4.0" }, "repository": { @@ -97,10 +97,10 @@ }, "homepage": "https://github.com/fireproof-storage/connect#readme", "peerDependencies": { - "@adviser/cement": "^0.4.1" + "@adviser/cement": "^0.4.2" }, "dependencies": { - "@adviser/cement": "^0.4.1", + "@adviser/cement": "^0.4.2", "@aws-sdk/client-dynamodb": "^3.758.0", "@aws-sdk/client-lambda": "^3.758.0", "@aws-sdk/client-s3": "^3.758.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94cd5394..c6661a53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@adviser/cement': - specifier: ^0.4.1 - version: 0.4.1(typescript@5.7.3) + specifier: ^0.4.2 + version: 0.4.2(typescript@5.7.3) '@aws-sdk/client-dynamodb': specifier: ^3.758.0 version: 3.758.0 @@ -34,7 +34,7 @@ importers: version: 4.20250303.0 '@fireproof/core': specifier: 0.20.0-dev-preview-53 - version: 0.20.0-dev-preview-53(@adviser/cement@0.4.1(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1) + version: 0.20.0-dev-preview-53(@adviser/cement@0.4.2(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1) '@fireproof/vendor': specifier: ~2.0.0 version: 2.0.1 @@ -241,16 +241,16 @@ importers: specifier: ^8.0.2 version: 8.0.2 wrangler: - specifier: 3.112.0 - version: 3.112.0(@cloudflare/workers-types@4.20250303.0) + specifier: ^3.114.1 + version: 3.114.1(@cloudflare/workers-types@4.20250303.0) zx: specifier: ^8.4.0 version: 8.4.0 packages: - '@adviser/cement@0.4.1': - resolution: {integrity: sha512-32Pa5mDXnIqFX5CV42dM785YHMEZ6Vc2c0PIN+f2tvthV4lRe8Nw5YiD72diiheEmxrTzUG0P708LH1yFE1w9Q==} + '@adviser/cement@0.4.2': + resolution: {integrity: sha512-12kcfWSvtMZKprMT8gRWSlOuNHi0wZ0uQ95PRscni33kK9+p0WBlRj9KGVl7D6zWnCWnB3tbjJB5wJI6f3OXQw==} engines: {node: '>=20'} '@aws-crypto/crc32@5.2.0': @@ -508,6 +508,15 @@ packages: workerd: optional: true + '@cloudflare/unenv-preset@2.0.2': + resolution: {integrity: sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==} + peerDependencies: + unenv: 2.0.0-rc.14 + workerd: ^1.20250124.0 + peerDependenciesMeta: + workerd: + optional: true + '@cloudflare/vitest-pool-workers@0.7.7': resolution: {integrity: sha512-/6R2tEbQWZyH4y0DdLkIejHHItr9fTIxo3f6IovKHCHecg3ZQ1DM3oPlaAevNOjj7Ya47/0y+AFAyGWEFIOmqQ==} peerDependencies: @@ -521,14 +530,14 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20250214.0': - resolution: {integrity: sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q==} + '@cloudflare/workerd-darwin-64@1.20250224.0': + resolution: {integrity: sha512-sBbaAF2vgQ9+T50ik1ihekdepStBp0w4fvNghBfXIw1iWqfNWnypcjDMmi/7JhXJt2uBxBrSlXCvE5H7Gz+kbw==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20250224.0': - resolution: {integrity: sha512-sBbaAF2vgQ9+T50ik1ihekdepStBp0w4fvNghBfXIw1iWqfNWnypcjDMmi/7JhXJt2uBxBrSlXCvE5H7Gz+kbw==} + '@cloudflare/workerd-darwin-64@1.20250310.0': + resolution: {integrity: sha512-LkLJO6F8lRNaCbK5sQCITi66SyCirDpffRuI5/5iILDJWQU4KVvAOKPvHrd4E5h/WDm9FGd22zMJwky7SxaNjg==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -539,14 +548,14 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20250214.0': - resolution: {integrity: sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw==} + '@cloudflare/workerd-darwin-arm64@1.20250224.0': + resolution: {integrity: sha512-naetGefgjAaDbEacpwaVruJXNwxmRRL7v3ppStgEiqAlPmTpQ/Edjn2SQ284QwOw3MvaVPHrWcaTBupUpkqCyg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20250224.0': - resolution: {integrity: sha512-naetGefgjAaDbEacpwaVruJXNwxmRRL7v3ppStgEiqAlPmTpQ/Edjn2SQ284QwOw3MvaVPHrWcaTBupUpkqCyg==} + '@cloudflare/workerd-darwin-arm64@1.20250310.0': + resolution: {integrity: sha512-WythDJQbsU3Ii1hhA7pJZLBQlHezeYWAnaMnv3gS2Exj45oF8G4chFvrO7zCzjlcJXwSeBTtQRJqxw9AiUDhyA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -557,14 +566,14 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20250214.0': - resolution: {integrity: sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ==} + '@cloudflare/workerd-linux-64@1.20250224.0': + resolution: {integrity: sha512-BtUvuj91rgB06TUAkLYvedghUA8nDFiLcY3GC7MXmWhxCxGmY4PWkrKq/+uHjrhwknCcXrE4aFsM28ja8EcAGA==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20250224.0': - resolution: {integrity: sha512-BtUvuj91rgB06TUAkLYvedghUA8nDFiLcY3GC7MXmWhxCxGmY4PWkrKq/+uHjrhwknCcXrE4aFsM28ja8EcAGA==} + '@cloudflare/workerd-linux-64@1.20250310.0': + resolution: {integrity: sha512-LbP769tT4/5QBHSj4lCt99QIKTi6cU+wYhLfF7rEtYHBnZS2+nIw9xttAzxeERx/aFrU+mxLcYPFV8fUeVxGng==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -575,14 +584,14 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20250214.0': - resolution: {integrity: sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA==} + '@cloudflare/workerd-linux-arm64@1.20250224.0': + resolution: {integrity: sha512-Gr4MPNi+BvwjfWF7clx0dJY2Vm4suaW5FtAQwrfqJmPtN5zb/BP16VZxxnFRMy377dP7ycoxpKfZZ6Q8RVGvbA==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20250224.0': - resolution: {integrity: sha512-Gr4MPNi+BvwjfWF7clx0dJY2Vm4suaW5FtAQwrfqJmPtN5zb/BP16VZxxnFRMy377dP7ycoxpKfZZ6Q8RVGvbA==} + '@cloudflare/workerd-linux-arm64@1.20250310.0': + resolution: {integrity: sha512-FzWeKM6id20EMZACaDg0Kkvg1C4lvXZgLBXVI6h6xaXTNFReoyEp4v4eMrRTuja5ec5k+m5iGKjP4/bMWJp9ew==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -593,14 +602,14 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20250214.0': - resolution: {integrity: sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ==} + '@cloudflare/workerd-windows-64@1.20250224.0': + resolution: {integrity: sha512-x2iF1CsmYmmPEorWb1GRpAAouX5rRjmhuHMC259ojIlozR4G0LarlB9XfmeLEvtw537Ea0kJ6SOhjvUcWzxSvA==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20250224.0': - resolution: {integrity: sha512-x2iF1CsmYmmPEorWb1GRpAAouX5rRjmhuHMC259ojIlozR4G0LarlB9XfmeLEvtw537Ea0kJ6SOhjvUcWzxSvA==} + '@cloudflare/workerd-windows-64@1.20250310.0': + resolution: {integrity: sha512-04OgaDzm8/8nkjF3tovB+WywZLjSdAHCQT2omXKCwH3EDd1kpd8vvzE1pErtdIyKCOf9/sArY4BhPdxRj7ijlg==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -5398,13 +5407,13 @@ packages: engines: {node: '>=16.13'} hasBin: true - miniflare@3.20250214.2: - resolution: {integrity: sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA==} + miniflare@3.20250224.0: + resolution: {integrity: sha512-DyLxzhHCQ9UWDceqEsT7tmw8ZTSAhb1yKUqUi5VDmSxsIocKi4y5kvMijw9ELK8+tq/CiCp/RQxwRNZRJD8Xbg==} engines: {node: '>=16.13'} hasBin: true - miniflare@3.20250224.0: - resolution: {integrity: sha512-DyLxzhHCQ9UWDceqEsT7tmw8ZTSAhb1yKUqUi5VDmSxsIocKi4y5kvMijw9ELK8+tq/CiCp/RQxwRNZRJD8Xbg==} + miniflare@3.20250310.0: + resolution: {integrity: sha512-TQAxoo2ZiQYjiOJoK3bbcyjKD/u1E3akYOeSHc2Zcp1sLVydrgzSjmxtrn65/3BfDIrUgfYHyy9wspT6wzBy/A==} engines: {node: '>=16.13'} hasBin: true @@ -5706,9 +5715,6 @@ packages: ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} - ohash@1.1.6: - resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==} - ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -7144,8 +7150,8 @@ packages: resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} engines: {node: '>=14.0'} - unenv@2.0.0-rc.1: - resolution: {integrity: sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg==} + unenv@2.0.0-rc.14: + resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==} unenv@2.0.0-rc.8: resolution: {integrity: sha512-wj/lN45LvZ4Uz95rti6DC5wg56eocAwA8KYzExk2SN01iuAb9ayzMwN13Kd3EG2eBXu27PzgMIXLTwWfSczKfw==} @@ -7480,33 +7486,33 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20250214.0: - resolution: {integrity: sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g==} + workerd@1.20250224.0: + resolution: {integrity: sha512-NntMg1d9SSkbS4vGdjV5NZxe6FUrvJXY7UiQD7fBtCRVpoPpqz9bVgTq86zalMm+vz64lftzabKT4ka4Y9hejQ==} engines: {node: '>=16'} hasBin: true - workerd@1.20250224.0: - resolution: {integrity: sha512-NntMg1d9SSkbS4vGdjV5NZxe6FUrvJXY7UiQD7fBtCRVpoPpqz9bVgTq86zalMm+vz64lftzabKT4ka4Y9hejQ==} + workerd@1.20250310.0: + resolution: {integrity: sha512-bAaZ9Bmts3mArbIrXYAtr+ZRsAJAAUEsCtvwfBavIYXaZ5sgdEOJBEiBbvsHp6CsVObegOM85tIWpYLpbTxQrQ==} engines: {node: '>=16'} hasBin: true - wrangler@3.112.0: - resolution: {integrity: sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA==} + wrangler@3.114.0: + resolution: {integrity: sha512-cY0HxgU5yuc24tE1Y4KD2n9UzYYEx+9lSL7p/Sqj18SgDfwyiMPY/FryXQAPYLuD/S+dxArRQyeEkFSokIr75Q==} engines: {node: '>=16.17.0'} + deprecated: Deployments with nodejs_compat are broken. Please downgrade to 3.112.0 hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20250214.0 + '@cloudflare/workers-types': ^4.20250224.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true - wrangler@3.114.0: - resolution: {integrity: sha512-cY0HxgU5yuc24tE1Y4KD2n9UzYYEx+9lSL7p/Sqj18SgDfwyiMPY/FryXQAPYLuD/S+dxArRQyeEkFSokIr75Q==} + wrangler@3.114.1: + resolution: {integrity: sha512-GuS6SrnAZZDiNb20Vf2Ww0KCfnctHUEzi5GyML1i2brfQPI6BikgI/W/u6XDtYtah0OkbIWIiNJ+SdhWT7KEcw==} engines: {node: '>=16.17.0'} - deprecated: Deployments with nodejs_compat are broken. Please downgrade to 3.112.0 hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20250224.0 + '@cloudflare/workers-types': ^4.20250310.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -7640,7 +7646,7 @@ packages: snapshots: - '@adviser/cement@0.4.1(typescript@5.7.3)': + '@adviser/cement@0.4.2(typescript@5.7.3)': dependencies: ts-essentials: 10.0.4(typescript@5.7.3) yaml: 2.7.0 @@ -8339,6 +8345,12 @@ snapshots: optionalDependencies: workerd: 1.20250224.0 + '@cloudflare/unenv-preset@2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250310.0)': + dependencies: + unenv: 2.0.0-rc.14 + optionalDependencies: + workerd: 1.20250310.0 + '@cloudflare/vitest-pool-workers@0.7.7(@cloudflare/workers-types@4.20250303.0)(@vitest/runner@3.0.8)(@vitest/snapshot@3.0.8)(vitest@3.0.8(@types/node@22.13.10)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@vitest/runner': 3.0.8 @@ -8360,46 +8372,46 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20240718.0': optional: true - '@cloudflare/workerd-darwin-64@1.20250214.0': + '@cloudflare/workerd-darwin-64@1.20250224.0': optional: true - '@cloudflare/workerd-darwin-64@1.20250224.0': + '@cloudflare/workerd-darwin-64@1.20250310.0': optional: true '@cloudflare/workerd-darwin-arm64@1.20240718.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20250214.0': + '@cloudflare/workerd-darwin-arm64@1.20250224.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20250224.0': + '@cloudflare/workerd-darwin-arm64@1.20250310.0': optional: true '@cloudflare/workerd-linux-64@1.20240718.0': optional: true - '@cloudflare/workerd-linux-64@1.20250214.0': + '@cloudflare/workerd-linux-64@1.20250224.0': optional: true - '@cloudflare/workerd-linux-64@1.20250224.0': + '@cloudflare/workerd-linux-64@1.20250310.0': optional: true '@cloudflare/workerd-linux-arm64@1.20240718.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20250214.0': + '@cloudflare/workerd-linux-arm64@1.20250224.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20250224.0': + '@cloudflare/workerd-linux-arm64@1.20250310.0': optional: true '@cloudflare/workerd-windows-64@1.20240718.0': optional: true - '@cloudflare/workerd-windows-64@1.20250214.0': + '@cloudflare/workerd-windows-64@1.20250224.0': optional: true - '@cloudflare/workerd-windows-64@1.20250224.0': + '@cloudflare/workerd-windows-64@1.20250310.0': optional: true '@cloudflare/workers-types@4.20240718.0': {} @@ -8867,9 +8879,9 @@ snapshots: fastq: 1.19.0 glob: 10.4.5 - '@fireproof/core@0.20.0-dev-preview-53(@adviser/cement@0.4.1(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1)': + '@fireproof/core@0.20.0-dev-preview-53(@adviser/cement@0.4.2(typescript@5.7.3))(@fireproof/vendor@2.0.1)(react@18.3.1)': dependencies: - '@adviser/cement': 0.4.1(typescript@5.7.3) + '@adviser/cement': 0.4.2(typescript@5.7.3) '@fireproof/vendor': 2.0.1 '@ipld/car': 5.4.0 '@ipld/dag-cbor': 9.2.2 @@ -13807,7 +13819,7 @@ snapshots: - supports-color - utf-8-validate - miniflare@3.20250214.2: + miniflare@3.20250224.0: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 @@ -13816,7 +13828,7 @@ snapshots: glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.5 - workerd: 1.20250214.0 + workerd: 1.20250224.0 ws: 8.18.0 youch: 3.2.3 zod: 3.22.3 @@ -13824,7 +13836,7 @@ snapshots: - bufferutil - utf-8-validate - miniflare@3.20250224.0: + miniflare@3.20250310.0: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 @@ -13833,7 +13845,7 @@ snapshots: glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.5 - workerd: 1.20250224.0 + workerd: 1.20250310.0 ws: 8.18.0 youch: 3.2.3 zod: 3.22.3 @@ -14276,8 +14288,6 @@ snapshots: node-fetch-native: 1.6.6 ufo: 1.5.4 - ohash@1.1.6: {} - ohash@2.0.11: {} omit.js@2.0.2: {} @@ -15836,12 +15846,12 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - unenv@2.0.0-rc.1: + unenv@2.0.0-rc.14: dependencies: defu: 6.1.4 - mlly: 1.7.4 - ohash: 1.1.6 - pathe: 1.1.2 + exsolve: 1.0.4 + ohash: 2.0.11 + pathe: 2.0.3 ufo: 1.5.4 unenv@2.0.0-rc.8: @@ -16171,14 +16181,6 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20240718.0 '@cloudflare/workerd-windows-64': 1.20240718.0 - workerd@1.20250214.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20250214.0 - '@cloudflare/workerd-darwin-arm64': 1.20250214.0 - '@cloudflare/workerd-linux-64': 1.20250214.0 - '@cloudflare/workerd-linux-arm64': 1.20250214.0 - '@cloudflare/workerd-windows-64': 1.20250214.0 - workerd@1.20250224.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20250224.0 @@ -16187,17 +16189,26 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20250224.0 '@cloudflare/workerd-windows-64': 1.20250224.0 - wrangler@3.112.0(@cloudflare/workers-types@4.20250303.0): + workerd@1.20250310.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250310.0 + '@cloudflare/workerd-darwin-arm64': 1.20250310.0 + '@cloudflare/workerd-linux-64': 1.20250310.0 + '@cloudflare/workerd-linux-arm64': 1.20250310.0 + '@cloudflare/workerd-windows-64': 1.20250310.0 + + wrangler@3.114.0(@cloudflare/workers-types@4.20250303.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 + '@cloudflare/unenv-preset': 2.0.0(unenv@2.0.0-rc.8)(workerd@1.20250224.0) '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 esbuild: 0.17.19 - miniflare: 3.20250214.2 + miniflare: 3.20250224.0 path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.1 - workerd: 1.20250214.0 + unenv: 2.0.0-rc.8 + workerd: 1.20250224.0 optionalDependencies: '@cloudflare/workers-types': 4.20250303.0 fsevents: 2.3.3 @@ -16206,18 +16217,18 @@ snapshots: - bufferutil - utf-8-validate - wrangler@3.114.0(@cloudflare/workers-types@4.20250303.0): + wrangler@3.114.1(@cloudflare/workers-types@4.20250303.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 - '@cloudflare/unenv-preset': 2.0.0(unenv@2.0.0-rc.8)(workerd@1.20250224.0) + '@cloudflare/unenv-preset': 2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250310.0) '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 esbuild: 0.17.19 - miniflare: 3.20250224.0 + miniflare: 3.20250310.0 path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.8 - workerd: 1.20250224.0 + unenv: 2.0.0-rc.14 + workerd: 1.20250310.0 optionalDependencies: '@cloudflare/workers-types': 4.20250303.0 fsevents: 2.3.3 diff --git a/src/sts-service/sts-service.ts b/src/sts-service/sts-service.ts index 42d0ca90..34912f7b 100644 --- a/src/sts-service/sts-service.ts +++ b/src/sts-service/sts-service.ts @@ -53,9 +53,10 @@ export class SessionTokenService { static async generateKeyPair( alg = "ES256", - options: GenerateKeyPairOptions = { extractable: true } + options: GenerateKeyPairOptions = { extractable: true }, + generateKeyPairFN = (alg: string, options: GenerateKeyPairOptions) => generateKeyPair(alg, options) ): Promise { - const material = await generateKeyPair(alg, options); + const material = await generateKeyPairFN(alg, options); return { material, strings: { @@ -109,7 +110,10 @@ export class SessionTokenService { } async validate(token: string): Promise>> { - return exception2Result(() => jwtVerify(token, this.#key)); + return exception2Result(async () => { + const ret = await jwtVerify(token, this.#key); + return ret; + }); } // async getEnvKey(): Promise { diff --git a/src/v2-cloud/backend/cf-hono-server.ts b/src/v2-cloud/backend/cf-hono-server.ts index 3db65f0c..dc538d13 100644 --- a/src/v2-cloud/backend/cf-hono-server.ts +++ b/src/v2-cloud/backend/cf-hono-server.ts @@ -15,6 +15,7 @@ import { defaultGestalt, EnDeCoder, Gestalt, + isProtocolCapabilities, MsgBase, MsgIsWithConn, MsgWithConnAuth, @@ -31,6 +32,8 @@ import { Env } from "./env.js"; import { WSRoom } from "../ws-room.js"; import { FPBackendDurableObject, FPRoomDurableObject } from "./server.js"; import { ConnItem } from "../msg-dispatch.js"; +import { SessionTokenService } from "../../sts-service/sts-service.js"; +import { portForLocalTest } from "../test-utils.js"; // const startedChs = new KeyedResolvOnce(); @@ -276,14 +279,16 @@ export class CFExposeCtx { id: string, sthis: SuperThis, logger: Logger, + port: number, ende: EnDeCoder, gs: Gestalt, + stsService: SessionTokenService, dbFactory: () => SQLDatabase, wsRoom: CFWSRoom ): CFExposeCtxItem { // const ctx = new CFExposeCtx(id, sthis, logger, ende, gs, db, wsRoom); env.FP_EXPOSE_CTX = env.FP_EXPOSE_CTX ?? new CFExposeCtx(); - return env.FP_EXPOSE_CTX.attach(id, sthis, logger, ende, gs, dbFactory, wsRoom); + return env.FP_EXPOSE_CTX.attach(id, sthis, logger, port, ende, gs, stsService, dbFactory, wsRoom); } private constructor() { @@ -302,12 +307,14 @@ export class CFExposeCtx { id: string, sthis: SuperThis, logger: Logger, + port: number, ende: EnDeCoder, gestalt: Gestalt, + stsService: SessionTokenService, dbFactory: () => SQLDatabase, wsRoom: CFWSRoom ) { - const item = { id, sthis, logger, ende, gestalt, dbFactory, wsRoom }; + const item = { id, sthis, logger, ende, gestalt, dbFactory, wsRoom, stsService, port }; this.#ctxs.set(id, item); return item; } @@ -338,20 +345,24 @@ export class CFHonoFactory implements HonoServerFactory { const logger = ensureLogger(sthis, `CFHono[${id}-${URI.from(c.req.url).pathname}]`); const ende = jsonEnDe(sthis); - const fpProtocol = sthis.env.get("FP_PROTOCOL"); + const reqURI = URI.from(c.req.url); + const protocolCapabilities = reqURI + .getParam("capabilities", "reqRes,stream") + .split(",") + .filter((s) => isProtocolCapabilities(s)); const msgP = defaultMsgParams(sthis, { hasPersistent: true, - protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], + protocolCapabilities, }); const gs = defaultGestalt(msgP, { - id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", + id: "FP-Storage-CF-Backend", }); - - const cfBackendMode = c.env.CF_BACKEND_MODE && c.env.CF_BACKEND_MODE === "DURABLE_OBJECT" ? "DURABLE_OBJECT" : "D1"; + const cfBackendMode = reqURI.getParam("backendMode", "D1"); + const port = portForLocalTest(sthis); let db: () => SQLDatabase; // let cfBackendKey: string; switch (cfBackendMode) { - case "DURABLE_OBJECT": + case "DO": // const cfBackendKey = c.env.CF_BACKEND_KEY ?? "FP_BACKEND_DO"; // console.log("DO-CF_BACKEND_KEY", cfBackendKey, c.env[cfBackendKey]); db = () => new CFDObjSQLDatabase(getBackendDurableObject(c.env, id)); @@ -374,8 +385,10 @@ export class CFHonoFactory implements HonoServerFactory { // break; } + const stsService = await SessionTokenService.createFromEnv(); + const wsRoom = new CFWSRoom(sthis); - const item = CFExposeCtx.attach(c.env, id, sthis, logger, ende, gs, db, wsRoom); + const item = CFExposeCtx.attach(c.env, id, sthis, logger, port, ende, gs, stsService, db, wsRoom); // wsRoom.applyGetWebSockets(c.env.FP_EXPOSE_CTX.getWebSockets); // TODO WE NEED TO START THE DURABLE OBJECT diff --git a/src/v2-cloud/backend/wrangler.toml b/src/v2-cloud/backend/wrangler.toml index 651010e7..ec0a8744 100644 --- a/src/v2-cloud/backend/wrangler.toml +++ b/src/v2-cloud/backend/wrangler.toml @@ -1,6 +1,6 @@ name = "fireproof-cloud" main = "server.ts" -compatibility_date = "2024-04-19" +compatibility_date = "2025-02-24" compatibility_flags = ["nodejs_compat"] # upload_source_maps = true @@ -38,8 +38,8 @@ SECRET_ACCESS_KEY = "minioadmin" FP_DEBUG = "FPMetaGroups" #FP_FORMAT = "yaml" # TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "http" -CF_BACKEND_MODE = "DURABLE_OBJECT" +# FP_PROTOCOL = "http" +# CF_BACKEND_MODE = "DURABLE_OBJECT" # [env.test.services] # bindings = [ @@ -70,7 +70,7 @@ SECRET_ACCESS_KEY = "minioadmin" FP_DEBUG = "FPMetaGroups" #FP_FORMAT = "yaml" # TEST_DATE = "20241121T225359Z" -FP_PROTOCOL = "http" +# FP_PROTOCOL = "http" [[env.test-reqRes-D1.d1_databases]] binding = "FP_BACKEND_D1" diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index b14eb518..45d7ddb3 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -17,124 +17,126 @@ describe("test multiple connections", () => { auth = await mockJWK(); }); - describe.each([NodeHonoServerFactory(), CFHonoServerFactory("D1"), CFHonoServerFactory("DO")])( - "$name - Gateway", - ({ factory }) => { - const port = 1024 + Math.floor(Math.random() * (65536 - 1024)); - let style; + describe.each([ + // force multi line + NodeHonoServerFactory(), + CFHonoServerFactory(sthis, "stream", "D1"), + CFHonoServerFactory(sthis, "stream", "DO"), + ])("$name - Gateway", ({ factory, port }) => { + const reqs = 1; + let style; - let server: HonoServer; - let gw: bs.Gateway; - let unregister: () => void; - let url: URI; - beforeAll(async () => { - // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); - style = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); - const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => - srv.once(app, port) + let server: HonoServer; + let gw: bs.Gateway; + let unregister: () => void; + let url: URI; + beforeAll(async () => { + // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); + style = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); + const app = new Hono(); + server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => + srv.once(app, port) + ); + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + gw = new FireproofCloudGateway(sthis); + const lurl = auth.applyAuthToURI( + BuildURI.from(`fireproof://localhost:${port}/`) + .setParam("protocol", "http") + .setParam("name", "ledger-name") + .setParam("tenant", "tendant") + ); + url = (await gw.start(lurl, sthis)).Ok(); + }); + afterAll(async () => { + await server.close(); + unregister(); + }); + describe("data", () => { + it("get not found", async () => { + await Promise.all( + Array(reqs) + .fill(async () => { + const my = url.build().setParam("store", "data").URI(); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my, key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) ); - unregister = registerFireproofCloudStoreProtocol("fireproof:"); - gw = new FireproofCloudGateway(sthis); - const lurl = auth.applyAuthToURI( - BuildURI.from(`fireproof://localhost:${port}/`) - .setParam("protocol", "http") - .setParam("name", "ledger-name") - .setParam("tenant", "tendant") - ); - url = (await gw.start(lurl, sthis)).Ok(); - }); - afterAll(async () => { - await server.close(); - unregister(); }); - describe("data", () => { - it("get not found", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const my = url.build().setParam("store", "data").URI(); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my, key, sthis)).Ok(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - it("put - get - del - get", async () => { - await Promise.all( - Array(1) - .fill(async () => { - const resStart = await gw.start(url, sthis); - expect(resStart.isOk()).toBeTruthy(); + it("put - get - del - get", async () => { + await Promise.all( + Array(1) + .fill(async () => { + const resStart = await gw.start(url, sthis); + expect(resStart.isOk()).toBeTruthy(); - const my = url.build().setParam("store", "data"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + const my = url.build().setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl, sthis); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl, sthis); - expect(resDel.isOk()).toBeTruthy(); + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl, sthis); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl, sthis); + expect(resDel.isOk()).toBeTruthy(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); }); + }); - describe("WAL", () => { - it("get not found", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const my = url.build().setParam("store", "wal"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); + describe("WAL", () => { + it("get not found", async () => { + await Promise.all( + Array(reqs) + .fill(async () => { + const my = url.build().setParam("store", "wal"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); - it("put - get - del - get", async () => { - await Promise.all( - Array(20) - .fill(async () => { - const resStart = await gw.start(url, sthis); - expect(resStart.isOk()).toBeTruthy(); + it("put - get - del - get", async () => { + await Promise.all( + Array(reqs) + .fill(async () => { + const resStart = await gw.start(url, sthis); + expect(resStart.isOk()).toBeTruthy(); - const my = url.build().setParam("store", "wal"); - const key = `theWALKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + const my = url.build().setParam("store", "wal"); + const key = `theWALKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl, sthis); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl, sthis); - expect(resDel.isOk()).toBeTruthy(); + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl, sthis); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl, sthis); + expect(resDel.isOk()).toBeTruthy(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); }); - } - ); + }); + }); }); diff --git a/src/v2-cloud/client/gateway.ts b/src/v2-cloud/client/gateway.ts index 0825f51c..c6ad1ff3 100644 --- a/src/v2-cloud/client/gateway.ts +++ b/src/v2-cloud/client/gateway.ts @@ -225,6 +225,7 @@ class DataGateway extends BaseGateway implements StoreTypeGateway { return this.logger.Error().Err(rResSignedUrl).Msg("Error in buildResSignedUrl").ResultError(); } const { signedUrl: uploadUrl } = rResSignedUrl; + // console.log("putConn", { uploadUrl }); return this.putObject(uri, uploadUrl, body, conn); } async delConn(uri: URI, conn: AuthedConnection): Promise> { diff --git a/src/v2-cloud/connection.test.ts b/src/v2-cloud/connection.test.ts index 9750678f..a3ea8883 100644 --- a/src/v2-cloud/connection.test.ts +++ b/src/v2-cloud/connection.test.ts @@ -1,5 +1,5 @@ import { ensureSuperThis } from "@fireproof/core"; -import { Result, URI } from "@adviser/cement"; +import { BuildURI, CoerceURI, Result, URI } from "@adviser/cement"; import { buildReqGestalt, buildReqOpen, @@ -57,7 +57,7 @@ import { } from "./msg-type-meta.js"; async function refURL(sp: ResOptionalSignedUrl) { - const { env } = await resolveToml("D1"); + const { env } = await resolveToml(); return ( await calculatePreSignedUrl(sp, { storageUrl: URI.from(env.STORAGE_URL), @@ -75,6 +75,12 @@ async function refURL(sp: ResOptionalSignedUrl) { .asObj(); } +function applyBackend(backend: "DO" | "D1", fn: (uri: CoerceURI) => URI): (uri: CoerceURI) => URI { + return (uri) => { + return fn(BuildURI.from(uri).setParam("backendMode", backend).URI()); + }; +} + describe("Connection", () => { const sthis = ensureSuperThis(); const msgP = defaultMsgParams(sthis, { hasPersistent: true }); @@ -82,7 +88,7 @@ describe("Connection", () => { // let privEnvJWK: string beforeAll(async () => { - sthis.env.sets((await resolveToml("D1")).env as unknown as Record); + sthis.env.sets((await resolveToml()).env as unknown as Record); auth = await mockJWK(); // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); }); @@ -90,17 +96,31 @@ describe("Connection", () => { describe.each([ // force multiple lines NodeHonoServerFactory(), - CFHonoServerFactory("DO"), - CFHonoServerFactory("D1"), + CFHonoServerFactory(sthis), ])("$name - Connection", (honoServer) => { - const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); + const port = honoServer.port; + // const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); - const styles: { name: string; action: () => ReturnType | ReturnType }[] = [ - // force multiple lines - { name: "http", action: () => httpStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, - { name: "ws", action: () => wsStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, - ]; + const styles: { name: string; action: () => ReturnType | ReturnType }[] = + honoServer.name === "NodeHonoServer" + ? [ + // force multiple lines + { name: "http", action: () => httpStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, + { name: "ws", action: () => wsStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, + ] + : [ + { + name: "http-DO", + action: () => httpStyle(sthis, applyBackend("DO", auth.applyAuthToURI), port, msgP, my), + }, + { name: "ws-DO", action: () => wsStyle(sthis, applyBackend("DO", auth.applyAuthToURI), port, msgP, my) }, + { + name: "http-D1", + action: () => httpStyle(sthis, applyBackend("D1", auth.applyAuthToURI), port, msgP, my), + }, + { name: "ws-D1", action: () => wsStyle(sthis, applyBackend("D1", auth.applyAuthToURI), port, msgP, my) }, + ]; describe.each(styles)(`${honoServer.name} - $name`, (styleFn) => { let style: ReturnType | ReturnType; @@ -183,7 +203,10 @@ describe("Connection", () => { if (!MsgIsResGestalt(r)) { assert.fail("expected MsgError", JSON.stringify(r)); } - expect(r.gestalt).toEqual(c.exchangedGestalt?.remote); + expect(r.gestalt).toEqual({ + ...c.exchangedGestalt?.remote, + id: r.gestalt.id, + }); }); it("openConnection", async () => { @@ -203,7 +226,7 @@ describe("Connection", () => { }); it("open", async () => { - const rC = await Msger.connect(sthis, auth.authType, `http://localhost:${port}/fp`, msgP, { + const rC = await Msger.connect(sthis, auth.authType, style.ok.url("fp"), msgP, { reqId: "req-open-testy", }); expect(rC.isOk()).toBeTruthy(); @@ -215,7 +238,7 @@ describe("Connection", () => { expect(c.raw).toBeInstanceOf(style.cInstance); expect(c.exchangedGestalt).toEqual({ my, - remote: style.remoteGestalt, + remote: { ...style.remoteGestalt, id: c.exchangedGestalt.remote.id }, }); await c.close((await c.msgConnAuth()).Ok()); }); diff --git a/src/v2-cloud/hono-server.ts b/src/v2-cloud/hono-server.ts index 577ad466..dbc6ebcc 100644 --- a/src/v2-cloud/hono-server.ts +++ b/src/v2-cloud/hono-server.ts @@ -12,6 +12,9 @@ import { EnDeCoder, Gestalt, MsgWithConnAuth, + FPCloudAuthType, + AuthType, + isAuthTypeFPCloudJWK, } from "./msg-types.js"; import { MsgDispatcher, MsgDispatcherCtx, Promisable, WSConnection } from "./msg-dispatch.js"; import { WSContext, WSContextInit, WSMessageReceive } from "hono/ws"; @@ -35,6 +38,7 @@ import { CFExposeCtxItem } from "./backend/cf-hono-server.js"; import { metaMerger } from "./meta-merger/meta-merger.js"; import { SuperThis } from "@fireproof/core"; import { SQLDatabase } from "./meta-merger/abstract-sql.js"; +import { SessionTokenService } from "../sts-service/sts-service.js"; // export interface RunTimeParams { // readonly sthis: SuperThis; @@ -54,9 +58,11 @@ export class WSContextWithId extends WSContext { export interface ExposeCtxItem { readonly sthis: SuperThis; + readonly port: number; readonly wsRoom: T; readonly logger: Logger; readonly ende: EnDeCoder; + readonly stsService: SessionTokenService; readonly gestalt: Gestalt; readonly dbFactory: () => SQLDatabase; // readonly metaMerger: MetaMerger; @@ -75,6 +81,8 @@ export interface WSEventsConnId { // eslint-disable-next-line @typescript-eslint/no-invalid-void-type export type ConnMiddleware = (conn: WSConnection, c: Context, next: Next) => Promise; export interface HonoServerImpl { + validateAuth(ctx: MsgDispatcherCtx, auth: AuthType): Promise>; + start(ctx: CFExposeCtxItem): Promise; // gestalt(): Gestalt; // getConnected(): Connected[]; @@ -125,6 +133,24 @@ export abstract class HonoServerBase implements HonoServerImpl { createEvents: (c: Context) => WSEventsConnId | Promise> ): ConnMiddleware; + async validateAuth(ctx: MsgDispatcherCtx, auth: AuthType): Promise> { + if (!isAuthTypeFPCloudJWK(auth)) { + return Promise.resolve(Result.Err("Only fp-cloud-jwt is supported")); + } + // console.log("validateAuth-0", auth.params.jwk, ctx.stsService); + const rAuth = await ctx.stsService.validate(auth.params.jwk); + // console.log("validateAuth-1", auth.params.jwk, ctx.stsService, rAuth); + if (rAuth.isErr()) { + return Result.Err(rAuth); + } + return Result.Ok({ + type: "fp-cloud", + params: { + claim: rAuth.Ok().payload, + jwk: auth.params.jwk, + }, + }); + } // abstract getConnected(): Connected[]; start(ctx: ExposeCtxItem, drop = false): Promise { @@ -212,7 +238,7 @@ export interface HonoServerFactory { inject(c: Context, fn: (rt: ExposeCtxItemWithImpl) => Promise): Promise; start(app: Hono): Promise; - serve(app: Hono, port?: number): Promise; + serve(app: Hono, port: number): Promise; close(): Promise; } @@ -230,19 +256,23 @@ class NoBackChannel implements MsgDispatcherCtx { this.ctx = ctx; this.impl = ctx.impl; this.id = ctx.id; + this.port = ctx.port; this.sthis = ctx.sthis; this.logger = ctx.logger; this.ende = ctx.ende; this.gestalt = ctx.gestalt; this.dbFactory = ctx.dbFactory; + this.stsService = ctx.stsService; } readonly impl: HonoServerImpl; + readonly port: number; readonly sthis: SuperThis; readonly logger: Logger; readonly ende: EnDeCoder; readonly gestalt: Gestalt; readonly dbFactory: () => SQLDatabase; readonly id: string; + readonly stsService: SessionTokenService; get ws(): WSContextWithId { return { @@ -277,14 +307,14 @@ export class HonoServer { } /* only for testing */ - async once(app: Hono, port?: number): Promise { + async once(app: Hono, port: number): Promise { this.register(app); await this.factory.start(app); await this.factory.serve(app, port); return this; } - async serve(app: Hono, port?: number): Promise { + async serve(app: Hono, port: number): Promise { await this.factory.serve(app, port); return this; } diff --git a/src/v2-cloud/http-connection.ts b/src/v2-cloud/http-connection.ts index 8fc4e4dc..932edb13 100644 --- a/src/v2-cloud/http-connection.ts +++ b/src/v2-cloud/http-connection.ts @@ -134,20 +134,25 @@ export class HttpConnection extends MsgRawConnectionBase implements MsgRawConnec } const res = rRes.Ok(); if (!res.ok) { - return this.toMsg( - buildErrorMsg( - this, - req, - this.logger - .Error() - .Url(url) - .Str("status", res.status.toString()) - .Str("statusText", res.statusText) - .Msg("HTTP Error") - .AsError(), - await res.text() - ) - ); + const data = new Uint8Array(await res.arrayBuffer()); + const ret = await exception2Result(async () => this.msgP.ende.decode(data) as S); + if (ret.isErr() || !MsgIsError(ret.Ok())) { + return this.toMsg( + buildErrorMsg( + this, + req, + this.logger + .Error() + .Url(url) + .Str("status", res.status.toString()) + .Str("statusText", res.statusText) + .Msg("HTTP Error") + .AsError(), + await res.text() + ) + ); + } + return this.toMsg(ret.Ok()); } const data = new Uint8Array(await res.arrayBuffer()); const ret = await exception2Result(async () => this.msgP.ende.decode(data) as S); diff --git a/src/v2-cloud/msg-dispatch.ts b/src/v2-cloud/msg-dispatch.ts index 4c1bd0a7..0ac3832b 100644 --- a/src/v2-cloud/msg-dispatch.ts +++ b/src/v2-cloud/msg-dispatch.ts @@ -1,10 +1,22 @@ import { SuperThis } from "@fireproof/core"; -import { MsgBase, buildErrorMsg, MsgWithError, QSId, MsgWithConnAuth } from "./msg-types.js"; +import { + MsgBase, + buildErrorMsg, + MsgWithError, + QSId, + MsgWithConnAuth, + isAuthTypeFPCloud, + FPJWKCloudAuthType, + MsgIsError, + isAuthTypeFPCloudJWK, + AuthType, +} from "./msg-types.js"; import { PreSignedMsg } from "./pre-signed-url.js"; import { ExposeCtxItemWithImpl, HonoServerImpl, WSContextWithId } from "./hono-server.js"; import { UnReg } from "./msger.js"; import { WSRoom } from "./ws-room.js"; +import { SessionTokenService } from "../sts-service/sts-service.js"; export interface MsgContext { calculatePreSignedUrl(p: PreSignedMsg): Promise; @@ -50,6 +62,7 @@ export interface ConnectionInfo { export interface MsgDispatcherCtx extends ExposeCtxItemWithImpl { readonly id: string; readonly impl: HonoServerImpl; + readonly stsService: SessionTokenService; // readonly auth: AuthFactory; readonly ws: WSContextWithId; } @@ -104,23 +117,65 @@ export class MsgDispatcher { } send(ctx: MsgDispatcherCtx, msg: MsgBase) { + const isError = MsgIsError(msg); const str = ctx.ende.encode(msg); ctx.ws.send(str); - return new Response(str); + return new Response(str, { + status: isError ? 500 : 200, + statusText: isError ? "error" : "ok", + }); } - async dispatch(ctx: MsgDispatcherCtx, msg: MsgBase): Promise { - const validateConn = async ( - msg: T, - fn: (msg: MsgWithConnAuth) => Promisable> - ): Promise => { - if (!ctx.wsRoom.isConnected(msg)) { - return this.send(ctx, buildErrorMsg(ctx, { ...msg }, new Error("dispatch missing connection"))); - // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); + async validateConn( + ctx: MsgDispatcherCtx, + msg: T, + fn: (msg: MsgWithConnAuth) => Promisable> + ): Promise { + if (!ctx.wsRoom.isConnected(msg)) { + return this.send(ctx, buildErrorMsg(ctx, { ...msg }, new Error("dispatch missing connection"))); + // return send(buildErrorMsg(this.sthis, this.logger, msg, new Error("non open connection"))); + } + // console.log("validateConn-1"); + const r = await this.validateAuth(ctx, msg, (msg) => fn(msg)); + return Promise.resolve(this.send(ctx, r)); + } + + async validateAuth( + ctx: MsgDispatcherCtx, + msg: T, + fn: (msg: T) => Promisable> + ): Promise> { + if (msg.auth) { + // console.log("validateAuth-1", msg.auth); + const rAuth = await ctx.impl.validateAuth(ctx, msg.auth); + if (rAuth.isErr()) { + return buildErrorMsg(ctx, msg, rAuth.Err()); } - const r = await fn(msg); - return Promise.resolve(this.send(ctx, r)); - }; + const sMsg = await fn({ + ...msg, + auth: rAuth.Ok(), + }); + switch (true) { + case isAuthTypeFPCloudJWK(sMsg.auth): + return sMsg; + case isAuthTypeFPCloud(sMsg.auth): + return { + ...sMsg, + auth: { + type: "fp-cloud-jwk", + params: { + jwk: sMsg.auth.params.jwk, + }, + } satisfies FPJWKCloudAuthType as AuthType, // send me to hell ts + }; + default: + return buildErrorMsg(ctx, msg, new Error("unexpected auth")); + } + } + return buildErrorMsg(ctx, msg, new Error("missing auth")); + } + + async dispatch(ctx: MsgDispatcherCtx, msg: MsgBase): Promise { try { // console.log("dispatch-1", msg); const found = Array.from(this.items.values()).find((item) => item.match(msg)); @@ -129,11 +184,11 @@ export class MsgDispatcher { return this.send(ctx, buildErrorMsg(ctx, msg, new Error("unexpected message"))); } if (!found.isNotConn) { - // console.log("dispatch-3", msg); - return validateConn(msg, (msg) => found.fn(ctx, msg)); + // console.log("dispatch-3"); + return this.validateConn(ctx, msg, (msg) => found.fn(ctx, msg)); } // console.log("dispatch-4", msg); - return this.send(ctx, await found.fn(ctx, msg)); + return this.send(ctx, await this.validateAuth(ctx, msg, (msg) => found.fn(ctx, msg))); } catch (e) { return this.send(ctx, buildErrorMsg(ctx, msg, e as Error)); } diff --git a/src/v2-cloud/msg-dispatcher-impl.ts b/src/v2-cloud/msg-dispatcher-impl.ts index f96daf43..5d432d47 100644 --- a/src/v2-cloud/msg-dispatcher-impl.ts +++ b/src/v2-cloud/msg-dispatcher-impl.ts @@ -59,7 +59,8 @@ export function buildMsgDispatcher( match: MsgIsReqGestalt, isNotConn: true, fn: (ctx, msg: ReqGestalt) => { - const resGestalt = buildResGestalt(msg, ctx.gestalt); + const resGestalt = buildResGestalt(msg, ctx.gestalt, msg.auth); + // console.log(">>>>>>>>>>>>>>", resGestalt); return resGestalt; }, }, diff --git a/src/v2-cloud/msg-types.ts b/src/v2-cloud/msg-types.ts index a2fb24fc..c9b8fe4f 100644 --- a/src/v2-cloud/msg-types.ts +++ b/src/v2-cloud/msg-types.ts @@ -39,6 +39,14 @@ export interface AuthType { readonly type: "ucan" | "error" | "fp-cloud-jwk" | "fp-cloud"; } +export function isAuthTypeFPCloudJWK(a: AuthType): a is FPJWKCloudAuthType { + return a.type === "fp-cloud-jwk"; +} + +export function isAuthTypeFPCloud(a: AuthType): a is FPCloudAuthType { + return a.type === "fp-cloud"; +} + export interface UCanAuth extends AuthType { readonly type: "ucan"; readonly params: { @@ -54,7 +62,10 @@ export interface FPJWKCloudAuthType extends AuthType { export interface FPCloudAuthType extends AuthType { readonly type: "fp-cloud"; - readonly params: TokenForParam; + readonly params: { + readonly claim: TokenForParam; + readonly jwk: string; // for reply + }; } export type AuthFactory = (tp?: Partial) => Promise>; @@ -133,6 +144,11 @@ export type FPStoreTypes = "meta" | "data" | "wal"; // stream is WebSocket export type ProtocolCapabilities = "reqRes" | "stream"; +export function isProtocolCapabilities(s: string): s is ProtocolCapabilities { + const x = s.trim(); + return x === "reqRes" || x === "stream"; +} + export interface Gestalt { /** * Describes StoreTypes which are handled @@ -362,7 +378,7 @@ export interface ResGestalt extends MsgBase { readonly gestalt: Gestalt; } -export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt, auth?: AuthType): ResGestalt | ErrorMsg { +export function buildResGestalt(req: ReqGestalt, gestalt: Gestalt, auth: AuthType): ResGestalt | ErrorMsg { return { tid: req.tid, auth: auth || req.auth, diff --git a/src/v2-cloud/msger.ts b/src/v2-cloud/msger.ts index 6cc32579..723208a4 100644 --- a/src/v2-cloud/msger.ts +++ b/src/v2-cloud/msger.ts @@ -22,13 +22,12 @@ import { buildReqClose, MsgIsResClose, AuthFactory, - FPCloudAuthType, AuthType, + FPJWKCloudAuthType, } from "./msg-types.js"; import { SuperThis } from "@fireproof/core"; import { HttpConnection } from "./http-connection.js"; import { WSConnection } from "./ws-connection.js"; -import { SessionTokenService } from "../sts-service/sts-service.js"; // const headers = { // "Content-Type": "application/json", @@ -122,24 +121,24 @@ export async function applyStart(prC: Promise>): Promis return rC; } -export async function authTypeFromUri(logger: Logger, curi: CoerceURI): Promise> { +export async function authTypeFromUri(logger: Logger, curi: CoerceURI): Promise> { const uri = URI.from(curi); const authJWK = uri.getParam("authJWK"); if (!authJWK) { return logger.Error().Url(uri).Msg("authJWK is required").ResultError(); } - const sts = await SessionTokenService.createFromEnv(); - const fpc = await sts.validate(authJWK); - if (fpc.isErr()) { - return logger.Error().Err(fpc).Msg("Invalid authJWK").ResultError(); - } + // const sts = await SessionTokenService.createFromEnv(); + // const fpc = await sts.validate(authJWK); + // if (fpc.isErr()) { + // return logger.Error().Err(fpc).Msg("Invalid authJWK").ResultError(); + // } return Result.Ok({ - type: "fp-cloud", + type: "fp-cloud-jwk", params: { - ...fpc.Ok().payload, + // claim: fpc.Ok().payload, jwk: authJWK, }, - } satisfies FPCloudAuthType); + } satisfies FPJWKCloudAuthType); } export class MsgConnected { @@ -321,7 +320,7 @@ export class Msger { waitFor: MsgIsResGestalt, }); if (!MsgIsResGestalt(resGestalt)) { - return Result.Err(new Error("Invalid Gestalt")); + return sthis.logger.Error().Any({ resGestalt }).Msg("should be ResGestalt").ResultError(); } await hc.close(resGestalt /* as MsgWithConnAuth */); const exGt = { my: gs, remote: resGestalt.gestalt } satisfies ExchangedGestalt; diff --git a/src/v2-cloud/node-hono-server.ts b/src/v2-cloud/node-hono-server.ts index def1b208..89ef060e 100644 --- a/src/v2-cloud/node-hono-server.ts +++ b/src/v2-cloud/node-hono-server.ts @@ -16,6 +16,7 @@ import { defaultMsgParams, jsonEnDe } from "./msger.js"; import { defaultGestalt, Gestalt, + isProtocolCapabilities, MsgBase, MsgerParams, MsgIsWithConn, @@ -26,6 +27,8 @@ import { import { SQLDatabase } from "./meta-merger/abstract-sql.js"; import { WSRoom } from "./ws-room.js"; import { ConnItem } from "./msg-dispatch.js"; +import { SessionTokenService } from "../sts-service/sts-service.js"; +import { portRandom } from "./test-utils.js"; interface ServerType { close(fn: () => void): void; @@ -166,7 +169,7 @@ export class NodeHonoFactory implements HonoServerFactory { this._wsRoom = new NodeWSRoom(sthis); } - inject( + async inject( c: Context, // eslint-disable-next-line @typescript-eslint/no-invalid-void-type fn: (rt: ExposeCtxItemWithImpl) => Promise @@ -180,24 +183,30 @@ export class NodeHonoFactory implements HonoServerFactory { const id = sthis.nextId(12).str; - const fpProtocol = sthis.env.get("FP_PROTOCOL"); + const protocolCapabilities = URI.from(c.req.url) + .getParam("capabilities", "reqRes,stream") + .split(",") + .filter((s) => isProtocolCapabilities(s)); const msgP = this.params.msgP ?? defaultMsgParams(sthis, { hasPersistent: true, - protocolCapabilities: fpProtocol ? (fpProtocol === "ws" ? ["stream"] : ["reqRes"]) : ["reqRes", "stream"], + protocolCapabilities, }); const gestalt = this.params.gs ?? defaultGestalt(msgP, { - id: fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", + id: "FP-Storage-Backend", // fpProtocol ? (fpProtocol === "http" ? "HTTP-server" : "WS-server") : "FP-CF-Server", }); + const stsService = await SessionTokenService.createFromEnv(); const ctx: ExposeCtxItem = { id, sthis, logger, wsRoom: this._wsRoom, + port: portRandom(), + stsService, gestalt, ende, dbFactory: () => this.params.sql, diff --git a/src/v2-cloud/pre-signed-url.ts b/src/v2-cloud/pre-signed-url.ts index eba056f3..7f33aa5f 100644 --- a/src/v2-cloud/pre-signed-url.ts +++ b/src/v2-cloud/pre-signed-url.ts @@ -77,5 +77,6 @@ export async function calculatePreSignedUrl(psm: PreSignedMsg, env: PreSignedEnv } ) .then((res) => res.url); + // console.log("opUrl", opUrl.toString(), psm.methodParams.method, signedUrl, env.aws); return Result.Ok(URI.from(signedUrl)); } diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 9c053d54..93e362d0 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -1,6 +1,6 @@ -import { BuildURI, CoerceURI, Future, Result, URI } from "@adviser/cement"; -import { SuperThis } from "@fireproof/core"; -import { $, fs, sleep } from "zx"; +import { BuildURI, CoerceURI, Future, Result, UnPromisify, URI } from "@adviser/cement"; +import { ensureSuperThis, SuperThis } from "@fireproof/core"; +import { $, fs } from "zx"; import { HttpConnection } from "./http-connection.js"; import { MsgerParams, @@ -27,15 +27,40 @@ import { HonoServer } from "./hono-server.js"; import { NodeHonoFactory } from "./node-hono-server.js"; import { CFHonoFactory } from "./backend/cf-hono-server.js"; import { BetterSQLDatabase } from "./meta-merger/bettersql-abstract-sql.js"; -import { envKeyDefaults, KeysResult, SessionTokenService, TokenForParam } from "../sts-service/sts-service.js"; +import { env2jwk, envKeyDefaults, KeysResult, SessionTokenService, TokenForParam } from "../sts-service/sts-service.js"; +import { GenerateKeyPairOptions } from "jose/key/generate/keypair"; +import { portForLocalTest, portRandom } from "./test-utils.js"; export interface MockJWK { keys: KeysResult; authType: FPJWKCloudAuthType; applyAuthToURI: (uri: CoerceURI) => URI; } -export async function mockJWK(claim: Partial = {}): Promise { - const keys = await SessionTokenService.generateKeyPair(); +export async function mockJWK(claim: Partial = {}, sthis = ensureSuperThis()): Promise { + // that could be solved better now with globalSetup.v2-cloud.ts + const publicJWK = await env2jwk( + "zeWndr5LEoaySgKSo2aZniYqaZvsKKu1RhfpL2R3hjarNgfXfN7CvR1cAiT74TMB9MQtMvh4acC759Xf8rTwCgxXvGHCBjHngThNtYpK2CoysiAMRJFUi9irMY9H7WApJkfxB15n8ss8iaEojcGB7voQVyk2T6aFPRnNdkoB6v5zk", + "ES256", + sthis + ); + const privateJWK = await env2jwk( + "z33KxHvFS3jLz72v9DeyGBqo79qkbpv5KNP43VKUKSh1fcLb629pFTFyiJEosZ9jCrr8r9TE44KXCPZ2z1FeWGsV1N5gKjGWmZvubUwNHPynxNjCYy4GeYoQ8ukBiKjcPG22pniWCnRMwZvueUBkVk6NdtNY1uwyPk2HAGTsfrw5CBJvTcYsaFeG11SKZ9Q55Xk1W2p4gtZQHzkYHdfQQhgZ73Ttq7zmFoms73kh7MsudYzErx", + "ES256", + sthis + ); + + const keys = await SessionTokenService.generateKeyPair( + "ES256", + { + extractable: true, + }, + (_alg: string, _options: GenerateKeyPairOptions) => { + return Promise.resolve({ + privateKey: privateJWK, + publicKey: publicJWK, + }); + } + ); const sts = await SessionTokenService.create({ token: keys.strings.privateKey, @@ -75,12 +100,20 @@ export function httpStyle( remoteGestalt: remote, cInstance: HttpConnection, ok: { - url: () => URI.from(`http://127.0.0.1:${port}/fp`), + url: (path = "fp") => + BuildURI.from(`http://127.0.0.1:${port}`) + .pathname(path) + .setParam("capabilities", remote.protocolCapabilities.join(",")) + .URI(), open: () => applyStart( Msger.openHttp( sthis, - [URI.from(`http://localhost:${port}/fp`)], + [ + BuildURI.from(`http://127.0.0.1:${port}/fp`) + .setParam("capabilities", remote.protocolCapabilities.join(",")) + .URI(), + ], { ...msgP, // protocol: "http", @@ -157,12 +190,20 @@ export function wsStyle( remoteGestalt: remote, cInstance: WSConnection, ok: { - url: () => URI.from(`http://127.0.0.1:${port}/ws`), + url: (path = "ws") => + BuildURI.from(`http://127.0.0.1:${port}`) + .pathname(path) + .setParam("capabilities", remote.protocolCapabilities.join(",")) + .URI(), open: () => applyStart( Msger.openWS( sthis, - applyAuthToURI(URI.from(`http://localhost:${port}/ws`)), + applyAuthToURI( + BuildURI.from(`http://127.0.0.1:${port}/ws`) + .setParam("capabilities", remote.protocolCapabilities.join(",")) + .URI() + ), { ...msgP, // protocol: "ws", @@ -203,7 +244,7 @@ export function wsStyle( }; } -export async function resolveToml(backend: "D1" | "DO") { +export async function resolveToml() { const tomlFile = "src/v2-cloud/backend/wrangler.toml"; const tomeStr = await fs.readFile(tomlFile, "utf-8"); const wranglerFile = toml.parse(tomeStr) as unknown as { @@ -211,15 +252,16 @@ export async function resolveToml(backend: "D1" | "DO") { }; return { tomlFile, - env: wranglerFile.env[`test-reqRes-${backend}`].vars, + env: wranglerFile.env[`test`].vars, }; } export function NodeHonoServerFactory() { return { name: "NodeHonoServer", + port: portRandom(), factory: async (sthis: SuperThis, msgP: MsgerParams, remoteGestalt: Gestalt, _port: number, pubEnvJWK: string) => { - const { env } = await resolveToml("D1"); + const { env } = await resolveToml(); sthis.env.set(envKeyDefaults.PUBLIC, pubEnvJWK); sthis.env.sets(env as unknown as Record); const nhf = new NodeHonoFactory(sthis, { @@ -232,52 +274,66 @@ export function NodeHonoServerFactory() { }; } +export type BackendParams = UnPromisify>; + +export async function setupBackend( + sthis: SuperThis, + // backend: "D1" | "DO", + // key: string, + port = portRandom() +): Promise<{ port: number; pid: number; envName: string }> { + const envName = `test`; + if (process.env.FP_WRANGLER_PORT) { + return Promise.resolve({ port: +process.env.FP_WRANGLER_PORT, pid: 0, envName }); + } + const { tomlFile } = await resolveToml(); + $.verbose = !!process.env.FP_DEBUG; + const auth = await mockJWK({}, sthis); + await writeEnvFile(sthis, tomlFile, envName, auth.keys.strings.publicKey); + // .dev.vars. + const runningWrangler = $` + wrangler dev -c ${tomlFile} --port ${port} --env ${envName} --no-show-interactive-dev-session --no-live-reload & + waitPid=$! + echo "PID:$waitPid" + wait $waitPid`; + const waitReady = new Future(); + let pid: number | undefined; + runningWrangler.stdout.on("data", (chunk) => { + // console.log(">>", chunk.toString()) + const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; + if (mightPid) { + pid = +mightPid; + } + if (chunk.includes("Starting local serv")) { + waitReady.resolve(true); + } + }); + runningWrangler.stderr.on("data", (chunk) => { + // eslint-disable-next-line no-console + console.error("!!", chunk.toString()); + }); + await waitReady.asPromise(); + return { port, pid: pid || 0, envName }; +} + async function writeEnvFile(sthis: SuperThis, tomlFile: string, env: string, envJWK: string) { const fname = sthis.pathOps.join(sthis.pathOps.dirname(tomlFile), `.dev.vars.${env}`); // console.log("Writing to", fname); await fs.writeFile(fname, `${envKeyDefaults.PUBLIC}=${envJWK}\n`); } -export function CFHonoServerFactory(backend: "D1" | "DO") { +export function CFHonoServerFactory(sthis: SuperThis) { return { - name: `CFHonoServer(${backend})`, - factory: async (sthis: SuperThis, _msgP: MsgerParams, remoteGestalt: Gestalt, port: number, pubEnvJWK: string) => { - if (process.env.FP_WRANGLER_PORT) { - return new HonoServer(new CFHonoFactory()); - } - const { tomlFile } = await resolveToml(backend); - $.verbose = !!process.env.FP_DEBUG; - const envName = `test-${remoteGestalt.protocolCapabilities[0]}-${backend}`; - await writeEnvFile(sthis, tomlFile, envName, pubEnvJWK); - // .dev.vars. - const runningWrangler = $` - wrangler dev -c ${tomlFile} --port ${port} --env ${envName} --no-show-interactive-dev-session --no-live-reload & - waitPid=$! - echo "PID:$waitPid" - wait $waitPid`; - const waitReady = new Future(); - let pid: number | undefined; - runningWrangler.stdout.on("data", (chunk) => { - // console.log(">>", chunk.toString()) - const mightPid = chunk.toString().match(/PID:(\d+)/)?.[1]; - if (mightPid) { - pid = +mightPid; - } - if (chunk.includes("Starting local serv")) { - waitReady.resolve(true); - } - }); - runningWrangler.stderr.on("data", (chunk) => { - // eslint-disable-next-line no-console - console.error("!!", chunk.toString()); - }); - await waitReady.asPromise(); - await sleep(300); - return new HonoServer( - new CFHonoFactory(() => { - if (pid) process.kill(pid); - }) - ); + name: `CFHonoServer`, + port: portForLocalTest(sthis), + factory: async ( + _sthis: SuperThis, + _msgP: MsgerParams, + _remoteGestalt: Gestalt, + _port: number, + _pubEnvJWK: string + ) => { + return new HonoServer(new CFHonoFactory()); }, }; } diff --git a/src/v2-cloud/test-utils.ts b/src/v2-cloud/test-utils.ts new file mode 100644 index 00000000..75bb7487 --- /dev/null +++ b/src/v2-cloud/test-utils.ts @@ -0,0 +1,24 @@ +import { SuperThis } from "@fireproof/core"; +import type { BackendParams } from "./test-helper.js"; + +export function portForLocalTest(sthis: SuperThis): number { + return wranglerParams(sthis).port; +} + +export function wranglerParams(sthis: SuperThis): BackendParams { + const cf_backend = sthis.env.get("FP_TEST_CF_BACKEND"); + if (!cf_backend) { + return { + port: 0, + pid: 0, + envName: "not-set", + }; + } + return JSON.parse(cf_backend) as BackendParams; +} + +export function portRandom(): number { + return process.env.FP_WRANGLER_PORT + ? +process.env.FP_WRANGLER_PORT + : 1024 + Math.floor(Math.random() * (65536 - 1024)); +} diff --git a/src/v2-cloud/ws-sockets.test.ts b/src/v2-cloud/ws-sockets.test.ts index a5e0b2a3..af39bcdd 100644 --- a/src/v2-cloud/ws-sockets.test.ts +++ b/src/v2-cloud/ws-sockets.test.ts @@ -13,11 +13,10 @@ describe("test multiple connections", () => { describe.each([ // dummy NodeHonoServerFactory(), - CFHonoServerFactory("D1"), - CFHonoServerFactory("DO"), - ])("$name - Gateway", ({ factory }) => { + CFHonoServerFactory(sthis), + ])("$name - Gateway", ({ factory, port }) => { const msgP = defaultMsgParams(sthis, { hasPersistent: true }); - const port = +(process.env.FP_WRANGLER_PORT || 0) || 1024 + Math.floor(Math.random() * (65536 - 1024)); + const my = defaultGestalt(msgP, { id: "FP-Universal-Client" }); let stype; const connections = 3; @@ -28,16 +27,6 @@ describe("test multiple connections", () => { beforeAll(async () => { auth = await mockJWK(); - // const pair = await SessionTokenService.generateKeyPair(); - // authFactory = await mockGetAuthFactory( - // pair.strings.privateKey, - // { - // userId: "hello", - // tenants: [], - // ledgers: [], - // }, - // sthis - // ); stype = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); const app = new Hono(); diff --git a/vitest.v2-cloud.config.ts b/vitest.v2-cloud.config.ts index 43441c8d..b28ae182 100644 --- a/vitest.v2-cloud.config.ts +++ b/vitest.v2-cloud.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ ], globals: true, setupFiles: "./setup.v2-cloud.ts", + globalSetup: "./globalSetup.v2-cloud.ts", testTimeout: 25000, // poolOptions: { // workers: { wrangler: { configPath: './src/cloud/backend/wrangler.toml' } }, From 5fb17d3da1052ed60bc536d28fb5ffc79edeff19 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Thu, 13 Mar 2025 10:53:04 +0100 Subject: [PATCH 13/14] chore: added D1 and DO test to cloud-gateway --- src/v2-cloud/client/cloud-gateway.test.ts | 245 ++++++++++++---------- src/v2-cloud/test-helper.ts | 6 + 2 files changed, 144 insertions(+), 107 deletions(-) diff --git a/src/v2-cloud/client/cloud-gateway.test.ts b/src/v2-cloud/client/cloud-gateway.test.ts index 45d7ddb3..a520f1e8 100644 --- a/src/v2-cloud/client/cloud-gateway.test.ts +++ b/src/v2-cloud/client/cloud-gateway.test.ts @@ -1,7 +1,15 @@ import { Hono } from "hono"; import { HonoServer } from "../hono-server.js"; import { defaultGestalt } from "../msg-types.js"; -import { NodeHonoServerFactory, CFHonoServerFactory, wsStyle, MockJWK, mockJWK } from "../test-helper.js"; +import { + NodeHonoServerFactory, + CFHonoServerFactory, + wsStyle, + MockJWK, + mockJWK, + httpStyle, + applyBackend, +} from "../test-helper.js"; import { bs, ensureSuperThis, NotFoundError } from "@fireproof/core"; import { defaultMsgParams } from "../msger.js"; import { FireproofCloudGateway, registerFireproofCloudStoreProtocol } from "./gateway.js"; @@ -20,122 +28,145 @@ describe("test multiple connections", () => { describe.each([ // force multi line NodeHonoServerFactory(), - CFHonoServerFactory(sthis, "stream", "D1"), - CFHonoServerFactory(sthis, "stream", "DO"), - ])("$name - Gateway", ({ factory, port }) => { - const reqs = 1; - let style; - + CFHonoServerFactory(sthis), + ])("$name - Gateway", ({ factory, port, name }) => { + const reqs = 10; let server: HonoServer; let gw: bs.Gateway; let unregister: () => void; let url: URI; - beforeAll(async () => { - // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); - style = wsStyle(sthis, auth.applyAuthToURI, port, msgP, my); - const app = new Hono(); - server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => - srv.once(app, port) - ); - unregister = registerFireproofCloudStoreProtocol("fireproof:"); - gw = new FireproofCloudGateway(sthis); - const lurl = auth.applyAuthToURI( - BuildURI.from(`fireproof://localhost:${port}/`) - .setParam("protocol", "http") - .setParam("name", "ledger-name") - .setParam("tenant", "tendant") - ); - url = (await gw.start(lurl, sthis)).Ok(); - }); - afterAll(async () => { - await server.close(); - unregister(); - }); - describe("data", () => { - it("get not found", async () => { - await Promise.all( - Array(reqs) - .fill(async () => { - const my = url.build().setParam("store", "data").URI(); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my, key, sthis)).Ok(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); - }); - it("put - get - del - get", async () => { - await Promise.all( - Array(1) - .fill(async () => { - const resStart = await gw.start(url, sthis); - expect(resStart.isOk()).toBeTruthy(); - - const my = url.build().setParam("store", "data"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl, sthis); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl, sthis); - expect(resDel.isOk()).toBeTruthy(); - - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) + const styles: { name: string; action: () => ReturnType | ReturnType }[] = + name === "NodeHonoServer" + ? [ + // force multiple lines + { name: "http", action: () => httpStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, + { name: "ws", action: () => wsStyle(sthis, auth.applyAuthToURI, port, msgP, my) }, + ] + : [ + { + name: "http-DO", + action: () => httpStyle(sthis, applyBackend("DO", auth.applyAuthToURI), port, msgP, my), + }, + { name: "ws-DO", action: () => wsStyle(sthis, applyBackend("DO", auth.applyAuthToURI), port, msgP, my) }, + { + name: "http-D1", + action: () => httpStyle(sthis, applyBackend("D1", auth.applyAuthToURI), port, msgP, my), + }, + { name: "ws-D1", action: () => wsStyle(sthis, applyBackend("D1", auth.applyAuthToURI), port, msgP, my) }, + ]; + + describe.each(styles)(`${name} - $name`, (styleFn) => { + let style: ReturnType | ReturnType; + + beforeAll(async () => { + // privEnvJWK = await jwk2env(keyPair.privateKey, sthis); + style = styleFn.action(); + const app = new Hono(); + server = await factory(sthis, msgP, style.remoteGestalt, port, auth.keys.strings.publicKey).then((srv) => + srv.once(app, port) + ); + unregister = registerFireproofCloudStoreProtocol("fireproof:"); + gw = new FireproofCloudGateway(sthis); + const lurl = auth.applyAuthToURI( + BuildURI.from(`fireproof://localhost:${port}/`) + .setParam("protocol", "http") + .setParam("name", "ledger-name") + .setParam("tenant", "tendant") ); + url = (await gw.start(lurl, sthis)).Ok(); + }); + afterAll(async () => { + await server.close(); + unregister(); }); - }); - describe("WAL", () => { - it("get not found", async () => { - await Promise.all( - Array(reqs) - .fill(async () => { - const my = url.build().setParam("store", "wal"); - const key = `theDataKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); + describe("data", () => { + it("get not found", async () => { + await Promise.all( + Array(reqs) + .fill(async () => { + const my = url.build().setParam("store", "data").URI(); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my, key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + + it("put - get - del - get", async () => { + await Promise.all( + Array(1) + .fill(async () => { + const resStart = await gw.start(url, sthis); + expect(resStart.isOk()).toBeTruthy(); + + const my = url.build().setParam("store", "data"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl, sthis); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl, sthis); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); }); - it("put - get - del - get", async () => { - await Promise.all( - Array(reqs) - .fill(async () => { - const resStart = await gw.start(url, sthis); - expect(resStart.isOk()).toBeTruthy(); - - const my = url.build().setParam("store", "wal"); - const key = `theWALKey-${sthis.nextId().str}`; - const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); - - const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); - expect(resPut.isOk()).toBeTruthy(); - const resGet = await gw.get(kurl, sthis); - expect(resGet.isOk()).toBeTruthy(); - expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); - const resDel = await gw.delete(kurl, sthis); - expect(resDel.isOk()).toBeTruthy(); - - const res = await gw.get(kurl, sthis); - expect(res.isErr()).toBeTruthy(); - expect(res.Err()).toBeInstanceOf(NotFoundError); - }) - .map((f) => f()) - ); + describe("WAL", () => { + it("get not found", async () => { + await Promise.all( + Array(reqs) + .fill(async () => { + const my = url.build().setParam("store", "wal"); + const key = `theDataKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); + + it("put - get - del - get", async () => { + await Promise.all( + Array(reqs) + .fill(async () => { + const resStart = await gw.start(url, sthis); + expect(resStart.isOk()).toBeTruthy(); + + const my = url.build().setParam("store", "wal"); + const key = `theWALKey-${sthis.nextId().str}`; + const kurl = (await gw.buildUrl(my.URI(), key, sthis)).Ok(); + + const resPut = await gw.put(kurl, sthis.txt.encode("Hello, World!"), sthis); + expect(resPut.isOk()).toBeTruthy(); + const resGet = await gw.get(kurl, sthis); + expect(resGet.isOk()).toBeTruthy(); + expect(sthis.txt.decode(resGet.Ok())).toBe("Hello, World!"); + const resDel = await gw.delete(kurl, sthis); + expect(resDel.isOk()).toBeTruthy(); + + const res = await gw.get(kurl, sthis); + expect(res.isErr()).toBeTruthy(); + expect(res.Err()).toBeInstanceOf(NotFoundError); + }) + .map((f) => f()) + ); + }); }); }); }); diff --git a/src/v2-cloud/test-helper.ts b/src/v2-cloud/test-helper.ts index 93e362d0..45a0233d 100644 --- a/src/v2-cloud/test-helper.ts +++ b/src/v2-cloud/test-helper.ts @@ -338,6 +338,12 @@ export function CFHonoServerFactory(sthis: SuperThis) { }; } +export function applyBackend(backend: "DO" | "D1", fn: (uri: CoerceURI) => URI): (uri: CoerceURI) => URI { + return (uri) => { + return fn(BuildURI.from(uri).setParam("backendMode", backend).URI()); + }; +} + // export async function mockGetAuthFactory(pk: string, factoryTp: TokenForParam, sthis: SuperThis): Promise { // const sts = await SessionTokenService.create( // { From 278123c605c090da60e600aba4107b804ebb9636 Mon Sep 17 00:00:00 2001 From: Meno Abels Date: Mon, 17 Mar 2025 09:47:37 +0100 Subject: [PATCH 14/14] WIP --- src/sts-service/sts-service.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/sts-service/sts-service.ts b/src/sts-service/sts-service.ts index 34912f7b..dd615850 100644 --- a/src/sts-service/sts-service.ts +++ b/src/sts-service/sts-service.ts @@ -4,12 +4,8 @@ import { exportJWK, importJWK, JWTPayload, JWTVerifyResult, jwtVerify, SignJWT } import { generateKeyPair, GenerateKeyPairOptions } from "jose/key/generate/keypair"; import { base58btc } from "multiformats/bases/base58"; -interface BaseTokenParam { - readonly alg: string; // defaults ES256 - readonly issuer: string; - readonly audience: string; - readonly validFor: number; -} + + interface SessionTokenServiceParam extends Partial { readonly token: string; // env encoded jwk } @@ -29,13 +25,6 @@ export async function env2jwk(env: string, alg: string, sthis = ensureSuperThis( return importJWK(inJWT, alg, { extractable: true }) as Promise; } -export interface FPCloudClaim extends JWTPayload { - readonly userId: string; - readonly tenants: { readonly id: string; readonly role: string }[]; - readonly ledgers: { readonly id: string; readonly role: string; readonly right: string }[]; -} - -export type TokenForParam = FPCloudClaim & Partial; export const envKeyDefaults = { SECRET: "CLOUD_SESSION_TOKEN_SECRET",