@qlever-llc/trellis 0.8.4 → 0.9.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/trellis-generate.js +0 -0
- package/esm/auth/browser/login.d.ts.map +1 -1
- package/esm/auth/browser/login.js +46 -3
- package/esm/auth/browser/portal.d.ts.map +1 -1
- package/esm/auth/browser/portal.js +5 -1
- package/esm/auth/browser/session.d.ts +18 -7
- package/esm/auth/browser/session.d.ts.map +1 -1
- package/esm/auth/browser/session.js +47 -11
- package/esm/auth/browser/storage.d.ts +6 -1
- package/esm/auth/browser/storage.d.ts.map +1 -1
- package/esm/auth/browser/storage.js +15 -3
- package/esm/auth/browser.d.ts +2 -2
- package/esm/auth/browser.d.ts.map +1 -1
- package/esm/auth/browser.js +1 -1
- package/esm/auth/device_activation.d.ts +36 -33
- package/esm/auth/device_activation.d.ts.map +1 -1
- package/esm/auth/device_activation.js +26 -22
- package/esm/auth/mod.d.ts +4 -4
- package/esm/auth/mod.d.ts.map +1 -1
- package/esm/auth/mod.js +2 -2
- package/esm/auth/proof.d.ts +3 -1
- package/esm/auth/proof.d.ts.map +1 -1
- package/esm/auth/proof.js +21 -15
- package/esm/auth/protocol.d.ts +2457 -941
- package/esm/auth/protocol.d.ts.map +1 -1
- package/esm/auth/protocol.js +747 -375
- package/esm/auth/schemas.d.ts +25 -4
- package/esm/auth/schemas.d.ts.map +1 -1
- package/esm/auth/schemas.js +14 -4
- package/esm/auth/session_auth.d.ts +1 -1
- package/esm/auth/session_auth.d.ts.map +1 -1
- package/esm/auth/session_auth.js +7 -1
- package/esm/client_connect.d.ts +2 -0
- package/esm/client_connect.d.ts.map +1 -1
- package/esm/client_connect.js +76 -15
- package/esm/contract.d.ts +3 -0
- package/esm/contract.d.ts.map +1 -1
- package/esm/contract_support/mod.d.ts +422 -43
- package/esm/contract_support/mod.d.ts.map +1 -1
- package/esm/contract_support/mod.js +734 -33
- package/esm/contract_support/protocol.d.ts +20 -5
- package/esm/contract_support/protocol.d.ts.map +1 -1
- package/esm/contract_support/protocol.js +18 -10
- package/esm/contract_support/runtime.d.ts +11 -0
- package/esm/contract_support/runtime.d.ts.map +1 -1
- package/esm/contract_support/schema_pointers.d.ts.map +1 -1
- package/esm/contract_support/schema_pointers.js +32 -14
- package/esm/device.d.ts +2 -0
- package/esm/device.d.ts.map +1 -1
- package/esm/device.js +3 -0
- package/esm/errors/AuthError.d.ts +2 -1
- package/esm/errors/AuthError.d.ts.map +1 -1
- package/esm/errors/AuthError.js +8 -3
- package/esm/errors/index.d.ts +4 -4
- package/esm/errors/index.d.ts.map +1 -1
- package/esm/errors/index.js +1 -0
- package/esm/generated-sdk/auth/api.d.ts +27 -9
- package/esm/generated-sdk/auth/api.d.ts.map +1 -1
- package/esm/generated-sdk/auth/api.js +16 -590
- package/esm/generated-sdk/auth/client.d.ts +91 -85
- package/esm/generated-sdk/auth/client.d.ts.map +1 -1
- package/esm/generated-sdk/auth/contract.d.ts +1 -1
- package/esm/generated-sdk/auth/contract.d.ts.map +1 -1
- package/esm/generated-sdk/auth/contract.js +4 -2
- package/esm/generated-sdk/auth/mod.d.ts +1 -0
- package/esm/generated-sdk/auth/mod.d.ts.map +1 -1
- package/esm/generated-sdk/auth/owned_api.d.ts +3 -0
- package/esm/generated-sdk/auth/owned_api.d.ts.map +1 -0
- package/esm/generated-sdk/auth/owned_api.js +594 -0
- package/esm/generated-sdk/auth/schemas.d.ts +9959 -5160
- package/esm/generated-sdk/auth/schemas.d.ts.map +1 -1
- package/esm/generated-sdk/auth/schemas.js +136 -137
- package/esm/generated-sdk/auth/types.d.ts +2418 -1557
- package/esm/generated-sdk/auth/types.d.ts.map +1 -1
- package/esm/generated-sdk/auth/types.js +1 -1
- package/esm/generated-sdk/health/api.d.ts +24 -9
- package/esm/generated-sdk/health/api.d.ts.map +1 -1
- package/esm/generated-sdk/health/api.js +12 -20
- package/esm/generated-sdk/health/client.d.ts +2 -1
- package/esm/generated-sdk/health/client.d.ts.map +1 -1
- package/esm/generated-sdk/health/contract.d.ts.map +1 -1
- package/esm/generated-sdk/health/contract.js +2 -0
- package/esm/generated-sdk/health/owned_api.d.ts +3 -0
- package/esm/generated-sdk/health/owned_api.d.ts.map +1 -0
- package/esm/generated-sdk/health/owned_api.js +16 -0
- package/esm/generated-sdk/health/types.d.ts +2 -0
- package/esm/generated-sdk/health/types.d.ts.map +1 -1
- package/esm/generated-sdk/jobs/api.d.ts +33 -9
- package/esm/generated-sdk/jobs/api.d.ts.map +1 -1
- package/esm/generated-sdk/jobs/api.js +22 -87
- package/esm/generated-sdk/jobs/client.d.ts +9 -2
- package/esm/generated-sdk/jobs/client.d.ts.map +1 -1
- package/esm/generated-sdk/jobs/contract.d.ts +1 -1
- package/esm/generated-sdk/jobs/contract.d.ts.map +1 -1
- package/esm/generated-sdk/jobs/contract.js +4 -2
- package/esm/generated-sdk/jobs/owned_api.d.ts +3 -0
- package/esm/generated-sdk/jobs/owned_api.d.ts.map +1 -0
- package/esm/generated-sdk/jobs/owned_api.js +118 -0
- package/esm/generated-sdk/jobs/schemas.d.ts +336 -123
- package/esm/generated-sdk/jobs/schemas.d.ts.map +1 -1
- package/esm/generated-sdk/jobs/schemas.js +17 -15
- package/esm/generated-sdk/jobs/types.d.ts +144 -34
- package/esm/generated-sdk/jobs/types.d.ts.map +1 -1
- package/esm/generated-sdk/jobs/types.js +36 -1
- package/esm/generated-sdk/state/api.d.ts +27 -9
- package/esm/generated-sdk/state/api.d.ts.map +1 -1
- package/esm/generated-sdk/state/api.js +16 -71
- package/esm/generated-sdk/state/client.d.ts +4 -2
- package/esm/generated-sdk/state/client.d.ts.map +1 -1
- package/esm/generated-sdk/state/contract.d.ts +1 -1
- package/esm/generated-sdk/state/contract.d.ts.map +1 -1
- package/esm/generated-sdk/state/contract.js +4 -2
- package/esm/generated-sdk/state/owned_api.d.ts +3 -0
- package/esm/generated-sdk/state/owned_api.d.ts.map +1 -0
- package/esm/generated-sdk/state/owned_api.js +66 -0
- package/esm/generated-sdk/state/schemas.d.ts +264 -284
- package/esm/generated-sdk/state/schemas.d.ts.map +1 -1
- package/esm/generated-sdk/state/schemas.js +6 -6
- package/esm/generated-sdk/state/types.d.ts +24 -23
- package/esm/generated-sdk/state/types.d.ts.map +1 -1
- package/esm/generated-sdk/state/types.js +1 -1
- package/esm/generated-sdk/trellis-core/api.d.ts +27 -9
- package/esm/generated-sdk/trellis-core/api.d.ts.map +1 -1
- package/esm/generated-sdk/trellis-core/api.js +16 -39
- package/esm/generated-sdk/trellis-core/client.d.ts +5 -2
- package/esm/generated-sdk/trellis-core/client.d.ts.map +1 -1
- package/esm/generated-sdk/trellis-core/contract.d.ts +1 -1
- package/esm/generated-sdk/trellis-core/contract.d.ts.map +1 -1
- package/esm/generated-sdk/trellis-core/contract.js +4 -2
- package/esm/generated-sdk/trellis-core/owned_api.d.ts +3 -0
- package/esm/generated-sdk/trellis-core/owned_api.d.ts.map +1 -0
- package/esm/generated-sdk/trellis-core/owned_api.js +42 -0
- package/esm/generated-sdk/trellis-core/schemas.d.ts +259 -11
- package/esm/generated-sdk/trellis-core/schemas.d.ts.map +1 -1
- package/esm/generated-sdk/trellis-core/schemas.js +5 -3
- package/esm/generated-sdk/trellis-core/types.d.ts +56 -1
- package/esm/generated-sdk/trellis-core/types.d.ts.map +1 -1
- package/esm/generated-sdk/trellis-core/types.js +1 -1
- package/esm/helpers.d.ts.map +1 -1
- package/esm/index.d.ts +4 -3
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/jobs.d.ts +10 -1
- package/esm/jobs.d.ts.map +1 -1
- package/esm/jobs.js +16 -1
- package/esm/kv.d.ts.map +1 -1
- package/esm/kv.js +10 -4
- package/esm/models/auth/rpc/Logout.d.ts +4 -4
- package/esm/models/auth/rpc/Logout.d.ts.map +1 -1
- package/esm/models/auth/rpc/Logout.js +2 -2
- package/esm/models/trellis/Page.d.ts +2 -0
- package/esm/models/trellis/Page.d.ts.map +1 -0
- package/esm/models/trellis/Page.js +1 -0
- package/esm/models/trellis/State.d.ts +1 -0
- package/esm/models/trellis/State.d.ts.map +1 -1
- package/esm/models/trellis/State.js +1 -0
- package/esm/models/trellis/rpc/StateList.d.ts +9 -12
- package/esm/models/trellis/rpc/StateList.d.ts.map +1 -1
- package/esm/models/trellis/rpc/StateList.js +16 -18
- package/esm/npm/src/auth/browser/login.d.ts.map +1 -1
- package/esm/npm/src/auth/browser/login.js +46 -3
- package/esm/npm/src/auth/browser/portal.d.ts.map +1 -1
- package/esm/npm/src/auth/browser/portal.js +5 -1
- package/esm/npm/src/auth/browser/session.d.ts +18 -7
- package/esm/npm/src/auth/browser/session.d.ts.map +1 -1
- package/esm/npm/src/auth/browser/session.js +47 -11
- package/esm/npm/src/auth/browser/storage.d.ts +6 -1
- package/esm/npm/src/auth/browser/storage.d.ts.map +1 -1
- package/esm/npm/src/auth/browser/storage.js +15 -3
- package/esm/npm/src/auth/browser.d.ts +2 -2
- package/esm/npm/src/auth/browser.d.ts.map +1 -1
- package/esm/npm/src/auth/browser.js +1 -1
- package/esm/npm/src/auth/device_activation.d.ts +36 -33
- package/esm/npm/src/auth/device_activation.d.ts.map +1 -1
- package/esm/npm/src/auth/device_activation.js +26 -22
- package/esm/npm/src/auth/mod.d.ts +4 -4
- package/esm/npm/src/auth/mod.d.ts.map +1 -1
- package/esm/npm/src/auth/mod.js +2 -2
- package/esm/npm/src/auth/proof.d.ts +3 -1
- package/esm/npm/src/auth/proof.d.ts.map +1 -1
- package/esm/npm/src/auth/proof.js +21 -15
- package/esm/npm/src/auth/protocol.d.ts +2457 -941
- package/esm/npm/src/auth/protocol.d.ts.map +1 -1
- package/esm/npm/src/auth/protocol.js +747 -375
- package/esm/npm/src/auth/schemas.d.ts +25 -4
- package/esm/npm/src/auth/schemas.d.ts.map +1 -1
- package/esm/npm/src/auth/schemas.js +14 -4
- package/esm/npm/src/auth/session_auth.d.ts +1 -1
- package/esm/npm/src/auth/session_auth.d.ts.map +1 -1
- package/esm/npm/src/auth/session_auth.js +7 -1
- package/esm/npm/src/client_connect.d.ts +2 -0
- package/esm/npm/src/client_connect.d.ts.map +1 -1
- package/esm/npm/src/client_connect.js +76 -15
- package/esm/npm/src/contract.d.ts +3 -0
- package/esm/npm/src/contract.d.ts.map +1 -1
- package/esm/npm/src/contract_support/mod.d.ts +422 -43
- package/esm/npm/src/contract_support/mod.d.ts.map +1 -1
- package/esm/npm/src/contract_support/mod.js +734 -33
- package/esm/npm/src/contract_support/protocol.d.ts +20 -5
- package/esm/npm/src/contract_support/protocol.d.ts.map +1 -1
- package/esm/npm/src/contract_support/protocol.js +18 -10
- package/esm/npm/src/contract_support/runtime.d.ts +11 -0
- package/esm/npm/src/contract_support/runtime.d.ts.map +1 -1
- package/esm/npm/src/contract_support/schema_pointers.d.ts.map +1 -1
- package/esm/npm/src/contract_support/schema_pointers.js +32 -14
- package/esm/npm/src/device/deno.d.ts.map +1 -1
- package/esm/npm/src/device/deno.js +6 -0
- package/esm/npm/src/device.d.ts +2 -0
- package/esm/npm/src/device.d.ts.map +1 -1
- package/esm/npm/src/device.js +3 -0
- package/esm/npm/src/errors/AuthError.d.ts +2 -1
- package/esm/npm/src/errors/AuthError.d.ts.map +1 -1
- package/esm/npm/src/errors/AuthError.js +8 -3
- package/esm/npm/src/errors/index.d.ts +4 -4
- package/esm/npm/src/errors/index.d.ts.map +1 -1
- package/esm/npm/src/errors/index.js +1 -0
- package/esm/npm/src/generate.js +39 -26
- package/esm/npm/src/helpers.d.ts.map +1 -1
- package/esm/npm/src/index.d.ts +4 -3
- package/esm/npm/src/index.d.ts.map +1 -1
- package/esm/npm/src/index.js +1 -0
- package/esm/npm/src/jobs.d.ts +10 -1
- package/esm/npm/src/jobs.d.ts.map +1 -1
- package/esm/npm/src/jobs.js +16 -1
- package/esm/npm/src/kv.d.ts.map +1 -1
- package/esm/npm/src/kv.js +10 -4
- package/esm/npm/src/models/auth/rpc/Logout.d.ts +4 -4
- package/esm/npm/src/models/auth/rpc/Logout.d.ts.map +1 -1
- package/esm/npm/src/models/auth/rpc/Logout.js +2 -2
- package/esm/npm/src/models/trellis/Page.d.ts +2 -0
- package/esm/npm/src/models/trellis/Page.d.ts.map +1 -0
- package/esm/npm/src/models/trellis/Page.js +1 -0
- package/esm/npm/src/models/trellis/State.d.ts +1 -0
- package/esm/npm/src/models/trellis/State.d.ts.map +1 -1
- package/esm/npm/src/models/trellis/State.js +1 -0
- package/esm/npm/src/models/trellis/rpc/StateList.d.ts +9 -12
- package/esm/npm/src/models/trellis/rpc/StateList.d.ts.map +1 -1
- package/esm/npm/src/models/trellis/rpc/StateList.js +16 -18
- package/esm/npm/src/operations.d.ts +16 -7
- package/esm/npm/src/operations.d.ts.map +1 -1
- package/esm/npm/src/operations.js +84 -19
- package/esm/npm/src/runtime_transport.d.ts +2 -0
- package/esm/npm/src/runtime_transport.d.ts.map +1 -1
- package/esm/npm/src/runtime_transport.js +1 -0
- package/esm/npm/src/server/internal_jobs/active-job.d.ts +2 -1
- package/esm/npm/src/server/internal_jobs/active-job.d.ts.map +1 -1
- package/esm/npm/src/server/internal_jobs/active-job.js +3 -0
- package/esm/npm/src/server/internal_jobs/job-manager.d.ts +4 -1
- package/esm/npm/src/server/internal_jobs/job-manager.d.ts.map +1 -1
- package/esm/npm/src/server/internal_jobs/job-manager.js +61 -1
- package/esm/npm/src/server/internal_jobs/projection.js +1 -0
- package/esm/npm/src/server/internal_jobs/runtime-worker.d.ts +13 -1
- package/esm/npm/src/server/internal_jobs/runtime-worker.d.ts.map +1 -1
- package/esm/npm/src/server/internal_jobs/runtime-worker.js +73 -13
- package/esm/npm/src/server/internal_jobs/types.d.ts +19 -0
- package/esm/npm/src/server/internal_jobs/types.d.ts.map +1 -1
- package/esm/npm/src/server/internal_jobs/types.js +10 -0
- package/esm/npm/src/server/runtime.d.ts +1 -0
- package/esm/npm/src/server/runtime.d.ts.map +1 -1
- package/esm/npm/src/server/service.d.ts +10 -1
- package/esm/npm/src/server/service.d.ts.map +1 -1
- package/esm/npm/src/server/service.js +210 -64
- package/esm/npm/src/server/transfer.d.ts.map +1 -1
- package/esm/npm/src/server/transfer.js +4 -0
- package/esm/npm/src/server.d.ts.map +1 -1
- package/esm/npm/src/server.js +337 -34
- package/esm/npm/src/store.d.ts +8 -1
- package/esm/npm/src/store.d.ts.map +1 -1
- package/esm/npm/src/store.js +46 -8
- package/esm/npm/src/transfer.d.ts +3 -0
- package/esm/npm/src/transfer.d.ts.map +1 -1
- package/esm/npm/src/transfer.js +20 -30
- package/esm/npm/src/trellis.d.ts +85 -22
- package/esm/npm/src/trellis.d.ts.map +1 -1
- package/esm/npm/src/trellis.js +525 -61
- package/esm/operations.d.ts +16 -7
- package/esm/operations.d.ts.map +1 -1
- package/esm/operations.js +84 -19
- package/esm/runtime_transport.d.ts +2 -0
- package/esm/runtime_transport.d.ts.map +1 -1
- package/esm/runtime_transport.js +1 -0
- package/esm/store.d.ts +8 -1
- package/esm/store.d.ts.map +1 -1
- package/esm/store.js +46 -8
- package/esm/transfer.d.ts +3 -0
- package/esm/transfer.d.ts.map +1 -1
- package/esm/transfer.js +20 -30
- package/esm/trellis.d.ts +85 -22
- package/esm/trellis.d.ts.map +1 -1
- package/esm/trellis.js +525 -61
- package/package.json +6 -3
- package/script/auth/browser/login.d.ts.map +1 -1
- package/script/auth/browser/login.js +46 -3
- package/script/auth/browser/portal.d.ts.map +1 -1
- package/script/auth/browser/portal.js +5 -1
- package/script/auth/browser/session.d.ts +18 -7
- package/script/auth/browser/session.d.ts.map +1 -1
- package/script/auth/browser/session.js +47 -11
- package/script/auth/browser/storage.d.ts +6 -1
- package/script/auth/browser/storage.d.ts.map +1 -1
- package/script/auth/browser/storage.js +15 -3
- package/script/auth/browser.d.ts +2 -2
- package/script/auth/browser.d.ts.map +1 -1
- package/script/auth/browser.js +2 -1
- package/script/auth/device_activation.d.ts +36 -33
- package/script/auth/device_activation.d.ts.map +1 -1
- package/script/auth/device_activation.js +25 -21
- package/script/auth/mod.d.ts +4 -4
- package/script/auth/mod.d.ts.map +1 -1
- package/script/auth/mod.js +132 -137
- package/script/auth/proof.d.ts +3 -1
- package/script/auth/proof.d.ts.map +1 -1
- package/script/auth/proof.js +21 -15
- package/script/auth/protocol.d.ts +2457 -941
- package/script/auth/protocol.d.ts.map +1 -1
- package/script/auth/protocol.js +749 -377
- package/script/auth/schemas.d.ts +25 -4
- package/script/auth/schemas.d.ts.map +1 -1
- package/script/auth/schemas.js +16 -5
- package/script/auth/session_auth.d.ts +1 -1
- package/script/auth/session_auth.d.ts.map +1 -1
- package/script/auth/session_auth.js +7 -1
- package/script/client_connect.d.ts +2 -0
- package/script/client_connect.d.ts.map +1 -1
- package/script/client_connect.js +76 -15
- package/script/contract.d.ts +3 -0
- package/script/contract.d.ts.map +1 -1
- package/script/contract_support/mod.d.ts +422 -43
- package/script/contract_support/mod.d.ts.map +1 -1
- package/script/contract_support/mod.js +757 -51
- package/script/contract_support/protocol.d.ts +20 -5
- package/script/contract_support/protocol.d.ts.map +1 -1
- package/script/contract_support/protocol.js +20 -11
- package/script/contract_support/runtime.d.ts +11 -0
- package/script/contract_support/runtime.d.ts.map +1 -1
- package/script/contract_support/schema_pointers.d.ts.map +1 -1
- package/script/contract_support/schema_pointers.js +32 -14
- package/script/device.d.ts +2 -0
- package/script/device.d.ts.map +1 -1
- package/script/device.js +3 -0
- package/script/errors/AuthError.d.ts +2 -1
- package/script/errors/AuthError.d.ts.map +1 -1
- package/script/errors/AuthError.js +8 -3
- package/script/errors/index.d.ts +4 -4
- package/script/errors/index.d.ts.map +1 -1
- package/script/errors/index.js +1 -0
- package/script/generated-sdk/auth/api.d.ts +27 -9
- package/script/generated-sdk/auth/api.d.ts.map +1 -1
- package/script/generated-sdk/auth/api.js +17 -591
- package/script/generated-sdk/auth/client.d.ts +91 -85
- package/script/generated-sdk/auth/client.d.ts.map +1 -1
- package/script/generated-sdk/auth/contract.d.ts +1 -1
- package/script/generated-sdk/auth/contract.d.ts.map +1 -1
- package/script/generated-sdk/auth/contract.js +4 -2
- package/script/generated-sdk/auth/mod.d.ts +1 -0
- package/script/generated-sdk/auth/mod.d.ts.map +1 -1
- package/script/generated-sdk/auth/owned_api.d.ts +3 -0
- package/script/generated-sdk/auth/owned_api.d.ts.map +1 -0
- package/script/generated-sdk/auth/owned_api.js +597 -0
- package/script/generated-sdk/auth/schemas.d.ts +9959 -5160
- package/script/generated-sdk/auth/schemas.d.ts.map +1 -1
- package/script/generated-sdk/auth/schemas.js +139 -140
- package/script/generated-sdk/auth/types.d.ts +2418 -1557
- package/script/generated-sdk/auth/types.d.ts.map +1 -1
- package/script/generated-sdk/auth/types.js +1 -1
- package/script/generated-sdk/health/api.d.ts +24 -9
- package/script/generated-sdk/health/api.d.ts.map +1 -1
- package/script/generated-sdk/health/api.js +13 -21
- package/script/generated-sdk/health/client.d.ts +2 -1
- package/script/generated-sdk/health/client.d.ts.map +1 -1
- package/script/generated-sdk/health/contract.d.ts.map +1 -1
- package/script/generated-sdk/health/contract.js +2 -0
- package/script/generated-sdk/health/owned_api.d.ts +3 -0
- package/script/generated-sdk/health/owned_api.d.ts.map +1 -0
- package/script/generated-sdk/health/owned_api.js +19 -0
- package/script/generated-sdk/health/types.d.ts +2 -0
- package/script/generated-sdk/health/types.d.ts.map +1 -1
- package/script/generated-sdk/jobs/api.d.ts +33 -9
- package/script/generated-sdk/jobs/api.d.ts.map +1 -1
- package/script/generated-sdk/jobs/api.js +23 -88
- package/script/generated-sdk/jobs/client.d.ts +9 -2
- package/script/generated-sdk/jobs/client.d.ts.map +1 -1
- package/script/generated-sdk/jobs/contract.d.ts +1 -1
- package/script/generated-sdk/jobs/contract.d.ts.map +1 -1
- package/script/generated-sdk/jobs/contract.js +4 -2
- package/script/generated-sdk/jobs/owned_api.d.ts +3 -0
- package/script/generated-sdk/jobs/owned_api.d.ts.map +1 -0
- package/script/generated-sdk/jobs/owned_api.js +154 -0
- package/script/generated-sdk/jobs/schemas.d.ts +336 -123
- package/script/generated-sdk/jobs/schemas.d.ts.map +1 -1
- package/script/generated-sdk/jobs/schemas.js +18 -16
- package/script/generated-sdk/jobs/types.d.ts +144 -34
- package/script/generated-sdk/jobs/types.d.ts.map +1 -1
- package/script/generated-sdk/jobs/types.js +38 -2
- package/script/generated-sdk/state/api.d.ts +27 -9
- package/script/generated-sdk/state/api.d.ts.map +1 -1
- package/script/generated-sdk/state/api.js +17 -72
- package/script/generated-sdk/state/client.d.ts +4 -2
- package/script/generated-sdk/state/client.d.ts.map +1 -1
- package/script/generated-sdk/state/contract.d.ts +1 -1
- package/script/generated-sdk/state/contract.d.ts.map +1 -1
- package/script/generated-sdk/state/contract.js +4 -2
- package/script/generated-sdk/state/owned_api.d.ts +3 -0
- package/script/generated-sdk/state/owned_api.d.ts.map +1 -0
- package/script/generated-sdk/state/owned_api.js +69 -0
- package/script/generated-sdk/state/schemas.d.ts +264 -284
- package/script/generated-sdk/state/schemas.d.ts.map +1 -1
- package/script/generated-sdk/state/schemas.js +6 -6
- package/script/generated-sdk/state/types.d.ts +24 -23
- package/script/generated-sdk/state/types.d.ts.map +1 -1
- package/script/generated-sdk/state/types.js +1 -1
- package/script/generated-sdk/trellis-core/api.d.ts +27 -9
- package/script/generated-sdk/trellis-core/api.d.ts.map +1 -1
- package/script/generated-sdk/trellis-core/api.js +17 -40
- package/script/generated-sdk/trellis-core/client.d.ts +5 -2
- package/script/generated-sdk/trellis-core/client.d.ts.map +1 -1
- package/script/generated-sdk/trellis-core/contract.d.ts +1 -1
- package/script/generated-sdk/trellis-core/contract.d.ts.map +1 -1
- package/script/generated-sdk/trellis-core/contract.js +4 -2
- package/script/generated-sdk/trellis-core/owned_api.d.ts +3 -0
- package/script/generated-sdk/trellis-core/owned_api.d.ts.map +1 -0
- package/script/generated-sdk/trellis-core/owned_api.js +45 -0
- package/script/generated-sdk/trellis-core/schemas.d.ts +259 -11
- package/script/generated-sdk/trellis-core/schemas.d.ts.map +1 -1
- package/script/generated-sdk/trellis-core/schemas.js +6 -4
- package/script/generated-sdk/trellis-core/types.d.ts +56 -1
- package/script/generated-sdk/trellis-core/types.d.ts.map +1 -1
- package/script/generated-sdk/trellis-core/types.js +1 -1
- package/script/helpers.d.ts.map +1 -1
- package/script/index.d.ts +4 -3
- package/script/index.d.ts.map +1 -1
- package/script/index.js +5 -2
- package/script/jobs.d.ts +10 -1
- package/script/jobs.d.ts.map +1 -1
- package/script/jobs.js +17 -2
- package/script/kv.d.ts.map +1 -1
- package/script/kv.js +10 -4
- package/script/models/auth/rpc/Logout.d.ts +4 -4
- package/script/models/auth/rpc/Logout.d.ts.map +1 -1
- package/script/models/auth/rpc/Logout.js +3 -3
- package/script/models/trellis/Page.d.ts +2 -0
- package/script/models/trellis/Page.d.ts.map +1 -0
- package/script/models/trellis/Page.js +6 -0
- package/script/models/trellis/State.d.ts +1 -0
- package/script/models/trellis/State.d.ts.map +1 -1
- package/script/models/trellis/State.js +1 -0
- package/script/models/trellis/rpc/StateList.d.ts +9 -12
- package/script/models/trellis/rpc/StateList.d.ts.map +1 -1
- package/script/models/trellis/rpc/StateList.js +16 -18
- package/script/npm/src/auth/browser/login.d.ts.map +1 -1
- package/script/npm/src/auth/browser/login.js +46 -3
- package/script/npm/src/auth/browser/portal.d.ts.map +1 -1
- package/script/npm/src/auth/browser/portal.js +5 -1
- package/script/npm/src/auth/browser/session.d.ts +18 -7
- package/script/npm/src/auth/browser/session.d.ts.map +1 -1
- package/script/npm/src/auth/browser/session.js +47 -11
- package/script/npm/src/auth/browser/storage.d.ts +6 -1
- package/script/npm/src/auth/browser/storage.d.ts.map +1 -1
- package/script/npm/src/auth/browser/storage.js +15 -3
- package/script/npm/src/auth/browser.d.ts +2 -2
- package/script/npm/src/auth/browser.d.ts.map +1 -1
- package/script/npm/src/auth/browser.js +2 -1
- package/script/npm/src/auth/device_activation.d.ts +36 -33
- package/script/npm/src/auth/device_activation.d.ts.map +1 -1
- package/script/npm/src/auth/device_activation.js +25 -21
- package/script/npm/src/auth/mod.d.ts +4 -4
- package/script/npm/src/auth/mod.d.ts.map +1 -1
- package/script/npm/src/auth/mod.js +132 -137
- package/script/npm/src/auth/proof.d.ts +3 -1
- package/script/npm/src/auth/proof.d.ts.map +1 -1
- package/script/npm/src/auth/proof.js +21 -15
- package/script/npm/src/auth/protocol.d.ts +2457 -941
- package/script/npm/src/auth/protocol.d.ts.map +1 -1
- package/script/npm/src/auth/protocol.js +749 -377
- package/script/npm/src/auth/schemas.d.ts +25 -4
- package/script/npm/src/auth/schemas.d.ts.map +1 -1
- package/script/npm/src/auth/schemas.js +16 -5
- package/script/npm/src/auth/session_auth.d.ts +1 -1
- package/script/npm/src/auth/session_auth.d.ts.map +1 -1
- package/script/npm/src/auth/session_auth.js +7 -1
- package/script/npm/src/client_connect.d.ts +2 -0
- package/script/npm/src/client_connect.d.ts.map +1 -1
- package/script/npm/src/client_connect.js +76 -15
- package/script/npm/src/contract.d.ts +3 -0
- package/script/npm/src/contract.d.ts.map +1 -1
- package/script/npm/src/contract_support/mod.d.ts +422 -43
- package/script/npm/src/contract_support/mod.d.ts.map +1 -1
- package/script/npm/src/contract_support/mod.js +757 -51
- package/script/npm/src/contract_support/protocol.d.ts +20 -5
- package/script/npm/src/contract_support/protocol.d.ts.map +1 -1
- package/script/npm/src/contract_support/protocol.js +20 -11
- package/script/npm/src/contract_support/runtime.d.ts +11 -0
- package/script/npm/src/contract_support/runtime.d.ts.map +1 -1
- package/script/npm/src/contract_support/schema_pointers.d.ts.map +1 -1
- package/script/npm/src/contract_support/schema_pointers.js +32 -14
- package/script/npm/src/device/deno.d.ts.map +1 -1
- package/script/npm/src/device/deno.js +6 -0
- package/script/npm/src/device.d.ts +2 -0
- package/script/npm/src/device.d.ts.map +1 -1
- package/script/npm/src/device.js +3 -0
- package/script/npm/src/errors/AuthError.d.ts +2 -1
- package/script/npm/src/errors/AuthError.d.ts.map +1 -1
- package/script/npm/src/errors/AuthError.js +8 -3
- package/script/npm/src/errors/index.d.ts +4 -4
- package/script/npm/src/errors/index.d.ts.map +1 -1
- package/script/npm/src/errors/index.js +1 -0
- package/script/npm/src/generate.js +39 -59
- package/script/npm/src/helpers.d.ts.map +1 -1
- package/script/npm/src/index.d.ts +4 -3
- package/script/npm/src/index.d.ts.map +1 -1
- package/script/npm/src/index.js +5 -2
- package/script/npm/src/jobs.d.ts +10 -1
- package/script/npm/src/jobs.d.ts.map +1 -1
- package/script/npm/src/jobs.js +17 -2
- package/script/npm/src/kv.d.ts.map +1 -1
- package/script/npm/src/kv.js +10 -4
- package/script/npm/src/models/auth/rpc/Logout.d.ts +4 -4
- package/script/npm/src/models/auth/rpc/Logout.d.ts.map +1 -1
- package/script/npm/src/models/auth/rpc/Logout.js +3 -3
- package/script/npm/src/models/trellis/Page.d.ts +2 -0
- package/script/npm/src/models/trellis/Page.d.ts.map +1 -0
- package/script/npm/src/models/trellis/Page.js +6 -0
- package/script/npm/src/models/trellis/State.d.ts +1 -0
- package/script/npm/src/models/trellis/State.d.ts.map +1 -1
- package/script/npm/src/models/trellis/State.js +1 -0
- package/script/npm/src/models/trellis/rpc/StateList.d.ts +9 -12
- package/script/npm/src/models/trellis/rpc/StateList.d.ts.map +1 -1
- package/script/npm/src/models/trellis/rpc/StateList.js +16 -18
- package/script/npm/src/operations.d.ts +16 -7
- package/script/npm/src/operations.d.ts.map +1 -1
- package/script/npm/src/operations.js +84 -19
- package/script/npm/src/runtime_transport.d.ts +2 -0
- package/script/npm/src/runtime_transport.d.ts.map +1 -1
- package/script/npm/src/runtime_transport.js +2 -1
- package/script/npm/src/server/internal_jobs/active-job.d.ts +2 -1
- package/script/npm/src/server/internal_jobs/active-job.d.ts.map +1 -1
- package/script/npm/src/server/internal_jobs/active-job.js +3 -0
- package/script/npm/src/server/internal_jobs/job-manager.d.ts +4 -1
- package/script/npm/src/server/internal_jobs/job-manager.d.ts.map +1 -1
- package/script/npm/src/server/internal_jobs/job-manager.js +61 -1
- package/script/npm/src/server/internal_jobs/projection.js +1 -0
- package/script/npm/src/server/internal_jobs/runtime-worker.d.ts +13 -1
- package/script/npm/src/server/internal_jobs/runtime-worker.d.ts.map +1 -1
- package/script/npm/src/server/internal_jobs/runtime-worker.js +74 -13
- package/script/npm/src/server/internal_jobs/types.d.ts +19 -0
- package/script/npm/src/server/internal_jobs/types.d.ts.map +1 -1
- package/script/npm/src/server/internal_jobs/types.js +11 -1
- package/script/npm/src/server/runtime.d.ts +1 -0
- package/script/npm/src/server/runtime.d.ts.map +1 -1
- package/script/npm/src/server/service.d.ts +10 -1
- package/script/npm/src/server/service.d.ts.map +1 -1
- package/script/npm/src/server/service.js +208 -62
- package/script/npm/src/server/transfer.d.ts.map +1 -1
- package/script/npm/src/server/transfer.js +4 -0
- package/script/npm/src/server.d.ts.map +1 -1
- package/script/npm/src/server.js +336 -33
- package/script/npm/src/store.d.ts +8 -1
- package/script/npm/src/store.d.ts.map +1 -1
- package/script/npm/src/store.js +46 -8
- package/script/npm/src/transfer.d.ts +3 -0
- package/script/npm/src/transfer.d.ts.map +1 -1
- package/script/npm/src/transfer.js +19 -29
- package/script/npm/src/trellis.d.ts +85 -22
- package/script/npm/src/trellis.d.ts.map +1 -1
- package/script/npm/src/trellis.js +525 -61
- package/script/operations.d.ts +16 -7
- package/script/operations.d.ts.map +1 -1
- package/script/operations.js +84 -19
- package/script/runtime_transport.d.ts +2 -0
- package/script/runtime_transport.d.ts.map +1 -1
- package/script/runtime_transport.js +2 -1
- package/script/store.d.ts +8 -1
- package/script/store.d.ts.map +1 -1
- package/script/store.js +46 -8
- package/script/transfer.d.ts +3 -0
- package/script/transfer.d.ts.map +1 -1
- package/script/transfer.js +19 -29
- package/script/trellis.d.ts +85 -22
- package/script/trellis.d.ts.map +1 -1
- package/script/trellis.js +525 -61
- package/src/_dnt.polyfills.ts +274 -0
- package/src/_dnt.shims.ts +64 -0
- package/src/auth/browser/login.ts +295 -0
- package/src/auth/browser/portal.ts +75 -0
- package/src/auth/browser/session.ts +197 -0
- package/src/auth/browser/storage.ts +105 -0
- package/src/auth/browser.ts +82 -0
- package/src/auth/device_activation.ts +715 -0
- package/src/auth/keys.ts +116 -0
- package/src/auth/mod.ts +298 -0
- package/src/auth/proof.ts +111 -0
- package/src/auth/protocol.ts +1629 -0
- package/src/auth/schemas.ts +145 -0
- package/src/auth/session_auth.ts +167 -0
- package/src/auth/time.ts +15 -0
- package/src/auth/trellis_id.ts +9 -0
- package/src/auth/types.ts +4 -0
- package/src/auth/utils.ts +87 -0
- package/src/auth.ts +2 -0
- package/src/browser.ts +8 -0
- package/src/client.ts +164 -0
- package/src/client_connect.ts +1328 -0
- package/src/codec.ts +107 -0
- package/src/connection.ts +466 -0
- package/src/contract.ts +84 -0
- package/src/contract_support/canonical.ts +217 -0
- package/src/contract_support/mod.ts +5079 -0
- package/src/contract_support/protocol.ts +213 -0
- package/src/contract_support/runtime.ts +129 -0
- package/src/contract_support/schema_pointers.ts +161 -0
- package/src/contracts.ts +9 -0
- package/src/device/deno.ts +941 -0
- package/src/device.ts +989 -0
- package/src/env.ts +1 -0
- package/src/errors/AuthError.ts +82 -0
- package/src/errors/KVError.ts +47 -0
- package/src/errors/RemoteError.ts +111 -0
- package/src/errors/StoreError.ts +43 -0
- package/src/errors/TransferError.ts +43 -0
- package/src/errors/TransportError.ts +48 -0
- package/src/errors/TrellisError.ts +20 -0
- package/src/errors/ValidationError.ts +80 -0
- package/src/errors/index.ts +195 -0
- package/src/generate.ts +329 -0
- package/src/globals.ts +26 -0
- package/src/health.ts +28 -0
- package/src/helpers.ts +63 -0
- package/src/host/mod.ts +9 -0
- package/src/host/node.ts +9 -0
- package/src/index.ts +233 -0
- package/src/jobs.ts +344 -0
- package/src/kv.ts +564 -0
- package/src/models/auth/rpc/Logout.ts +15 -0
- package/src/models/trellis/Page.ts +6 -0
- package/src/models/trellis/State.ts +55 -0
- package/src/models/trellis/TrellisError.ts +21 -0
- package/src/models/trellis/rpc/StateDelete.ts +13 -0
- package/src/models/trellis/rpc/StateGet.ts +25 -0
- package/src/models/trellis/rpc/StateList.ts +26 -0
- package/src/models/trellis/rpc/StatePut.ts +42 -0
- package/src/operations.ts +1508 -0
- package/src/runtime_transport.ts +132 -0
- package/src/sdk/auth.ts +2 -0
- package/src/sdk/core.ts +2 -0
- package/src/sdk/health.ts +2 -0
- package/src/sdk/jobs.ts +2 -0
- package/src/sdk/state.ts +2 -0
- package/src/server/health.ts +379 -0
- package/src/server/health_rpc.ts +51 -0
- package/src/server/health_schemas.ts +61 -0
- package/src/server/internal_jobs/active-job.ts +115 -0
- package/src/server/internal_jobs/bindings.ts +26 -0
- package/src/server/internal_jobs/cancellation-registry.ts +71 -0
- package/src/server/internal_jobs/heartbeat.ts +120 -0
- package/src/server/internal_jobs/job-manager.ts +456 -0
- package/src/server/internal_jobs/projection.ts +48 -0
- package/src/server/internal_jobs/runtime-worker.ts +741 -0
- package/src/server/internal_jobs/types.ts +124 -0
- package/src/server/runtime.ts +27 -0
- package/src/server/service.ts +2377 -0
- package/src/server/subscription.ts +143 -0
- package/src/server/transfer.ts +962 -0
- package/src/server.ts +1725 -0
- package/src/server_logger.ts +10 -0
- package/src/service/deno.ts +18 -0
- package/src/service/mod.ts +68 -0
- package/src/service/node.ts +18 -0
- package/src/store.ts +658 -0
- package/src/tasks.ts +34 -0
- package/src/telemetry/carrier.ts +35 -0
- package/src/telemetry/core.ts +31 -0
- package/src/telemetry/env.ts +23 -0
- package/src/telemetry/mod.ts +26 -0
- package/src/telemetry/nats.ts +15 -0
- package/src/telemetry/result.ts +20 -0
- package/src/telemetry/trace.ts +39 -0
- package/src/telemetry/trellis.ts +1 -0
- package/src/tracing.ts +28 -0
- package/src/transfer.ts +602 -0
- package/src/trellis.ts +3650 -0
- package/esm/models/trellis/Paginate.d.ts +0 -7
- package/esm/models/trellis/Paginate.d.ts.map +0 -1
- package/esm/models/trellis/Paginate.js +0 -5
- package/esm/npm/src/models/trellis/Paginate.d.ts +0 -7
- package/esm/npm/src/models/trellis/Paginate.d.ts.map +0 -1
- package/esm/npm/src/models/trellis/Paginate.js +0 -5
- package/script/models/trellis/Paginate.d.ts +0 -7
- package/script/models/trellis/Paginate.d.ts.map +0 -1
- package/script/models/trellis/Paginate.js +0 -11
- package/script/npm/src/models/trellis/Paginate.d.ts +0 -7
- package/script/npm/src/models/trellis/Paginate.d.ts.map +0 -1
- package/script/npm/src/models/trellis/Paginate.js +0 -11
package/src/trellis.ts
ADDED
|
@@ -0,0 +1,3650 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConsumerMessages,
|
|
3
|
+
jetstream,
|
|
4
|
+
type JetStreamClient,
|
|
5
|
+
jetstreamManager,
|
|
6
|
+
} from "@nats-io/jetstream";
|
|
7
|
+
import {
|
|
8
|
+
createInbox,
|
|
9
|
+
headers as natsHeaders,
|
|
10
|
+
type Msg,
|
|
11
|
+
type MsgHdrs,
|
|
12
|
+
type NatsConnection,
|
|
13
|
+
} from "@nats-io/nats-core";
|
|
14
|
+
import type {
|
|
15
|
+
EventDesc,
|
|
16
|
+
FeedDesc,
|
|
17
|
+
InferSchemaType,
|
|
18
|
+
RPCDesc,
|
|
19
|
+
TrellisAPI,
|
|
20
|
+
} from "./contracts.js";
|
|
21
|
+
import {
|
|
22
|
+
CONTRACT_JOBS_METADATA,
|
|
23
|
+
CONTRACT_KV_METADATA,
|
|
24
|
+
CONTRACT_STATE_METADATA,
|
|
25
|
+
type ContractJobsMetadata,
|
|
26
|
+
type ContractKvMetadata,
|
|
27
|
+
} from "./contract_support/mod.js";
|
|
28
|
+
import type { StaticDecode } from "typebox";
|
|
29
|
+
import {
|
|
30
|
+
AuthRequestsValidateResponseSchema,
|
|
31
|
+
AuthRequestsValidateSchema,
|
|
32
|
+
} from "./auth/protocol.js";
|
|
33
|
+
import {
|
|
34
|
+
AsyncResult,
|
|
35
|
+
BaseError,
|
|
36
|
+
err,
|
|
37
|
+
type InferErr,
|
|
38
|
+
isErr,
|
|
39
|
+
type MaybeAsync,
|
|
40
|
+
ok,
|
|
41
|
+
Result,
|
|
42
|
+
} from "@qlever-llc/result";
|
|
43
|
+
import {
|
|
44
|
+
context,
|
|
45
|
+
createNatsHeaderCarrier,
|
|
46
|
+
extractTraceContext,
|
|
47
|
+
injectTraceContext,
|
|
48
|
+
SpanStatusCode,
|
|
49
|
+
startClientSpan,
|
|
50
|
+
startServerSpan,
|
|
51
|
+
trace,
|
|
52
|
+
withSpanAsync,
|
|
53
|
+
} from "./tracing.js";
|
|
54
|
+
import { Type } from "typebox";
|
|
55
|
+
import { AssertError, Pointer } from "typebox/value";
|
|
56
|
+
import { ulid } from "ulid";
|
|
57
|
+
import {
|
|
58
|
+
encodeSchema,
|
|
59
|
+
type JsonValue,
|
|
60
|
+
parse,
|
|
61
|
+
parseSchema,
|
|
62
|
+
parseUnknownSchema,
|
|
63
|
+
} from "./codec.js";
|
|
64
|
+
import {
|
|
65
|
+
AuthError,
|
|
66
|
+
BUILTIN_RPC_ERRORS,
|
|
67
|
+
getBuiltinRpcError,
|
|
68
|
+
type StoreError,
|
|
69
|
+
TransferError,
|
|
70
|
+
TransportError,
|
|
71
|
+
type TrellisErrorInstance,
|
|
72
|
+
type TrellisErrorMap,
|
|
73
|
+
type TrellisErrorName,
|
|
74
|
+
UnexpectedError,
|
|
75
|
+
ValidationError,
|
|
76
|
+
} from "./errors/index.js";
|
|
77
|
+
import { RemoteError } from "./errors/RemoteError.js";
|
|
78
|
+
import { logger, type LoggerLike } from "./globals.js";
|
|
79
|
+
import { TypedKV } from "./kv.js";
|
|
80
|
+
import { TrellisErrorDataSchema } from "./models/trellis/TrellisError.js";
|
|
81
|
+
import type { ActiveJob, JobRef, JobTypeMetadata } from "./jobs.js";
|
|
82
|
+
import type { StoreWaitOptions, TypedStore, TypedStoreEntry } from "./store.js";
|
|
83
|
+
import {
|
|
84
|
+
OperationInvoker,
|
|
85
|
+
type OperationRefData,
|
|
86
|
+
type OperationTransport,
|
|
87
|
+
} from "./operations.js";
|
|
88
|
+
import type { Span } from "./telemetry/mod.js";
|
|
89
|
+
import type { StateDeleteResponse } from "./models/trellis/rpc/StateDelete.js";
|
|
90
|
+
import {
|
|
91
|
+
StateDeleteResponseSchema,
|
|
92
|
+
StateDeleteSchema,
|
|
93
|
+
} from "./models/trellis/rpc/StateDelete.js";
|
|
94
|
+
import type { StateGetResponse } from "./models/trellis/rpc/StateGet.js";
|
|
95
|
+
import {
|
|
96
|
+
StateGetResponseSchema,
|
|
97
|
+
StateGetSchema,
|
|
98
|
+
} from "./models/trellis/rpc/StateGet.js";
|
|
99
|
+
import type { StateListResponse } from "./models/trellis/rpc/StateList.js";
|
|
100
|
+
import {
|
|
101
|
+
StateListResponseSchema,
|
|
102
|
+
StateListSchema,
|
|
103
|
+
} from "./models/trellis/rpc/StateList.js";
|
|
104
|
+
import type { StatePutResponse } from "./models/trellis/rpc/StatePut.js";
|
|
105
|
+
import {
|
|
106
|
+
StatePutResponseSchema,
|
|
107
|
+
StatePutSchema,
|
|
108
|
+
} from "./models/trellis/rpc/StatePut.js";
|
|
109
|
+
import {
|
|
110
|
+
createTransferHandle,
|
|
111
|
+
type FileInfo,
|
|
112
|
+
type ReceiveTransferGrant,
|
|
113
|
+
type ReceiveTransferHandle,
|
|
114
|
+
type SendTransferGrant,
|
|
115
|
+
type SendTransferHandle,
|
|
116
|
+
type TransferBody,
|
|
117
|
+
type TransferGrant,
|
|
118
|
+
} from "./transfer.js";
|
|
119
|
+
import { TrellisTasks } from "./tasks.js";
|
|
120
|
+
import { TrellisConnection } from "./connection.js";
|
|
121
|
+
|
|
122
|
+
export type { NatsConnection } from "@nats-io/nats-core";
|
|
123
|
+
|
|
124
|
+
type RuntimeRpcErrorDesc = {
|
|
125
|
+
type: string;
|
|
126
|
+
schema?: unknown;
|
|
127
|
+
fromSerializable(data: unknown): Error;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type InferRuntimeRpcError<T> = T extends {
|
|
131
|
+
fromSerializable(data: unknown): infer TError;
|
|
132
|
+
} ? TError
|
|
133
|
+
: never;
|
|
134
|
+
|
|
135
|
+
export type AuthRequestsValidateResponse = StaticDecode<
|
|
136
|
+
typeof AuthRequestsValidateResponseSchema
|
|
137
|
+
>;
|
|
138
|
+
export type AuthRequestsValidateInput = StaticDecode<
|
|
139
|
+
typeof AuthRequestsValidateSchema
|
|
140
|
+
>;
|
|
141
|
+
|
|
142
|
+
export type SessionCaller = AuthRequestsValidateResponse["caller"];
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Safely extract JSON from a NATS message.
|
|
146
|
+
* The .json() method can throw if the message data is not valid JSON.
|
|
147
|
+
*/
|
|
148
|
+
export function safeJson(msg: Msg): Result<JsonValue, UnexpectedError> {
|
|
149
|
+
return Result.try(() => msg.json() as JsonValue);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function transportCauseContext(cause: unknown): Record<string, unknown> {
|
|
153
|
+
if (cause instanceof Error) {
|
|
154
|
+
return {
|
|
155
|
+
causeName: cause.name,
|
|
156
|
+
causeMessage: cause.message,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { cause: String(cause) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createTransportError(args: {
|
|
164
|
+
code: string;
|
|
165
|
+
message: string;
|
|
166
|
+
hint: string;
|
|
167
|
+
context?: Record<string, unknown>;
|
|
168
|
+
cause?: unknown;
|
|
169
|
+
}): TransportError {
|
|
170
|
+
return new TransportError({
|
|
171
|
+
code: args.code,
|
|
172
|
+
message: args.message,
|
|
173
|
+
hint: args.hint,
|
|
174
|
+
cause: args.cause,
|
|
175
|
+
context: {
|
|
176
|
+
...(args.context ?? {}),
|
|
177
|
+
...(args.cause === undefined ? {} : transportCauseContext(args.cause)),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function requestFailedTransportError(args: {
|
|
183
|
+
code: string;
|
|
184
|
+
method?: string;
|
|
185
|
+
subject: string;
|
|
186
|
+
hint: string;
|
|
187
|
+
message: string;
|
|
188
|
+
cause?: unknown;
|
|
189
|
+
context?: Record<string, unknown>;
|
|
190
|
+
}): TransportError {
|
|
191
|
+
return createTransportError({
|
|
192
|
+
code: args.code,
|
|
193
|
+
message: args.message,
|
|
194
|
+
hint: args.hint,
|
|
195
|
+
cause: args.cause,
|
|
196
|
+
context: {
|
|
197
|
+
subject: args.subject,
|
|
198
|
+
...(args.method === undefined ? {} : { method: args.method }),
|
|
199
|
+
...(args.context ?? {}),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function classifyRequestTransportFailure(args: {
|
|
205
|
+
method?: string;
|
|
206
|
+
subject: string;
|
|
207
|
+
callerCapabilities?: readonly string[];
|
|
208
|
+
cause: unknown;
|
|
209
|
+
}): TransportError {
|
|
210
|
+
const message = args.cause instanceof Error
|
|
211
|
+
? args.cause.message
|
|
212
|
+
: String(args.cause);
|
|
213
|
+
const isNoResponders = message.includes("no responders");
|
|
214
|
+
const isNatsPermission = message.includes("Permissions Violation");
|
|
215
|
+
|
|
216
|
+
return requestFailedTransportError({
|
|
217
|
+
code: isNoResponders
|
|
218
|
+
? "trellis.request.unavailable"
|
|
219
|
+
: isNatsPermission
|
|
220
|
+
? "trellis.request.denied"
|
|
221
|
+
: "trellis.request.failed",
|
|
222
|
+
message: isNoResponders
|
|
223
|
+
? "Trellis could not reach the requested capability."
|
|
224
|
+
: isNatsPermission
|
|
225
|
+
? "Trellis denied this request."
|
|
226
|
+
: "Trellis could not complete the request.",
|
|
227
|
+
hint: isNoResponders
|
|
228
|
+
? "Check that the target service is installed and reachable, then try again."
|
|
229
|
+
: isNatsPermission
|
|
230
|
+
? "Sign in with a profile that has the required capability, then try again."
|
|
231
|
+
: "Retry the request. If it keeps failing, check Trellis runtime health.",
|
|
232
|
+
cause: args.cause,
|
|
233
|
+
method: args.method,
|
|
234
|
+
subject: args.subject,
|
|
235
|
+
context: {
|
|
236
|
+
...(args.callerCapabilities === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { requiredCapabilities: args.callerCapabilities }),
|
|
239
|
+
noResponders: isNoResponders,
|
|
240
|
+
lowLevelMessage: message,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function encodeRuntimeSchema(
|
|
246
|
+
schema: unknown,
|
|
247
|
+
data: unknown,
|
|
248
|
+
): Result<string, ValidationError | UnexpectedError> {
|
|
249
|
+
return encodeSchema(schema as never, data);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseRuntimeSchema(
|
|
253
|
+
schema: unknown,
|
|
254
|
+
data: JsonValue,
|
|
255
|
+
): Result<unknown, ValidationError | UnexpectedError> {
|
|
256
|
+
return parseUnknownSchema(
|
|
257
|
+
schema as Parameters<typeof parseUnknownSchema>[0],
|
|
258
|
+
data,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function base64urlEncode(data: Uint8Array): string {
|
|
263
|
+
const b64 = btoa(String.fromCharCode(...data));
|
|
264
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function base64urlDecode(s: string): Uint8Array {
|
|
268
|
+
const normalized = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
269
|
+
const padLen = (4 - (normalized.length % 4)) % 4;
|
|
270
|
+
const padded = normalized + "=".repeat(padLen);
|
|
271
|
+
const bin = atob(padded);
|
|
272
|
+
const out = new Uint8Array(bin.length);
|
|
273
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function toArrayBuffer(data: Uint8Array): ArrayBuffer {
|
|
278
|
+
const buf = data.buffer;
|
|
279
|
+
if (buf instanceof ArrayBuffer) {
|
|
280
|
+
return buf.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
281
|
+
}
|
|
282
|
+
const copy = new Uint8Array(data.byteLength);
|
|
283
|
+
copy.set(data);
|
|
284
|
+
return copy.buffer;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function sha256(data: Uint8Array): Promise<Uint8Array> {
|
|
288
|
+
const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
|
|
289
|
+
return new Uint8Array(digest);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function buildProofInput(
|
|
293
|
+
sessionKey: string,
|
|
294
|
+
subject: string,
|
|
295
|
+
payloadHash: Uint8Array,
|
|
296
|
+
iat: number,
|
|
297
|
+
requestId: string,
|
|
298
|
+
): Uint8Array {
|
|
299
|
+
const enc = new TextEncoder();
|
|
300
|
+
const sessionKeyBytes = enc.encode(sessionKey);
|
|
301
|
+
const subjectBytes = enc.encode(subject);
|
|
302
|
+
const iatBytes = enc.encode(String(iat));
|
|
303
|
+
const requestIdBytes = enc.encode(requestId);
|
|
304
|
+
|
|
305
|
+
const buf = new Uint8Array(
|
|
306
|
+
4 +
|
|
307
|
+
sessionKeyBytes.length +
|
|
308
|
+
4 +
|
|
309
|
+
subjectBytes.length +
|
|
310
|
+
4 +
|
|
311
|
+
payloadHash.length +
|
|
312
|
+
4 +
|
|
313
|
+
iatBytes.length +
|
|
314
|
+
4 +
|
|
315
|
+
requestIdBytes.length,
|
|
316
|
+
);
|
|
317
|
+
const view = new DataView(buf.buffer);
|
|
318
|
+
|
|
319
|
+
let offset = 0;
|
|
320
|
+
view.setUint32(offset, sessionKeyBytes.length);
|
|
321
|
+
offset += 4;
|
|
322
|
+
buf.set(sessionKeyBytes, offset);
|
|
323
|
+
offset += sessionKeyBytes.length;
|
|
324
|
+
|
|
325
|
+
view.setUint32(offset, subjectBytes.length);
|
|
326
|
+
offset += 4;
|
|
327
|
+
buf.set(subjectBytes, offset);
|
|
328
|
+
offset += subjectBytes.length;
|
|
329
|
+
|
|
330
|
+
view.setUint32(offset, payloadHash.length);
|
|
331
|
+
offset += 4;
|
|
332
|
+
buf.set(payloadHash, offset);
|
|
333
|
+
offset += payloadHash.length;
|
|
334
|
+
|
|
335
|
+
view.setUint32(offset, iatBytes.length);
|
|
336
|
+
offset += 4;
|
|
337
|
+
buf.set(iatBytes, offset);
|
|
338
|
+
offset += iatBytes.length;
|
|
339
|
+
|
|
340
|
+
view.setUint32(offset, requestIdBytes.length);
|
|
341
|
+
offset += 4;
|
|
342
|
+
buf.set(requestIdBytes, offset);
|
|
343
|
+
|
|
344
|
+
return buf;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export type TrellisSigner = (
|
|
348
|
+
data: Uint8Array,
|
|
349
|
+
) => Promise<Uint8Array> | Uint8Array;
|
|
350
|
+
|
|
351
|
+
export type TrellisAuth = {
|
|
352
|
+
sessionKey: string;
|
|
353
|
+
sign: TrellisSigner;
|
|
354
|
+
currentIat?: () => number;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export type AnyTrellisAPI = TrellisAPI;
|
|
358
|
+
export type TrellisMode = "client" | "server";
|
|
359
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
360
|
+
type OwnedApiFor<TContract> = TContract extends
|
|
361
|
+
{ API: { owned: infer TOwnedApi } }
|
|
362
|
+
? TOwnedApi extends AnyTrellisAPI ? TOwnedApi
|
|
363
|
+
: never
|
|
364
|
+
: never;
|
|
365
|
+
type ContractKvFor<TContract> = TContract extends {
|
|
366
|
+
readonly [CONTRACT_KV_METADATA]?: infer TKv;
|
|
367
|
+
} ? NonNullable<TKv> extends ContractKvMetadata ? NonNullable<TKv>
|
|
368
|
+
: {}
|
|
369
|
+
: {};
|
|
370
|
+
type ContractJobsFor<TContract> = TContract extends {
|
|
371
|
+
readonly [CONTRACT_JOBS_METADATA]?: infer TJobs;
|
|
372
|
+
} ? NonNullable<TJobs> extends ContractJobsMetadata ? NonNullable<TJobs>
|
|
373
|
+
: {}
|
|
374
|
+
: {};
|
|
375
|
+
export type RuntimeStateStoreShape = {
|
|
376
|
+
kind: "value" | "map";
|
|
377
|
+
value: unknown;
|
|
378
|
+
schema?: unknown;
|
|
379
|
+
stateVersion?: string;
|
|
380
|
+
acceptedVersions?: Record<string, unknown>;
|
|
381
|
+
};
|
|
382
|
+
export type RuntimeStateStores = Record<string, RuntimeStateStoreShape>;
|
|
383
|
+
export type RuntimeStateStoresForContract<TContract> = TContract extends {
|
|
384
|
+
readonly [CONTRACT_STATE_METADATA]?: infer TState;
|
|
385
|
+
} ? NonNullable<TState> extends RuntimeStateStores ? NonNullable<TState>
|
|
386
|
+
: {}
|
|
387
|
+
: {};
|
|
388
|
+
type TrellisApiFor<TContract> = TContract extends
|
|
389
|
+
{ API: { trellis: infer TTrellisApi } }
|
|
390
|
+
? TTrellisApi extends AnyTrellisAPI ? TTrellisApi
|
|
391
|
+
: OwnedApiFor<TContract>
|
|
392
|
+
: OwnedApiFor<TContract>;
|
|
393
|
+
type RpcMethodsOf<TA extends AnyTrellisAPI> = TA["rpc"];
|
|
394
|
+
export type MethodsOf<TA extends AnyTrellisAPI> =
|
|
395
|
+
& keyof RpcMethodsOf<TA>
|
|
396
|
+
& string;
|
|
397
|
+
export type RpcMethodNameOf<TA extends AnyTrellisAPI> = MethodsOf<TA>;
|
|
398
|
+
export type OperationsOf<TA extends AnyTrellisAPI> =
|
|
399
|
+
& keyof TA["operations"]
|
|
400
|
+
& string;
|
|
401
|
+
type EventsOf<TA extends AnyTrellisAPI> = keyof TA["events"] & string;
|
|
402
|
+
export type FeedsOf<TA extends AnyTrellisAPI> =
|
|
403
|
+
& keyof NonNullable<TA["feeds"]>
|
|
404
|
+
& string;
|
|
405
|
+
type RpcMethodOf<TA extends AnyTrellisAPI, M extends keyof TA["rpc"] & string> =
|
|
406
|
+
RpcMethodsOf<TA>[M];
|
|
407
|
+
type MethodInputOf<
|
|
408
|
+
TA extends AnyTrellisAPI,
|
|
409
|
+
M extends keyof TA["rpc"] & string,
|
|
410
|
+
> = RpcMethodOf<TA, M> extends { input: infer TInput } ? InferSchemaType<TInput>
|
|
411
|
+
: never;
|
|
412
|
+
export type RpcInputOf<
|
|
413
|
+
TA extends AnyTrellisAPI,
|
|
414
|
+
M extends RpcMethodNameOf<TA>,
|
|
415
|
+
> = MethodInputOf<TA, M>;
|
|
416
|
+
type MethodOutputOf<
|
|
417
|
+
TA extends AnyTrellisAPI,
|
|
418
|
+
M extends keyof TA["rpc"] & string,
|
|
419
|
+
> = RpcMethodOf<TA, M> extends { output: infer TOutput }
|
|
420
|
+
? InferSchemaType<TOutput>
|
|
421
|
+
: never;
|
|
422
|
+
export type RpcOutputOf<
|
|
423
|
+
TA extends AnyTrellisAPI,
|
|
424
|
+
M extends RpcMethodNameOf<TA>,
|
|
425
|
+
> = MethodOutputOf<TA, M>;
|
|
426
|
+
type RpcRequestShapes<TA extends AnyTrellisAPI> = {
|
|
427
|
+
[M in keyof TA["rpc"] & string]: {
|
|
428
|
+
input: MethodInputOf<TA, M>;
|
|
429
|
+
output: MethodOutputOf<TA, M>;
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
type RequestMethodOf<TRequests> = keyof TRequests & string;
|
|
433
|
+
type RequestInputOf<TRequests, M extends RequestMethodOf<TRequests>> =
|
|
434
|
+
TRequests[M] extends { input: infer TInput } ? TInput : never;
|
|
435
|
+
type RequestOutputOf<TRequests, M extends RequestMethodOf<TRequests>> =
|
|
436
|
+
TRequests[M] extends { output: infer TOutput } ? TOutput : never;
|
|
437
|
+
type RpcDescriptorOf<
|
|
438
|
+
TA extends AnyTrellisAPI,
|
|
439
|
+
M extends keyof TA["rpc"] & string,
|
|
440
|
+
> = RpcMethodOf<TA, M> extends {
|
|
441
|
+
input: infer TInput;
|
|
442
|
+
output: infer TOutput;
|
|
443
|
+
errors?: infer TErrors;
|
|
444
|
+
runtimeErrors?: infer TRuntimeErrors;
|
|
445
|
+
declaredErrorTypes?: infer TDeclaredErrorTypes;
|
|
446
|
+
} ? {
|
|
447
|
+
input: TInput;
|
|
448
|
+
output: TOutput;
|
|
449
|
+
errors?: TErrors;
|
|
450
|
+
runtimeErrors?: TRuntimeErrors;
|
|
451
|
+
declaredErrorTypes?: TDeclaredErrorTypes;
|
|
452
|
+
} & RpcMethodOf<TA, M>
|
|
453
|
+
: never;
|
|
454
|
+
type DeclaredBuiltinErrorOf<TNames> = TNames extends readonly (infer TName)[]
|
|
455
|
+
? TName extends TrellisErrorName ? TrellisErrorMap[TName]
|
|
456
|
+
: never
|
|
457
|
+
: never;
|
|
458
|
+
type DeclaredRuntimeErrorOf<TRuntimeErrors> = TRuntimeErrors extends readonly (
|
|
459
|
+
infer TRuntimeError
|
|
460
|
+
)[] ? InferRuntimeRpcError<TRuntimeError>
|
|
461
|
+
: never;
|
|
462
|
+
type MethodDeclaredErrorOf<
|
|
463
|
+
TA extends AnyTrellisAPI,
|
|
464
|
+
M extends keyof TA["rpc"] & string,
|
|
465
|
+
> = RpcDescriptorOf<TA, M> extends {
|
|
466
|
+
errors?: infer TErrors;
|
|
467
|
+
runtimeErrors?: infer TRuntimeErrors;
|
|
468
|
+
} ? DeclaredBuiltinErrorOf<TErrors> | DeclaredRuntimeErrorOf<TRuntimeErrors>
|
|
469
|
+
: never;
|
|
470
|
+
type RequestErrorOf<TA extends AnyTrellisAPI, M extends MethodsOf<TA>> =
|
|
471
|
+
| MethodDeclaredErrorOf<TA, M>
|
|
472
|
+
| RemoteError
|
|
473
|
+
| TransportError
|
|
474
|
+
| ValidationError
|
|
475
|
+
| UnexpectedError;
|
|
476
|
+
type HandlerErrorOf<TA extends AnyTrellisAPI, M extends MethodsOf<TA>> =
|
|
477
|
+
| MethodDeclaredErrorOf<TA, M>
|
|
478
|
+
| TrellisErrorInstance;
|
|
479
|
+
type EventOf<TA extends AnyTrellisAPI, E extends EventsOf<TA>> =
|
|
480
|
+
TA["events"][E] extends EventDesc<infer TEvent> ? InferSchemaType<TEvent>
|
|
481
|
+
: never;
|
|
482
|
+
type EventDescriptorOf<TA extends AnyTrellisAPI, E extends EventsOf<TA>> =
|
|
483
|
+
TA["events"][E] extends EventDesc<infer TEvent>
|
|
484
|
+
? EventDesc<TEvent> & TA["events"][E]
|
|
485
|
+
: never;
|
|
486
|
+
type EventPayloadOf<TA extends AnyTrellisAPI, E extends EventsOf<TA>> = Omit<
|
|
487
|
+
EventOf<TA, E>,
|
|
488
|
+
"header"
|
|
489
|
+
>;
|
|
490
|
+
export type FeedInputOf<TA extends AnyTrellisAPI, F extends FeedsOf<TA>> =
|
|
491
|
+
NonNullable<TA["feeds"]>[F] extends FeedDesc<infer TInput, infer _TEvent>
|
|
492
|
+
? InferSchemaType<TInput>
|
|
493
|
+
: never;
|
|
494
|
+
export type FeedEventOf<TA extends AnyTrellisAPI, F extends FeedsOf<TA>> =
|
|
495
|
+
NonNullable<TA["feeds"]>[F] extends FeedDesc<infer _TInput, infer TEvent>
|
|
496
|
+
? InferSchemaType<TEvent>
|
|
497
|
+
: never;
|
|
498
|
+
type FeedDescriptorOf<TA extends AnyTrellisAPI, F extends FeedsOf<TA>> =
|
|
499
|
+
NonNullable<TA["feeds"]>[F] extends FeedDesc<infer TInput, infer TEvent>
|
|
500
|
+
? FeedDesc<TInput, TEvent> & NonNullable<TA["feeds"]>[F]
|
|
501
|
+
: never;
|
|
502
|
+
export type OperationInputOf<
|
|
503
|
+
TA extends AnyTrellisAPI,
|
|
504
|
+
O extends OperationsOf<TA>,
|
|
505
|
+
> = TA["operations"][O] extends { input: infer TInput }
|
|
506
|
+
? InferSchemaType<TInput>
|
|
507
|
+
: never;
|
|
508
|
+
export type OperationProgressOf<
|
|
509
|
+
TA extends AnyTrellisAPI,
|
|
510
|
+
O extends OperationsOf<TA>,
|
|
511
|
+
> = TA["operations"][O] extends { progress: infer TProgress }
|
|
512
|
+
? TProgress extends undefined ? unknown
|
|
513
|
+
: InferSchemaType<NonNullable<TProgress>>
|
|
514
|
+
: unknown;
|
|
515
|
+
export type OperationOutputOf<
|
|
516
|
+
TA extends AnyTrellisAPI,
|
|
517
|
+
O extends OperationsOf<TA>,
|
|
518
|
+
> = TA["operations"][O] extends { output: infer TOutput }
|
|
519
|
+
? TOutput extends undefined ? unknown : InferSchemaType<NonNullable<TOutput>>
|
|
520
|
+
: unknown;
|
|
521
|
+
export type OperationRuntimeHandle<TProgress = unknown, TOutput = unknown> = {
|
|
522
|
+
id: string;
|
|
523
|
+
started(): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
524
|
+
progress(
|
|
525
|
+
value: TProgress,
|
|
526
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
527
|
+
complete(
|
|
528
|
+
value: TOutput,
|
|
529
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
530
|
+
fail(
|
|
531
|
+
error: BaseError,
|
|
532
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
533
|
+
cancel(): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
534
|
+
attach(
|
|
535
|
+
job: { wait(): AsyncResult<unknown, BaseError> },
|
|
536
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
537
|
+
signals(): AsyncIterable<RuntimeOperationSignal>;
|
|
538
|
+
nextSignal(
|
|
539
|
+
name?: string,
|
|
540
|
+
): AsyncResult<RuntimeOperationSignal, BaseError>;
|
|
541
|
+
defer(): OperationDeferred;
|
|
542
|
+
};
|
|
543
|
+
export type OperationDeferred = {
|
|
544
|
+
kind: "deferred";
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Returns true when a handler result explicitly leaves operation completion to
|
|
549
|
+
* an external control path.
|
|
550
|
+
*/
|
|
551
|
+
export function isOperationDeferred(
|
|
552
|
+
value: unknown,
|
|
553
|
+
): value is OperationDeferred {
|
|
554
|
+
return !!value && typeof value === "object" &&
|
|
555
|
+
"kind" in value && value.kind === "deferred";
|
|
556
|
+
}
|
|
557
|
+
export type AcceptedOperation<TProgress = unknown, TOutput = unknown> =
|
|
558
|
+
& OperationRuntimeHandle<TProgress, TOutput>
|
|
559
|
+
& {
|
|
560
|
+
ref: OperationRefData;
|
|
561
|
+
snapshot: RuntimeOperationSnapshot & {
|
|
562
|
+
progress?: TProgress;
|
|
563
|
+
output?: TOutput;
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
export type OperationTransferHandle = {
|
|
567
|
+
updates(): AsyncIterable<RuntimeOperationTransferProgress>;
|
|
568
|
+
completed(): AsyncResult<FileInfo, TransferError>;
|
|
569
|
+
};
|
|
570
|
+
type StateEntryBase<TValue> = {
|
|
571
|
+
value: TValue;
|
|
572
|
+
revision: string;
|
|
573
|
+
updatedAt: string;
|
|
574
|
+
expiresAt?: string;
|
|
575
|
+
};
|
|
576
|
+
type ValueStateEntry<TValue> = StateEntryBase<TValue>;
|
|
577
|
+
type MapStateEntry<TValue> = StateEntryBase<TValue> & { key: string };
|
|
578
|
+
type StateMigrationRequiredEntry<TEntry> = {
|
|
579
|
+
migrationRequired: true;
|
|
580
|
+
entry: TEntry;
|
|
581
|
+
stateVersion: string;
|
|
582
|
+
currentStateVersion: string;
|
|
583
|
+
writerContractDigest: string;
|
|
584
|
+
};
|
|
585
|
+
type StateGetResult<TStore extends RuntimeStateStoreShape> =
|
|
586
|
+
| { found: false }
|
|
587
|
+
| {
|
|
588
|
+
found: true;
|
|
589
|
+
entry: TStore["kind"] extends "map" ? MapStateEntry<TStore["value"]>
|
|
590
|
+
: ValueStateEntry<TStore["value"]>;
|
|
591
|
+
}
|
|
592
|
+
| StateMigrationRequiredEntry<
|
|
593
|
+
TStore["kind"] extends "map" ? MapStateEntry<unknown>
|
|
594
|
+
: ValueStateEntry<unknown>
|
|
595
|
+
>;
|
|
596
|
+
type StatePutResult<TStore extends RuntimeStateStoreShape> =
|
|
597
|
+
| {
|
|
598
|
+
applied: true;
|
|
599
|
+
entry: TStore["kind"] extends "map" ? MapStateEntry<TStore["value"]>
|
|
600
|
+
: ValueStateEntry<TStore["value"]>;
|
|
601
|
+
}
|
|
602
|
+
| {
|
|
603
|
+
applied: false;
|
|
604
|
+
found: boolean;
|
|
605
|
+
entry?:
|
|
606
|
+
| (TStore["kind"] extends "map" ? MapStateEntry<TStore["value"]>
|
|
607
|
+
: ValueStateEntry<TStore["value"]>)
|
|
608
|
+
| StateMigrationRequiredEntry<
|
|
609
|
+
TStore["kind"] extends "map" ? MapStateEntry<unknown>
|
|
610
|
+
: ValueStateEntry<unknown>
|
|
611
|
+
>;
|
|
612
|
+
};
|
|
613
|
+
type StateDeleteOptions = {
|
|
614
|
+
expectedRevision?: string;
|
|
615
|
+
};
|
|
616
|
+
type StatePutOptions = {
|
|
617
|
+
expectedRevision?: string | null;
|
|
618
|
+
ttlMs?: number;
|
|
619
|
+
};
|
|
620
|
+
type StateListOptions = {
|
|
621
|
+
offset?: number;
|
|
622
|
+
limit?: number;
|
|
623
|
+
};
|
|
624
|
+
export type ValueStateStoreClient<TValue> = {
|
|
625
|
+
get(): AsyncResult<
|
|
626
|
+
StateGetResult<{ kind: "value"; value: TValue }>,
|
|
627
|
+
BaseError
|
|
628
|
+
>;
|
|
629
|
+
put(
|
|
630
|
+
value: TValue,
|
|
631
|
+
opts?: StatePutOptions,
|
|
632
|
+
): AsyncResult<StatePutResult<{ kind: "value"; value: TValue }>, BaseError>;
|
|
633
|
+
delete(
|
|
634
|
+
opts?: StateDeleteOptions,
|
|
635
|
+
): AsyncResult<{ deleted: boolean }, BaseError>;
|
|
636
|
+
};
|
|
637
|
+
export type MapStateStoreClient<TValue> = {
|
|
638
|
+
get(
|
|
639
|
+
key: string,
|
|
640
|
+
): AsyncResult<StateGetResult<{ kind: "map"; value: TValue }>, BaseError>;
|
|
641
|
+
put(
|
|
642
|
+
key: string,
|
|
643
|
+
value: TValue,
|
|
644
|
+
opts?: StatePutOptions,
|
|
645
|
+
): AsyncResult<StatePutResult<{ kind: "map"; value: TValue }>, BaseError>;
|
|
646
|
+
delete(
|
|
647
|
+
key: string,
|
|
648
|
+
opts?: StateDeleteOptions,
|
|
649
|
+
): AsyncResult<{ deleted: boolean }, BaseError>;
|
|
650
|
+
list(opts?: StateListOptions): AsyncResult<{
|
|
651
|
+
entries: Array<
|
|
652
|
+
| MapStateEntry<TValue>
|
|
653
|
+
| StateMigrationRequiredEntry<MapStateEntry<unknown>>
|
|
654
|
+
>;
|
|
655
|
+
count: number;
|
|
656
|
+
offset: number;
|
|
657
|
+
limit: number;
|
|
658
|
+
nextOffset?: number;
|
|
659
|
+
}, BaseError>;
|
|
660
|
+
prefix(path: string): MapStateStoreClient<TValue>;
|
|
661
|
+
};
|
|
662
|
+
export type StateFacade<TState extends RuntimeStateStores> = {
|
|
663
|
+
[K in keyof TState]: TState[K]["kind"] extends "map"
|
|
664
|
+
? MapStateStoreClient<TState[K]["value"]>
|
|
665
|
+
: ValueStateStoreClient<TState[K]["value"]>;
|
|
666
|
+
};
|
|
667
|
+
export type OperationHandlerContext<
|
|
668
|
+
TInput,
|
|
669
|
+
TProgress = unknown,
|
|
670
|
+
TOutput = unknown,
|
|
671
|
+
TTransfer = undefined,
|
|
672
|
+
> = {
|
|
673
|
+
input: TInput;
|
|
674
|
+
op: OperationRuntimeHandle<TProgress, TOutput>;
|
|
675
|
+
caller: SessionCaller;
|
|
676
|
+
} & (TTransfer extends undefined ? {} : { transfer: TTransfer });
|
|
677
|
+
export type OperationRegistration<
|
|
678
|
+
TInput,
|
|
679
|
+
TProgress = unknown,
|
|
680
|
+
TOutput = unknown,
|
|
681
|
+
TTransfer = undefined,
|
|
682
|
+
> = {
|
|
683
|
+
accept(args: {
|
|
684
|
+
sessionKey: string;
|
|
685
|
+
}): AsyncResult<AcceptedOperation<TProgress, TOutput>, UnexpectedError>;
|
|
686
|
+
/**
|
|
687
|
+
* Loads an existing operation by id and returns a service-side control handle.
|
|
688
|
+
* The operation must belong to this service and registration name.
|
|
689
|
+
*/
|
|
690
|
+
control(
|
|
691
|
+
operationId: string,
|
|
692
|
+
): AsyncResult<OperationRuntimeHandle<TProgress, TOutput>, BaseError>;
|
|
693
|
+
handle(
|
|
694
|
+
handler: (
|
|
695
|
+
context: OperationHandlerContext<TInput, TProgress, TOutput, TTransfer>,
|
|
696
|
+
) => unknown | Promise<unknown>,
|
|
697
|
+
): Promise<void>;
|
|
698
|
+
};
|
|
699
|
+
export type OperationTransferContextOf<
|
|
700
|
+
TA extends AnyTrellisAPI,
|
|
701
|
+
O extends OperationsOf<TA>,
|
|
702
|
+
> = TA["operations"][O] extends { transfer: infer TTransfer }
|
|
703
|
+
? TTransfer extends undefined ? undefined
|
|
704
|
+
: OperationTransferHandle
|
|
705
|
+
: undefined;
|
|
706
|
+
export type OperationSurface<
|
|
707
|
+
TA extends AnyTrellisAPI,
|
|
708
|
+
TMode extends TrellisMode,
|
|
709
|
+
O extends OperationsOf<TA>,
|
|
710
|
+
> = TMode extends "server" ? OperationRegistration<
|
|
711
|
+
OperationInputOf<TA, O>,
|
|
712
|
+
OperationProgressOf<TA, O>,
|
|
713
|
+
OperationOutputOf<TA, O>,
|
|
714
|
+
OperationTransferContextOf<TA, O>
|
|
715
|
+
>
|
|
716
|
+
: OperationInvoker<TA["operations"][O] & RuntimeOperationDesc>;
|
|
717
|
+
|
|
718
|
+
export function isResultLike(
|
|
719
|
+
value: unknown,
|
|
720
|
+
): value is Result<unknown, BaseError> {
|
|
721
|
+
return value instanceof Result;
|
|
722
|
+
}
|
|
723
|
+
export type RuntimeOperationDesc = {
|
|
724
|
+
subject: string;
|
|
725
|
+
input: unknown;
|
|
726
|
+
progress?: unknown;
|
|
727
|
+
output?: unknown;
|
|
728
|
+
signals?: Record<string, { input: unknown }>;
|
|
729
|
+
cancelCapabilities?: readonly string[];
|
|
730
|
+
controlCapabilities?: readonly string[];
|
|
731
|
+
transfer?: {
|
|
732
|
+
store: string;
|
|
733
|
+
key: `/${string}`;
|
|
734
|
+
contentType?: `/${string}`;
|
|
735
|
+
metadata?: `/${string}`;
|
|
736
|
+
expiresInMs?: number;
|
|
737
|
+
maxBytes?: number;
|
|
738
|
+
};
|
|
739
|
+
cancel?: boolean;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
export type RuntimeOperationSignal = {
|
|
743
|
+
operationId: string;
|
|
744
|
+
sequence: number;
|
|
745
|
+
signal: string;
|
|
746
|
+
input?: JsonValue;
|
|
747
|
+
acceptedAt: string;
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
export type RuntimeOperationSignalWaiter = (
|
|
751
|
+
result: Result<RuntimeOperationSignal, BaseError>,
|
|
752
|
+
) => void;
|
|
753
|
+
|
|
754
|
+
export type RuntimeOperationTransferProgress = {
|
|
755
|
+
chunkIndex: number;
|
|
756
|
+
chunkBytes: number;
|
|
757
|
+
transferredBytes: number;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
export type RuntimeOperationState =
|
|
761
|
+
| "pending"
|
|
762
|
+
| "running"
|
|
763
|
+
| "completed"
|
|
764
|
+
| "failed"
|
|
765
|
+
| "cancelled";
|
|
766
|
+
|
|
767
|
+
export type RuntimeOperationSnapshot = {
|
|
768
|
+
id: string;
|
|
769
|
+
service: string;
|
|
770
|
+
operation: string;
|
|
771
|
+
revision: number;
|
|
772
|
+
state: RuntimeOperationState;
|
|
773
|
+
createdAt: string;
|
|
774
|
+
updatedAt: string;
|
|
775
|
+
completedAt?: string;
|
|
776
|
+
progress?: unknown;
|
|
777
|
+
transfer?: RuntimeOperationTransferProgress;
|
|
778
|
+
output?: unknown;
|
|
779
|
+
error?: {
|
|
780
|
+
type: string;
|
|
781
|
+
message: string;
|
|
782
|
+
};
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
export type RuntimeOperationRecord = {
|
|
786
|
+
id: string;
|
|
787
|
+
service: string;
|
|
788
|
+
operation: string;
|
|
789
|
+
ownerSessionKey: string;
|
|
790
|
+
snapshot: RuntimeOperationSnapshot;
|
|
791
|
+
sequence: number;
|
|
792
|
+
signalSequence: number;
|
|
793
|
+
signals: RuntimeOperationSignal[];
|
|
794
|
+
terminal: boolean;
|
|
795
|
+
watchers: Set<string>;
|
|
796
|
+
waiters: Set<string>;
|
|
797
|
+
signalWaiters: Set<RuntimeOperationSignalWaiter>;
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
export type DurableOperationRecord = {
|
|
801
|
+
ownerSessionKey: string;
|
|
802
|
+
sequence: number;
|
|
803
|
+
signalSequence?: number;
|
|
804
|
+
signals?: RuntimeOperationSignal[];
|
|
805
|
+
snapshot: RuntimeOperationSnapshot;
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const DurableOperationSignalSchema = Type.Object({
|
|
809
|
+
operationId: Type.String(),
|
|
810
|
+
sequence: Type.Number(),
|
|
811
|
+
signal: Type.String(),
|
|
812
|
+
input: Type.Optional(Type.Any()),
|
|
813
|
+
acceptedAt: Type.String(),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const DurableOperationSnapshotSchema = Type.Object({
|
|
817
|
+
id: Type.String(),
|
|
818
|
+
service: Type.String(),
|
|
819
|
+
operation: Type.String(),
|
|
820
|
+
revision: Type.Number(),
|
|
821
|
+
state: Type.Union([
|
|
822
|
+
Type.Literal("pending"),
|
|
823
|
+
Type.Literal("running"),
|
|
824
|
+
Type.Literal("completed"),
|
|
825
|
+
Type.Literal("failed"),
|
|
826
|
+
Type.Literal("cancelled"),
|
|
827
|
+
]),
|
|
828
|
+
createdAt: Type.String(),
|
|
829
|
+
updatedAt: Type.String(),
|
|
830
|
+
completedAt: Type.Optional(Type.String()),
|
|
831
|
+
progress: Type.Optional(Type.Any()),
|
|
832
|
+
transfer: Type.Optional(Type.Object({
|
|
833
|
+
chunkIndex: Type.Number(),
|
|
834
|
+
chunkBytes: Type.Number(),
|
|
835
|
+
transferredBytes: Type.Number(),
|
|
836
|
+
})),
|
|
837
|
+
output: Type.Optional(Type.Any()),
|
|
838
|
+
error: Type.Optional(Type.Object({
|
|
839
|
+
type: Type.String(),
|
|
840
|
+
message: Type.String(),
|
|
841
|
+
})),
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
export const DurableOperationRecordSchema = Type.Object({
|
|
845
|
+
ownerSessionKey: Type.String(),
|
|
846
|
+
sequence: Type.Number(),
|
|
847
|
+
signalSequence: Type.Optional(Type.Number()),
|
|
848
|
+
signals: Type.Optional(Type.Array(DurableOperationSignalSchema)),
|
|
849
|
+
snapshot: DurableOperationSnapshotSchema,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
export type RuntimeOperationAcceptedEnvelope = {
|
|
853
|
+
kind: "accepted";
|
|
854
|
+
ref: OperationRefData;
|
|
855
|
+
snapshot: RuntimeOperationSnapshot;
|
|
856
|
+
transfer?: SendTransferGrant;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
export type RuntimeOperationControlRequest =
|
|
860
|
+
| {
|
|
861
|
+
action: "get" | "wait" | "watch" | "cancel";
|
|
862
|
+
operationId: string;
|
|
863
|
+
}
|
|
864
|
+
| {
|
|
865
|
+
action: "signal";
|
|
866
|
+
operationId: string;
|
|
867
|
+
signal: string;
|
|
868
|
+
input?: JsonValue;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
export type RuntimeOperationController = {
|
|
872
|
+
get(
|
|
873
|
+
operationId: string,
|
|
874
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
875
|
+
started(
|
|
876
|
+
operationId: string,
|
|
877
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
878
|
+
progress(
|
|
879
|
+
operationId: string,
|
|
880
|
+
progress: unknown,
|
|
881
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
882
|
+
complete(
|
|
883
|
+
operationId: string,
|
|
884
|
+
output: unknown,
|
|
885
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
886
|
+
fail(
|
|
887
|
+
operationId: string,
|
|
888
|
+
error: BaseError,
|
|
889
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
890
|
+
cancel(
|
|
891
|
+
operationId: string,
|
|
892
|
+
): AsyncResult<RuntimeOperationSnapshot, BaseError>;
|
|
893
|
+
signals(operationId: string): AsyncIterable<RuntimeOperationSignal>;
|
|
894
|
+
nextSignal(
|
|
895
|
+
operationId: string,
|
|
896
|
+
name?: string,
|
|
897
|
+
): AsyncResult<RuntimeOperationSignal, BaseError>;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
export function buildRuntimeOperationSnapshot(
|
|
901
|
+
runtime: Pick<
|
|
902
|
+
RuntimeOperationRecord,
|
|
903
|
+
"id" | "service" | "operation" | "snapshot"
|
|
904
|
+
>,
|
|
905
|
+
state: RuntimeOperationState,
|
|
906
|
+
patch?: Partial<RuntimeOperationSnapshot>,
|
|
907
|
+
): RuntimeOperationSnapshot {
|
|
908
|
+
const updatedAt = new Date().toISOString();
|
|
909
|
+
const completedAt =
|
|
910
|
+
state === "completed" || state === "failed" || state === "cancelled"
|
|
911
|
+
? (patch?.completedAt ?? updatedAt)
|
|
912
|
+
: patch?.completedAt;
|
|
913
|
+
return {
|
|
914
|
+
id: runtime.id,
|
|
915
|
+
service: runtime.service,
|
|
916
|
+
operation: runtime.operation,
|
|
917
|
+
revision: patch?.revision ?? runtime.snapshot.revision + 1,
|
|
918
|
+
state,
|
|
919
|
+
createdAt: patch?.createdAt ?? runtime.snapshot.createdAt,
|
|
920
|
+
updatedAt,
|
|
921
|
+
...(completedAt ? { completedAt } : {}),
|
|
922
|
+
...(patch?.progress !== undefined
|
|
923
|
+
? { progress: patch.progress }
|
|
924
|
+
: runtime.snapshot.progress !== undefined
|
|
925
|
+
? { progress: runtime.snapshot.progress }
|
|
926
|
+
: {}),
|
|
927
|
+
...(patch?.transfer !== undefined
|
|
928
|
+
? { transfer: patch.transfer }
|
|
929
|
+
: runtime.snapshot.transfer !== undefined
|
|
930
|
+
? { transfer: runtime.snapshot.transfer }
|
|
931
|
+
: {}),
|
|
932
|
+
...(patch?.output !== undefined
|
|
933
|
+
? { output: patch.output }
|
|
934
|
+
: runtime.snapshot.output !== undefined
|
|
935
|
+
? { output: runtime.snapshot.output }
|
|
936
|
+
: {}),
|
|
937
|
+
...(patch?.error
|
|
938
|
+
? { error: patch.error }
|
|
939
|
+
: runtime.snapshot.error
|
|
940
|
+
? { error: runtime.snapshot.error }
|
|
941
|
+
: {}),
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function isRuntimeOperationSnapshot(
|
|
946
|
+
value: unknown,
|
|
947
|
+
): value is RuntimeOperationSnapshot {
|
|
948
|
+
return !!value && typeof value === "object" &&
|
|
949
|
+
typeof (value as RuntimeOperationSnapshot).id === "string" &&
|
|
950
|
+
typeof (value as RuntimeOperationSnapshot).service === "string" &&
|
|
951
|
+
typeof (value as RuntimeOperationSnapshot).operation === "string" &&
|
|
952
|
+
typeof (value as RuntimeOperationSnapshot).revision === "number" &&
|
|
953
|
+
typeof (value as RuntimeOperationSnapshot).state === "string" &&
|
|
954
|
+
typeof (value as RuntimeOperationSnapshot).createdAt === "string" &&
|
|
955
|
+
typeof (value as RuntimeOperationSnapshot).updatedAt === "string";
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export function isTerminalRuntimeOperationSnapshot(
|
|
959
|
+
value: unknown,
|
|
960
|
+
): value is RuntimeOperationSnapshot {
|
|
961
|
+
return isRuntimeOperationSnapshot(value) && (
|
|
962
|
+
value.state === "completed" || value.state === "failed" ||
|
|
963
|
+
value.state === "cancelled"
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
type NoResponderRetryOpts = {
|
|
968
|
+
maxAttempts?: number;
|
|
969
|
+
baseDelayMs?: number;
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
export type TrellisOpts<TA extends AnyTrellisAPI> = {
|
|
973
|
+
log?: LoggerLike;
|
|
974
|
+
timeout?: number;
|
|
975
|
+
stream?: string;
|
|
976
|
+
noResponderRetry?: NoResponderRetryOpts;
|
|
977
|
+
api?: TA;
|
|
978
|
+
state?: RuntimeStateStores;
|
|
979
|
+
connection?: TrellisConnection;
|
|
980
|
+
onSessionNotFound?: () => MaybePromise<void>;
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
export type RequestOpts = {
|
|
984
|
+
timeout?: number;
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
export type EventOpts = {
|
|
988
|
+
mode?: "durable" | "ephemeral";
|
|
989
|
+
replay?: "all" | "new";
|
|
990
|
+
durableName?: string;
|
|
991
|
+
signal?: AbortSignal;
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
export type FeedSubscribeOpts = {
|
|
995
|
+
signal?: AbortSignal;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
export type FeedSubscription<TEvent> = AsyncIterable<TEvent>;
|
|
999
|
+
|
|
1000
|
+
export type FeedInputBuilder<TInput, TEvent> = {
|
|
1001
|
+
input(input: TInput): {
|
|
1002
|
+
subscribe(
|
|
1003
|
+
opts?: FeedSubscribeOpts,
|
|
1004
|
+
): AsyncResult<FeedSubscription<TEvent>, BaseError>;
|
|
1005
|
+
};
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
export type FeedHandlerContext<TInput, TEvent> = {
|
|
1009
|
+
input: TInput;
|
|
1010
|
+
caller: SessionCaller;
|
|
1011
|
+
signal: AbortSignal;
|
|
1012
|
+
emit(event: TEvent): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
export type FeedRegistration<TInput, TEvent> = {
|
|
1016
|
+
handle(
|
|
1017
|
+
handler: (
|
|
1018
|
+
context: FeedHandlerContext<TInput, TEvent>,
|
|
1019
|
+
) => unknown | Promise<unknown>,
|
|
1020
|
+
): Promise<void>;
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
export type FeedSurface<
|
|
1024
|
+
TA extends AnyTrellisAPI,
|
|
1025
|
+
TMode extends TrellisMode,
|
|
1026
|
+
F extends FeedsOf<TA>,
|
|
1027
|
+
> = TMode extends "server"
|
|
1028
|
+
? FeedRegistration<FeedInputOf<TA, F>, FeedEventOf<TA, F>>
|
|
1029
|
+
: FeedInputBuilder<FeedInputOf<TA, F>, FeedEventOf<TA, F>>;
|
|
1030
|
+
|
|
1031
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
1032
|
+
|
|
1033
|
+
type EventCallback<TMessage> = {
|
|
1034
|
+
bivarianceHack(message: TMessage): MaybeAsync<void, BaseError>;
|
|
1035
|
+
}["bivarianceHack"];
|
|
1036
|
+
|
|
1037
|
+
export type RpcHandlerContext = {
|
|
1038
|
+
caller: SessionCaller;
|
|
1039
|
+
sessionKey: string;
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
export type HandlerTrellis<
|
|
1043
|
+
TA extends AnyTrellisAPI,
|
|
1044
|
+
TRequests = RpcRequestShapes<TA>,
|
|
1045
|
+
> = {
|
|
1046
|
+
request<const M extends RequestMethodOf<TRequests>>(
|
|
1047
|
+
method: M,
|
|
1048
|
+
input: RequestInputOf<TRequests, M>,
|
|
1049
|
+
opts?: RequestOpts,
|
|
1050
|
+
): AsyncResult<RequestOutputOf<TRequests, M>, BaseError>;
|
|
1051
|
+
publish(
|
|
1052
|
+
event: string,
|
|
1053
|
+
data: Record<string, unknown>,
|
|
1054
|
+
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
1055
|
+
event<E extends EventsOf<TA>>(
|
|
1056
|
+
event: E,
|
|
1057
|
+
subjectData: Record<string, unknown>,
|
|
1058
|
+
fn: EventCallback<EventOf<TA, E>>,
|
|
1059
|
+
opts?: EventOpts,
|
|
1060
|
+
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
1061
|
+
operation<O extends OperationsOf<TA>>(
|
|
1062
|
+
operation: O,
|
|
1063
|
+
): OperationSurface<TA, TrellisMode, O>;
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
export type HandlerKvFacade<TKv extends ContractKvMetadata> = {
|
|
1067
|
+
[K in keyof TKv]: TKv[K]["required"] extends false
|
|
1068
|
+
? TypedKV<TKv[K]["schema"]> | undefined
|
|
1069
|
+
: TypedKV<TKv[K]["schema"]>;
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
export type HandlerStoreHandle = {
|
|
1073
|
+
open(): AsyncResult<TypedStore, StoreError>;
|
|
1074
|
+
waitFor(
|
|
1075
|
+
key: string,
|
|
1076
|
+
options?: StoreWaitOptions,
|
|
1077
|
+
): AsyncResult<TypedStoreEntry, StoreError>;
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
export type HandlerJobQueue<
|
|
1081
|
+
TPayload,
|
|
1082
|
+
TResult,
|
|
1083
|
+
TTrellis,
|
|
1084
|
+
> = {
|
|
1085
|
+
create(payload: TPayload): AsyncResult<JobRef<TPayload, TResult>, BaseError>;
|
|
1086
|
+
handle(
|
|
1087
|
+
handler: (args: {
|
|
1088
|
+
job: ActiveJob<TPayload, TResult>;
|
|
1089
|
+
trellis: TTrellis;
|
|
1090
|
+
}) => Promise<Result<TResult, BaseError>>,
|
|
1091
|
+
): void;
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
export type HandlerJobsFacade<
|
|
1095
|
+
TJobs extends Record<string, JobTypeMetadata>,
|
|
1096
|
+
TTrellis,
|
|
1097
|
+
> = {
|
|
1098
|
+
[K in keyof TJobs]: HandlerJobQueue<
|
|
1099
|
+
TJobs[K]["payload"],
|
|
1100
|
+
TJobs[K]["result"],
|
|
1101
|
+
TTrellis
|
|
1102
|
+
>;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
export type HandlerTrellisForContract<TContract> =
|
|
1106
|
+
& HandlerTrellis<TrellisApiFor<TContract>>
|
|
1107
|
+
& {
|
|
1108
|
+
kv: HandlerKvFacade<ContractKvFor<TContract>>;
|
|
1109
|
+
store: Record<string, HandlerStoreHandle>;
|
|
1110
|
+
jobs: HandlerJobsFacade<
|
|
1111
|
+
ContractJobsFor<TContract>,
|
|
1112
|
+
HandlerTrellisForContract<TContract>
|
|
1113
|
+
>;
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
/** Public client-side surface returned by `TrellisClient.connect`. */
|
|
1117
|
+
export type ClientTrellis<
|
|
1118
|
+
TA extends AnyTrellisAPI = TrellisAPI,
|
|
1119
|
+
TState extends RuntimeStateStores = {},
|
|
1120
|
+
TRequests = RpcRequestShapes<TA>,
|
|
1121
|
+
> = {
|
|
1122
|
+
readonly name: string;
|
|
1123
|
+
readonly timeout: number;
|
|
1124
|
+
readonly stream: string;
|
|
1125
|
+
readonly api: TA;
|
|
1126
|
+
readonly state: StateFacade<TState>;
|
|
1127
|
+
readonly connection: TrellisConnection;
|
|
1128
|
+
readonly natsConnection: NatsConnection;
|
|
1129
|
+
request<const M extends RequestMethodOf<TRequests>>(
|
|
1130
|
+
method: M,
|
|
1131
|
+
input: RequestInputOf<TRequests, M>,
|
|
1132
|
+
opts?: RequestOpts,
|
|
1133
|
+
): AsyncResult<RequestOutputOf<TRequests, M>, BaseError>;
|
|
1134
|
+
publish<E extends EventsOf<TA>>(
|
|
1135
|
+
event: E,
|
|
1136
|
+
data: EventPayloadOf<TA, E>,
|
|
1137
|
+
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
1138
|
+
event(
|
|
1139
|
+
event: string,
|
|
1140
|
+
subjectData: Record<string, unknown>,
|
|
1141
|
+
fn: EventCallback<unknown>,
|
|
1142
|
+
opts?: EventOpts,
|
|
1143
|
+
): AsyncResult<void, ValidationError | UnexpectedError>;
|
|
1144
|
+
feed<F extends FeedsOf<TA>>(
|
|
1145
|
+
feed: F,
|
|
1146
|
+
): FeedInputBuilder<FeedInputOf<TA, F>, FeedEventOf<TA, F>>;
|
|
1147
|
+
operation<O extends OperationsOf<TA>>(
|
|
1148
|
+
operation: O,
|
|
1149
|
+
): OperationSurface<TA, "client", O>;
|
|
1150
|
+
transfer(grant: SendTransferGrant): SendTransferHandle;
|
|
1151
|
+
transfer(grant: ReceiveTransferGrant): ReceiveTransferHandle;
|
|
1152
|
+
wait(): AsyncResult<void, BaseError>;
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
/** Connected client type for a generated Trellis contract. */
|
|
1156
|
+
export type ConnectedTrellisClient<TContract> = Simplify<
|
|
1157
|
+
ClientTrellis<
|
|
1158
|
+
TContract extends { API: { trellis: infer TApi } }
|
|
1159
|
+
? TApi extends AnyTrellisAPI ? TApi : TrellisAPI
|
|
1160
|
+
: TrellisAPI,
|
|
1161
|
+
RuntimeStateStoresForContract<TContract>
|
|
1162
|
+
>
|
|
1163
|
+
>;
|
|
1164
|
+
|
|
1165
|
+
export type HandlerArgs<
|
|
1166
|
+
TMountApi extends AnyTrellisAPI,
|
|
1167
|
+
M extends MethodsOf<TMountApi>,
|
|
1168
|
+
TOutboundApi extends AnyTrellisAPI = TMountApi,
|
|
1169
|
+
TTrellis = HandlerTrellis<TOutboundApi>,
|
|
1170
|
+
> = {
|
|
1171
|
+
input: MethodInputOf<TMountApi, M>;
|
|
1172
|
+
context: RpcHandlerContext;
|
|
1173
|
+
trellis: TTrellis;
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
export type HandlerFn<
|
|
1177
|
+
TMountApi extends AnyTrellisAPI,
|
|
1178
|
+
M extends MethodsOf<TMountApi>,
|
|
1179
|
+
TOutboundApi extends AnyTrellisAPI = TMountApi,
|
|
1180
|
+
TTrellis = HandlerTrellis<TOutboundApi>,
|
|
1181
|
+
> = (args: HandlerArgs<TMountApi, M, TOutboundApi, TTrellis>) => MaybePromise<
|
|
1182
|
+
Result<MethodOutputOf<TMountApi, M>, HandlerErrorOf<TMountApi, M>>
|
|
1183
|
+
>;
|
|
1184
|
+
export type RpcHandlerFn<
|
|
1185
|
+
TA extends AnyTrellisAPI,
|
|
1186
|
+
M extends RpcMethodNameOf<TA>,
|
|
1187
|
+
> = HandlerFn<TA, M, TA>;
|
|
1188
|
+
export type TrellisFor<TContract> = HandlerTrellisForContract<TContract>;
|
|
1189
|
+
|
|
1190
|
+
const DEFAULT_STATE_LIST_LIMIT = 100;
|
|
1191
|
+
|
|
1192
|
+
const STATE_RUNTIME_RPC = {
|
|
1193
|
+
get: {
|
|
1194
|
+
subject: "rpc.v1.State.Get",
|
|
1195
|
+
input: StateGetSchema,
|
|
1196
|
+
output: StateGetResponseSchema,
|
|
1197
|
+
callerCapabilities: [],
|
|
1198
|
+
errors: ["AuthError", "ValidationError", "UnexpectedError"] as const,
|
|
1199
|
+
declaredErrorTypes: [
|
|
1200
|
+
"AuthError",
|
|
1201
|
+
"ValidationError",
|
|
1202
|
+
"UnexpectedError",
|
|
1203
|
+
] as const,
|
|
1204
|
+
},
|
|
1205
|
+
put: {
|
|
1206
|
+
subject: "rpc.v1.State.Put",
|
|
1207
|
+
input: StatePutSchema,
|
|
1208
|
+
output: StatePutResponseSchema,
|
|
1209
|
+
callerCapabilities: [],
|
|
1210
|
+
errors: ["AuthError", "ValidationError", "UnexpectedError"] as const,
|
|
1211
|
+
declaredErrorTypes: [
|
|
1212
|
+
"AuthError",
|
|
1213
|
+
"ValidationError",
|
|
1214
|
+
"UnexpectedError",
|
|
1215
|
+
] as const,
|
|
1216
|
+
},
|
|
1217
|
+
delete: {
|
|
1218
|
+
subject: "rpc.v1.State.Delete",
|
|
1219
|
+
input: StateDeleteSchema,
|
|
1220
|
+
output: StateDeleteResponseSchema,
|
|
1221
|
+
callerCapabilities: [],
|
|
1222
|
+
errors: ["AuthError", "ValidationError", "UnexpectedError"] as const,
|
|
1223
|
+
declaredErrorTypes: [
|
|
1224
|
+
"AuthError",
|
|
1225
|
+
"ValidationError",
|
|
1226
|
+
"UnexpectedError",
|
|
1227
|
+
] as const,
|
|
1228
|
+
},
|
|
1229
|
+
list: {
|
|
1230
|
+
subject: "rpc.v1.State.List",
|
|
1231
|
+
input: StateListSchema,
|
|
1232
|
+
output: StateListResponseSchema,
|
|
1233
|
+
callerCapabilities: [],
|
|
1234
|
+
errors: ["AuthError", "ValidationError", "UnexpectedError"] as const,
|
|
1235
|
+
declaredErrorTypes: [
|
|
1236
|
+
"AuthError",
|
|
1237
|
+
"ValidationError",
|
|
1238
|
+
"UnexpectedError",
|
|
1239
|
+
] as const,
|
|
1240
|
+
},
|
|
1241
|
+
} satisfies Record<string, {
|
|
1242
|
+
subject: string;
|
|
1243
|
+
input: unknown;
|
|
1244
|
+
output: unknown;
|
|
1245
|
+
callerCapabilities: readonly string[];
|
|
1246
|
+
errors: readonly string[];
|
|
1247
|
+
declaredErrorTypes: readonly string[];
|
|
1248
|
+
}>;
|
|
1249
|
+
|
|
1250
|
+
function joinStatePath(prefix: string | undefined, key: string): string {
|
|
1251
|
+
return [prefix, key]
|
|
1252
|
+
.flatMap((value) => value?.split("/") ?? [])
|
|
1253
|
+
.filter((segment) => segment.length > 0)
|
|
1254
|
+
.join("/");
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function validateStateValue(
|
|
1258
|
+
schema: unknown,
|
|
1259
|
+
value: JsonValue,
|
|
1260
|
+
): Result<unknown, ValidationError | UnexpectedError> {
|
|
1261
|
+
return parseRuntimeSchema(schema, value);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function validateStateGetResult<TStore extends RuntimeStateStoreShape>(
|
|
1265
|
+
descriptor: RuntimeStateStoreShape,
|
|
1266
|
+
result: StateGetResult<TStore>,
|
|
1267
|
+
): Result<StateGetResult<TStore>, ValidationError | UnexpectedError> {
|
|
1268
|
+
if ("migrationRequired" in result) {
|
|
1269
|
+
const schema = descriptor.acceptedVersions?.[result.stateVersion];
|
|
1270
|
+
if (!schema) {
|
|
1271
|
+
return Result.err(
|
|
1272
|
+
new ValidationError({
|
|
1273
|
+
errors: [{
|
|
1274
|
+
path: "/stateVersion",
|
|
1275
|
+
message:
|
|
1276
|
+
`state version '${result.stateVersion}' is not accepted by the runtime store`,
|
|
1277
|
+
}],
|
|
1278
|
+
}),
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const parsed = validateStateValue(schema, result.entry.value as JsonValue);
|
|
1282
|
+
if (parsed.isErr()) return Result.err(parsed.error);
|
|
1283
|
+
return Result.ok({
|
|
1284
|
+
...result,
|
|
1285
|
+
entry: {
|
|
1286
|
+
...result.entry,
|
|
1287
|
+
value: parsed.unwrapOrElse(() => {
|
|
1288
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1289
|
+
}),
|
|
1290
|
+
},
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (!result.found) {
|
|
1295
|
+
return Result.ok(result);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const parsed = validateStateValue(
|
|
1299
|
+
descriptor.schema,
|
|
1300
|
+
result.entry.value as JsonValue,
|
|
1301
|
+
);
|
|
1302
|
+
if (parsed.isErr()) {
|
|
1303
|
+
return Result.err(parsed.error);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
return Result.ok({
|
|
1307
|
+
...result,
|
|
1308
|
+
entry: {
|
|
1309
|
+
...result.entry,
|
|
1310
|
+
value: parsed.unwrapOrElse(() => {
|
|
1311
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1312
|
+
}),
|
|
1313
|
+
},
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function validateStatePutResult<TStore extends RuntimeStateStoreShape>(
|
|
1318
|
+
descriptor: RuntimeStateStoreShape,
|
|
1319
|
+
result: StatePutResult<TStore>,
|
|
1320
|
+
): Result<StatePutResult<TStore>, ValidationError | UnexpectedError> {
|
|
1321
|
+
if (result.applied) {
|
|
1322
|
+
const parsed = validateStateValue(
|
|
1323
|
+
descriptor.schema,
|
|
1324
|
+
result.entry.value as JsonValue,
|
|
1325
|
+
);
|
|
1326
|
+
if (parsed.isErr()) return Result.err(parsed.error);
|
|
1327
|
+
return Result.ok({
|
|
1328
|
+
...result,
|
|
1329
|
+
entry: {
|
|
1330
|
+
...result.entry,
|
|
1331
|
+
value: parsed.unwrapOrElse(() => {
|
|
1332
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1333
|
+
}),
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (!result.entry) {
|
|
1339
|
+
return Result.ok(result);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if ("migrationRequired" in result.entry) {
|
|
1343
|
+
const schema = descriptor.acceptedVersions?.[result.entry.stateVersion];
|
|
1344
|
+
if (!schema) {
|
|
1345
|
+
return Result.err(
|
|
1346
|
+
new ValidationError({
|
|
1347
|
+
errors: [{
|
|
1348
|
+
path: "/stateVersion",
|
|
1349
|
+
message:
|
|
1350
|
+
`state version '${result.entry.stateVersion}' is not accepted by the runtime store`,
|
|
1351
|
+
}],
|
|
1352
|
+
}),
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
const parsed = validateStateValue(
|
|
1356
|
+
schema,
|
|
1357
|
+
result.entry.entry.value as JsonValue,
|
|
1358
|
+
);
|
|
1359
|
+
if (parsed.isErr()) return Result.err(parsed.error);
|
|
1360
|
+
return Result.ok({
|
|
1361
|
+
...result,
|
|
1362
|
+
entry: {
|
|
1363
|
+
...result.entry,
|
|
1364
|
+
entry: {
|
|
1365
|
+
...result.entry.entry,
|
|
1366
|
+
value: parsed.unwrapOrElse(() => {
|
|
1367
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1368
|
+
}),
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const parsed = validateStateValue(
|
|
1375
|
+
descriptor.schema,
|
|
1376
|
+
result.entry.value as JsonValue,
|
|
1377
|
+
);
|
|
1378
|
+
if (parsed.isErr()) {
|
|
1379
|
+
return Result.err(parsed.error);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return Result.ok({
|
|
1383
|
+
...result,
|
|
1384
|
+
entry: {
|
|
1385
|
+
...result.entry,
|
|
1386
|
+
value: parsed.unwrapOrElse(() => {
|
|
1387
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1388
|
+
}),
|
|
1389
|
+
},
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function validateStateListResult(
|
|
1394
|
+
descriptor: RuntimeStateStoreShape,
|
|
1395
|
+
result: {
|
|
1396
|
+
entries: Array<
|
|
1397
|
+
MapStateEntry<unknown> | {
|
|
1398
|
+
migrationRequired: true;
|
|
1399
|
+
entry: MapStateEntry<unknown>;
|
|
1400
|
+
stateVersion: string;
|
|
1401
|
+
currentStateVersion: string;
|
|
1402
|
+
writerContractDigest: string;
|
|
1403
|
+
}
|
|
1404
|
+
>;
|
|
1405
|
+
count: number;
|
|
1406
|
+
offset: number;
|
|
1407
|
+
limit: number;
|
|
1408
|
+
nextOffset?: number;
|
|
1409
|
+
},
|
|
1410
|
+
): Result<typeof result, ValidationError | UnexpectedError> {
|
|
1411
|
+
const entries: typeof result.entries = [];
|
|
1412
|
+
for (const entry of result.entries) {
|
|
1413
|
+
if ("migrationRequired" in entry) {
|
|
1414
|
+
const schema = descriptor.acceptedVersions?.[entry.stateVersion];
|
|
1415
|
+
if (!schema) {
|
|
1416
|
+
return Result.err(
|
|
1417
|
+
new ValidationError({
|
|
1418
|
+
errors: [{
|
|
1419
|
+
path: "/stateVersion",
|
|
1420
|
+
message:
|
|
1421
|
+
`state version '${entry.stateVersion}' is not accepted by the runtime store`,
|
|
1422
|
+
}],
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
const parsed = validateStateValue(schema, entry.entry.value as JsonValue);
|
|
1427
|
+
if (parsed.isErr()) return Result.err(parsed.error);
|
|
1428
|
+
entries.push({
|
|
1429
|
+
...entry,
|
|
1430
|
+
entry: {
|
|
1431
|
+
...entry.entry,
|
|
1432
|
+
value: parsed.unwrapOrElse(() => {
|
|
1433
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1434
|
+
}),
|
|
1435
|
+
},
|
|
1436
|
+
});
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const parsed = validateStateValue(
|
|
1441
|
+
descriptor.schema,
|
|
1442
|
+
entry.value as JsonValue,
|
|
1443
|
+
);
|
|
1444
|
+
if (parsed.isErr()) {
|
|
1445
|
+
return Result.err(parsed.error);
|
|
1446
|
+
}
|
|
1447
|
+
entries.push({
|
|
1448
|
+
...entry,
|
|
1449
|
+
value: parsed.unwrapOrElse(() => {
|
|
1450
|
+
throw new Error("state value validation unexpectedly failed");
|
|
1451
|
+
}),
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return Result.ok({ ...result, entries });
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
export type RpcArgs<
|
|
1459
|
+
TContract,
|
|
1460
|
+
M extends RpcMethodNameOf<OwnedApiFor<TContract>>,
|
|
1461
|
+
> = HandlerArgs<
|
|
1462
|
+
OwnedApiFor<TContract>,
|
|
1463
|
+
M,
|
|
1464
|
+
TrellisApiFor<TContract>,
|
|
1465
|
+
HandlerTrellisForContract<TContract>
|
|
1466
|
+
>;
|
|
1467
|
+
export type RpcResult<
|
|
1468
|
+
TContract,
|
|
1469
|
+
M extends RpcMethodNameOf<OwnedApiFor<TContract>>,
|
|
1470
|
+
> = Result<
|
|
1471
|
+
RpcOutputOf<OwnedApiFor<TContract>, M>,
|
|
1472
|
+
RpcHandlerErrorOf<OwnedApiFor<TContract>, M>
|
|
1473
|
+
>;
|
|
1474
|
+
export type RpcRequestErrorOf<
|
|
1475
|
+
TA extends AnyTrellisAPI,
|
|
1476
|
+
M extends RpcMethodNameOf<TA>,
|
|
1477
|
+
> = RequestErrorOf<TA, M>;
|
|
1478
|
+
export type RpcHandlerErrorOf<
|
|
1479
|
+
TA extends AnyTrellisAPI,
|
|
1480
|
+
M extends RpcMethodNameOf<TA>,
|
|
1481
|
+
> = HandlerErrorOf<TA, M>;
|
|
1482
|
+
export type EventName<TContract> = EventsOf<OwnedApiFor<TContract>>;
|
|
1483
|
+
export type EventType<
|
|
1484
|
+
TContract,
|
|
1485
|
+
E extends EventName<TContract>,
|
|
1486
|
+
> = EventOf<OwnedApiFor<TContract>, E>;
|
|
1487
|
+
export type EventPayload<
|
|
1488
|
+
TContract,
|
|
1489
|
+
E extends EventName<TContract>,
|
|
1490
|
+
> = EventPayloadOf<OwnedApiFor<TContract>, E>;
|
|
1491
|
+
export type EventHandler<
|
|
1492
|
+
TContract,
|
|
1493
|
+
E extends EventName<TContract>,
|
|
1494
|
+
> = (event: EventType<TContract, E>) => MaybeAsync<void, BaseError>;
|
|
1495
|
+
|
|
1496
|
+
type DeepRecord<T> = {
|
|
1497
|
+
[k: string]: T | DeepRecord<T>;
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
const NATS_SUBJECT_TOKEN_FORBIDDEN = /[\u0000\s.*>~]/gu;
|
|
1501
|
+
|
|
1502
|
+
const DEFAULT_NO_RESPONDER_MAX_RETRIES = 2;
|
|
1503
|
+
const DEFAULT_NO_RESPONDER_RETRY_MS = 200;
|
|
1504
|
+
const DEFAULT_AUTH_VALIDATE_SESSION_RETRY_ATTEMPTS = 3;
|
|
1505
|
+
const DEFAULT_AUTH_VALIDATE_SESSION_RETRY_MS = 25;
|
|
1506
|
+
|
|
1507
|
+
function activeTraceId(span: Span): string | undefined {
|
|
1508
|
+
const traceId = span.spanContext().traceId;
|
|
1509
|
+
return traceId === "00000000000000000000000000000000" ? undefined : traceId;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function traceIdFromTraceparent(
|
|
1513
|
+
traceparent: string | undefined,
|
|
1514
|
+
): string | undefined {
|
|
1515
|
+
const [version, traceId, parentId, flags, extra] = traceparent?.split("-") ??
|
|
1516
|
+
[];
|
|
1517
|
+
if (
|
|
1518
|
+
extra !== undefined ||
|
|
1519
|
+
!/^[0-9a-f]{2}$/u.test(version ?? "") ||
|
|
1520
|
+
version === "ff" ||
|
|
1521
|
+
!/^[0-9a-f]{32}$/u.test(traceId ?? "") ||
|
|
1522
|
+
traceId === "00000000000000000000000000000000" ||
|
|
1523
|
+
!/^[0-9a-f]{16}$/u.test(parentId ?? "") ||
|
|
1524
|
+
parentId === "0000000000000000" ||
|
|
1525
|
+
!/^[0-9a-f]{2}$/u.test(flags ?? "")
|
|
1526
|
+
) {
|
|
1527
|
+
return undefined;
|
|
1528
|
+
}
|
|
1529
|
+
return traceId;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const EMPTY_TRELLIS_API: TrellisAPI = {
|
|
1533
|
+
rpc: {},
|
|
1534
|
+
operations: {},
|
|
1535
|
+
events: {},
|
|
1536
|
+
feeds: {},
|
|
1537
|
+
subjects: {},
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
type AuthCacheEntry = {
|
|
1541
|
+
caller: SessionCaller;
|
|
1542
|
+
expires: number;
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
function isBrowserAuthRequiredError(error: unknown): boolean {
|
|
1546
|
+
const isAuthRequiredReason = (reason: unknown): boolean =>
|
|
1547
|
+
reason === "session_not_found" || reason === "reauth_required";
|
|
1548
|
+
|
|
1549
|
+
if (error instanceof AuthError) {
|
|
1550
|
+
return isAuthRequiredReason(error.reason);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (
|
|
1554
|
+
error instanceof RemoteError &&
|
|
1555
|
+
error.remoteError.type === "AuthError"
|
|
1556
|
+
) {
|
|
1557
|
+
const reason = Reflect.get(error.remoteError, "reason");
|
|
1558
|
+
return isAuthRequiredReason(reason);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function isTransientAuthValidateSessionError(error: unknown): boolean {
|
|
1565
|
+
if (error instanceof AuthError) {
|
|
1566
|
+
return error.reason === "session_not_found";
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (
|
|
1570
|
+
error instanceof RemoteError &&
|
|
1571
|
+
error.remoteError.type === "AuthError"
|
|
1572
|
+
) {
|
|
1573
|
+
const reason = Reflect.get(error.remoteError, "reason");
|
|
1574
|
+
return reason === "session_not_found";
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function isDeclaredRpcError(
|
|
1581
|
+
errorNames: readonly string[] | undefined,
|
|
1582
|
+
type: string,
|
|
1583
|
+
): boolean {
|
|
1584
|
+
return !!errorNames?.includes(type);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function isRuntimeRpcErrorDesc(value: unknown): value is RuntimeRpcErrorDesc {
|
|
1588
|
+
return !!value && typeof value === "object" &&
|
|
1589
|
+
typeof Reflect.get(value, "type") === "string" &&
|
|
1590
|
+
typeof Reflect.get(value, "fromSerializable") === "function";
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const payloadSizeEncoder = new TextEncoder();
|
|
1594
|
+
|
|
1595
|
+
function payloadByteLength(payload: string | Uint8Array): number {
|
|
1596
|
+
return typeof payload === "string"
|
|
1597
|
+
? payloadSizeEncoder.encode(payload).byteLength
|
|
1598
|
+
: payload.byteLength;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function causeMessage(cause: unknown): string {
|
|
1602
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function causeLogData(cause: unknown): unknown {
|
|
1606
|
+
return cause instanceof Error
|
|
1607
|
+
? { message: cause.message, stack: cause.stack, name: cause.name }
|
|
1608
|
+
: cause;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function reconstructDeclaredRpcError(
|
|
1612
|
+
errorNames: readonly string[] | undefined,
|
|
1613
|
+
runtimeErrors: readonly RuntimeRpcErrorDesc[] | undefined,
|
|
1614
|
+
data: StaticDecode<typeof TrellisErrorDataSchema>,
|
|
1615
|
+
json: JsonValue,
|
|
1616
|
+
): BaseError | ValidationError | UnexpectedError | null {
|
|
1617
|
+
if (!isDeclaredRpcError(errorNames, data.type)) {
|
|
1618
|
+
return null;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const runtimeError = getBuiltinRpcError(data.type) ??
|
|
1622
|
+
runtimeErrors?.find((candidate) => candidate.type === data.type);
|
|
1623
|
+
if (!runtimeError) {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const parsed = runtimeError.schema
|
|
1628
|
+
? parseRuntimeSchema(runtimeError.schema, json).take()
|
|
1629
|
+
: data;
|
|
1630
|
+
if (isErr(parsed)) {
|
|
1631
|
+
return parsed.error instanceof ValidationError ||
|
|
1632
|
+
parsed.error instanceof UnexpectedError
|
|
1633
|
+
? parsed.error
|
|
1634
|
+
: new UnexpectedError({ cause: parsed.error });
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
try {
|
|
1638
|
+
const reconstructed = runtimeError.fromSerializable(parsed);
|
|
1639
|
+
if (reconstructed instanceof BaseError) {
|
|
1640
|
+
return reconstructed;
|
|
1641
|
+
}
|
|
1642
|
+
return new UnexpectedError({
|
|
1643
|
+
cause: new Error(
|
|
1644
|
+
`RPC error '${data.type}' reconstructed to a non-Trellis error instance`,
|
|
1645
|
+
),
|
|
1646
|
+
});
|
|
1647
|
+
} catch (cause) {
|
|
1648
|
+
return new UnexpectedError({ cause });
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
async function sleep(ms: number): Promise<void> {
|
|
1653
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
export class Trellis<
|
|
1657
|
+
TA extends AnyTrellisAPI = TrellisAPI,
|
|
1658
|
+
TMode extends TrellisMode = "client",
|
|
1659
|
+
TState extends RuntimeStateStores = {},
|
|
1660
|
+
TRequests = RpcRequestShapes<TA>,
|
|
1661
|
+
> {
|
|
1662
|
+
readonly name: string;
|
|
1663
|
+
readonly timeout: number;
|
|
1664
|
+
readonly stream: string;
|
|
1665
|
+
readonly state: StateFacade<TState>;
|
|
1666
|
+
/** Framework-neutral lifecycle handle for this Trellis runtime connection. */
|
|
1667
|
+
readonly connection: TrellisConnection;
|
|
1668
|
+
|
|
1669
|
+
protected nats: NatsConnection;
|
|
1670
|
+
protected js: JetStreamClient;
|
|
1671
|
+
protected auth: TrellisAuth;
|
|
1672
|
+
readonly api: TA;
|
|
1673
|
+
#log: LoggerLike;
|
|
1674
|
+
#tasks: TrellisTasks;
|
|
1675
|
+
#hasExplicitApi: boolean;
|
|
1676
|
+
#noResponderMaxRetries: number;
|
|
1677
|
+
#noResponderRetryMs: number;
|
|
1678
|
+
#onSessionNotFound?: () => MaybePromise<void>;
|
|
1679
|
+
#operationStore?: Promise<TypedKV<typeof DurableOperationRecordSchema>>;
|
|
1680
|
+
|
|
1681
|
+
constructor(
|
|
1682
|
+
name: string, // Must be unique for a service
|
|
1683
|
+
nats: NatsConnection,
|
|
1684
|
+
auth: TrellisAuth,
|
|
1685
|
+
opts?: TrellisOpts<TA>,
|
|
1686
|
+
) {
|
|
1687
|
+
const api = opts?.api;
|
|
1688
|
+
|
|
1689
|
+
this.name = name;
|
|
1690
|
+
this.nats = nats;
|
|
1691
|
+
this.js = jetstream(this.nats);
|
|
1692
|
+
this.auth = auth as TrellisAuth;
|
|
1693
|
+
this.api = (api ?? EMPTY_TRELLIS_API) as TA;
|
|
1694
|
+
this.#log = (opts?.log ?? logger).child({ lib: "trellis" });
|
|
1695
|
+
this.timeout = opts?.timeout ?? 3000;
|
|
1696
|
+
this.stream = opts?.stream ?? "trellis";
|
|
1697
|
+
this.#hasExplicitApi = api !== undefined;
|
|
1698
|
+
this.#noResponderMaxRetries = opts?.noResponderRetry?.maxAttempts ??
|
|
1699
|
+
DEFAULT_NO_RESPONDER_MAX_RETRIES;
|
|
1700
|
+
this.#noResponderRetryMs = opts?.noResponderRetry?.baseDelayMs ??
|
|
1701
|
+
DEFAULT_NO_RESPONDER_RETRY_MS;
|
|
1702
|
+
this.#onSessionNotFound = opts?.onSessionNotFound;
|
|
1703
|
+
this.connection = opts?.connection ??
|
|
1704
|
+
new TrellisConnection({ kind: "client" });
|
|
1705
|
+
|
|
1706
|
+
this.#tasks = new TrellisTasks({ log: this.#log });
|
|
1707
|
+
this.state = this.#createStateFacade(opts?.state as TState | undefined);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Returns the underlying NATS connection.
|
|
1712
|
+
*/
|
|
1713
|
+
get natsConnection(): NatsConnection {
|
|
1714
|
+
return this.nats;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
#createStateFacade(state: TState | undefined): StateFacade<TState> {
|
|
1718
|
+
const stores = (state ?? {}) as RuntimeStateStores;
|
|
1719
|
+
const facade = Object.fromEntries(
|
|
1720
|
+
Object.entries(stores).map(([store, descriptor]) => {
|
|
1721
|
+
if (descriptor.kind === "value") {
|
|
1722
|
+
const client: ValueStateStoreClient<unknown> = {
|
|
1723
|
+
get: () =>
|
|
1724
|
+
AsyncResult.from((async () => {
|
|
1725
|
+
const result = await this.#requestBuiltRpc<
|
|
1726
|
+
StateGetResult<{ kind: "value"; value: unknown }>
|
|
1727
|
+
>(
|
|
1728
|
+
"State.Get",
|
|
1729
|
+
{ store },
|
|
1730
|
+
STATE_RUNTIME_RPC.get,
|
|
1731
|
+
);
|
|
1732
|
+
if (result.isErr()) return result;
|
|
1733
|
+
return validateStateGetResult(
|
|
1734
|
+
descriptor,
|
|
1735
|
+
result.unwrapOrElse(() => {
|
|
1736
|
+
throw new Error("state get unexpectedly failed");
|
|
1737
|
+
}),
|
|
1738
|
+
);
|
|
1739
|
+
})()),
|
|
1740
|
+
put: (value, opts) =>
|
|
1741
|
+
AsyncResult.from((async () => {
|
|
1742
|
+
const encoded = encodeRuntimeSchema(descriptor.schema, value)
|
|
1743
|
+
.take();
|
|
1744
|
+
if (isErr(encoded)) {
|
|
1745
|
+
return Result.err(encoded.error);
|
|
1746
|
+
}
|
|
1747
|
+
const result = await this.#requestBuiltRpc<
|
|
1748
|
+
StatePutResult<{ kind: "value"; value: unknown }>
|
|
1749
|
+
>(
|
|
1750
|
+
"State.Put",
|
|
1751
|
+
{ store, value, ...opts },
|
|
1752
|
+
STATE_RUNTIME_RPC.put,
|
|
1753
|
+
);
|
|
1754
|
+
if (result.isErr()) return result;
|
|
1755
|
+
return validateStatePutResult(
|
|
1756
|
+
descriptor,
|
|
1757
|
+
result.unwrapOrElse(() => {
|
|
1758
|
+
throw new Error("state put unexpectedly failed");
|
|
1759
|
+
}),
|
|
1760
|
+
);
|
|
1761
|
+
})()),
|
|
1762
|
+
delete: (opts) =>
|
|
1763
|
+
this.#requestBuiltRpc<{ deleted: boolean }>(
|
|
1764
|
+
"State.Delete",
|
|
1765
|
+
{ store, ...opts },
|
|
1766
|
+
STATE_RUNTIME_RPC.delete,
|
|
1767
|
+
),
|
|
1768
|
+
};
|
|
1769
|
+
return [store, client];
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const mapClient = (prefix?: string): MapStateStoreClient<unknown> => ({
|
|
1773
|
+
get: (key) =>
|
|
1774
|
+
AsyncResult.from((async () => {
|
|
1775
|
+
const result = await this.#requestBuiltRpc<
|
|
1776
|
+
StateGetResult<{ kind: "map"; value: unknown }>
|
|
1777
|
+
>(
|
|
1778
|
+
"State.Get",
|
|
1779
|
+
{ store, key: joinStatePath(prefix, key) },
|
|
1780
|
+
STATE_RUNTIME_RPC.get,
|
|
1781
|
+
);
|
|
1782
|
+
if (result.isErr()) return result;
|
|
1783
|
+
return validateStateGetResult(
|
|
1784
|
+
descriptor,
|
|
1785
|
+
result.unwrapOrElse(() => {
|
|
1786
|
+
throw new Error("state get unexpectedly failed");
|
|
1787
|
+
}),
|
|
1788
|
+
);
|
|
1789
|
+
})()),
|
|
1790
|
+
put: (key, value, opts) =>
|
|
1791
|
+
AsyncResult.from((async () => {
|
|
1792
|
+
const encoded = encodeRuntimeSchema(descriptor.schema, value)
|
|
1793
|
+
.take();
|
|
1794
|
+
if (isErr(encoded)) {
|
|
1795
|
+
return Result.err(encoded.error);
|
|
1796
|
+
}
|
|
1797
|
+
const result = await this.#requestBuiltRpc<
|
|
1798
|
+
StatePutResult<{ kind: "map"; value: unknown }>
|
|
1799
|
+
>(
|
|
1800
|
+
"State.Put",
|
|
1801
|
+
{ store, key: joinStatePath(prefix, key), value, ...opts },
|
|
1802
|
+
STATE_RUNTIME_RPC.put,
|
|
1803
|
+
);
|
|
1804
|
+
if (result.isErr()) return result;
|
|
1805
|
+
return validateStatePutResult(
|
|
1806
|
+
descriptor,
|
|
1807
|
+
result.unwrapOrElse(() => {
|
|
1808
|
+
throw new Error("state put unexpectedly failed");
|
|
1809
|
+
}),
|
|
1810
|
+
);
|
|
1811
|
+
})()),
|
|
1812
|
+
delete: (key, opts) =>
|
|
1813
|
+
this.#requestBuiltRpc<{ deleted: boolean }>(
|
|
1814
|
+
"State.Delete",
|
|
1815
|
+
{ store, key: joinStatePath(prefix, key), ...opts },
|
|
1816
|
+
STATE_RUNTIME_RPC.delete,
|
|
1817
|
+
),
|
|
1818
|
+
list: (opts) =>
|
|
1819
|
+
AsyncResult.from((async () => {
|
|
1820
|
+
const result = await this.#requestBuiltRpc<{
|
|
1821
|
+
entries: Array<
|
|
1822
|
+
| MapStateEntry<unknown>
|
|
1823
|
+
| StateMigrationRequiredEntry<MapStateEntry<unknown>>
|
|
1824
|
+
>;
|
|
1825
|
+
count: number;
|
|
1826
|
+
offset: number;
|
|
1827
|
+
limit: number;
|
|
1828
|
+
nextOffset?: number;
|
|
1829
|
+
}>(
|
|
1830
|
+
"State.List",
|
|
1831
|
+
{
|
|
1832
|
+
store,
|
|
1833
|
+
...(prefix ? { prefix } : {}),
|
|
1834
|
+
offset: opts?.offset ?? 0,
|
|
1835
|
+
limit: opts?.limit ?? DEFAULT_STATE_LIST_LIMIT,
|
|
1836
|
+
},
|
|
1837
|
+
STATE_RUNTIME_RPC.list,
|
|
1838
|
+
);
|
|
1839
|
+
if (result.isErr()) return result;
|
|
1840
|
+
return validateStateListResult(
|
|
1841
|
+
descriptor,
|
|
1842
|
+
result.unwrapOrElse(() => {
|
|
1843
|
+
throw new Error("state list unexpectedly failed");
|
|
1844
|
+
}),
|
|
1845
|
+
);
|
|
1846
|
+
})()),
|
|
1847
|
+
prefix: (path) => mapClient(joinStatePath(prefix, path)),
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
return [store, mapClient()];
|
|
1851
|
+
}),
|
|
1852
|
+
);
|
|
1853
|
+
|
|
1854
|
+
return facade as StateFacade<TState>;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
#unknownApiError(
|
|
1858
|
+
kind: "RPC method" | "operation" | "event" | "feed",
|
|
1859
|
+
name: string,
|
|
1860
|
+
): Error {
|
|
1861
|
+
const base = `Unknown ${kind} '${name}'.`;
|
|
1862
|
+
if (this.#hasExplicitApi) {
|
|
1863
|
+
return new Error(`${base} Did you forget to include its API module?`);
|
|
1864
|
+
}
|
|
1865
|
+
return new Error(
|
|
1866
|
+
`${base} No API surface was provided. Pass opts.api, use createClient(contract, ...), or await createCoreClient(...) instead.`,
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
async operationStoreHandle(): Promise<
|
|
1871
|
+
TypedKV<typeof DurableOperationRecordSchema>
|
|
1872
|
+
> {
|
|
1873
|
+
if (!this.#operationStore) {
|
|
1874
|
+
const bucket = `trellis_operations_${this.auth.sessionKey.slice(0, 16)}`;
|
|
1875
|
+
this.#operationStore = (async () => {
|
|
1876
|
+
const result = await TypedKV.open(
|
|
1877
|
+
this.nats,
|
|
1878
|
+
bucket,
|
|
1879
|
+
DurableOperationRecordSchema,
|
|
1880
|
+
{
|
|
1881
|
+
history: 5,
|
|
1882
|
+
ttl: 0,
|
|
1883
|
+
},
|
|
1884
|
+
);
|
|
1885
|
+
const value = result.take();
|
|
1886
|
+
if (isErr(value)) {
|
|
1887
|
+
throw value.error;
|
|
1888
|
+
}
|
|
1889
|
+
return value;
|
|
1890
|
+
})();
|
|
1891
|
+
}
|
|
1892
|
+
return this.#operationStore;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
async loadOperationRecord(
|
|
1896
|
+
operationId: string,
|
|
1897
|
+
): Promise<DurableOperationRecord | null> {
|
|
1898
|
+
const store = await this.operationStoreHandle();
|
|
1899
|
+
const entry = await store.get(operationId);
|
|
1900
|
+
const value = entry.take();
|
|
1901
|
+
if (isErr(value)) {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
return value.value as DurableOperationRecord;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
async saveOperationRecord(runtime: RuntimeOperationRecord): Promise<void> {
|
|
1908
|
+
const store = await this.operationStoreHandle();
|
|
1909
|
+
const record: DurableOperationRecord = {
|
|
1910
|
+
ownerSessionKey: runtime.ownerSessionKey,
|
|
1911
|
+
sequence: runtime.sequence,
|
|
1912
|
+
signalSequence: runtime.signalSequence,
|
|
1913
|
+
signals: runtime.signals,
|
|
1914
|
+
snapshot: runtime.snapshot,
|
|
1915
|
+
};
|
|
1916
|
+
await store.put(runtime.id, record);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Makes an authenticated request to a Trellis RPC method.
|
|
1921
|
+
*
|
|
1922
|
+
* @template M The specific RPC method being called.
|
|
1923
|
+
* @param method The name of the RPC method to call.
|
|
1924
|
+
* @param input The input data for the method, conforming to its schema.
|
|
1925
|
+
* @param opts Optional request-specific options.
|
|
1926
|
+
* @returns An `AsyncResult` containing either the method's output or an error.
|
|
1927
|
+
* @returns A `Result` object after awaiting:
|
|
1928
|
+
* ok: A validated response for method M
|
|
1929
|
+
* err: declared RPC errors | RemoteError | ValidationError | UnexpectedError
|
|
1930
|
+
*/
|
|
1931
|
+
request<const M extends RequestMethodOf<TRequests>>(
|
|
1932
|
+
method: M,
|
|
1933
|
+
input: RequestInputOf<TRequests, M>,
|
|
1934
|
+
opts?: RequestOpts,
|
|
1935
|
+
): AsyncResult<RequestOutputOf<TRequests, M>, BaseError>;
|
|
1936
|
+
request(
|
|
1937
|
+
method: string,
|
|
1938
|
+
input: unknown,
|
|
1939
|
+
opts?: RequestOpts,
|
|
1940
|
+
): AsyncResult<unknown, BaseError> {
|
|
1941
|
+
const rpcApi = this.api["rpc"] as Record<string, unknown>;
|
|
1942
|
+
const ctx = rpcApi[method] as {
|
|
1943
|
+
subject: string;
|
|
1944
|
+
input: unknown;
|
|
1945
|
+
output: unknown;
|
|
1946
|
+
callerCapabilities: readonly string[];
|
|
1947
|
+
errors?: readonly string[];
|
|
1948
|
+
declaredErrorTypes?: readonly string[];
|
|
1949
|
+
runtimeErrors?: readonly RuntimeRpcErrorDesc[];
|
|
1950
|
+
} | undefined;
|
|
1951
|
+
if (!ctx) {
|
|
1952
|
+
return AsyncResult.from(Promise.resolve(err(
|
|
1953
|
+
new UnexpectedError({
|
|
1954
|
+
cause: this.#unknownApiError("RPC method", method.toString()),
|
|
1955
|
+
context: { method: method.toString() },
|
|
1956
|
+
}),
|
|
1957
|
+
)));
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
return this.#requestBuiltRpcUnknown(method, input, ctx, opts);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
#requestBuiltRpcUnknown(
|
|
1964
|
+
method: string,
|
|
1965
|
+
input: unknown,
|
|
1966
|
+
ctx: {
|
|
1967
|
+
subject: string;
|
|
1968
|
+
input: unknown;
|
|
1969
|
+
output: unknown;
|
|
1970
|
+
callerCapabilities: readonly string[];
|
|
1971
|
+
errors?: readonly string[];
|
|
1972
|
+
declaredErrorTypes?: readonly string[];
|
|
1973
|
+
runtimeErrors?: readonly RuntimeRpcErrorDesc[];
|
|
1974
|
+
},
|
|
1975
|
+
opts?: RequestOpts,
|
|
1976
|
+
): AsyncResult<unknown, BaseError> {
|
|
1977
|
+
return this.#requestBuiltRpc(method, input, ctx, opts);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
#requestBuiltRpc<TOutput>(
|
|
1981
|
+
method: string,
|
|
1982
|
+
input: unknown,
|
|
1983
|
+
ctx: {
|
|
1984
|
+
subject: string;
|
|
1985
|
+
input: unknown;
|
|
1986
|
+
output: unknown;
|
|
1987
|
+
callerCapabilities: readonly string[];
|
|
1988
|
+
errors?: readonly string[];
|
|
1989
|
+
declaredErrorTypes?: readonly string[];
|
|
1990
|
+
runtimeErrors?: readonly RuntimeRpcErrorDesc[];
|
|
1991
|
+
},
|
|
1992
|
+
opts?: RequestOpts,
|
|
1993
|
+
): AsyncResult<TOutput, BaseError> {
|
|
1994
|
+
return AsyncResult.from((async () => {
|
|
1995
|
+
this.#log.trace(
|
|
1996
|
+
{ method: String(method) },
|
|
1997
|
+
`Calling ${method.toString()}.`,
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
const msg = encodeRuntimeSchema(ctx.input, input).take();
|
|
2001
|
+
if (isErr(msg)) {
|
|
2002
|
+
return msg;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
const subject = this.template(ctx.subject, input).take();
|
|
2006
|
+
if (isErr(subject)) {
|
|
2007
|
+
return subject;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const span = startClientSpan(method, subject);
|
|
2011
|
+
const attempt = async (): Promise<Result<TOutput, BaseError>> => {
|
|
2012
|
+
const authHeaders = await this.#createProof(subject, msg);
|
|
2013
|
+
|
|
2014
|
+
const headers = natsHeaders();
|
|
2015
|
+
headers.set("session-key", this.auth.sessionKey);
|
|
2016
|
+
headers.set("proof", authHeaders.proof);
|
|
2017
|
+
headers.set("iat", String(authHeaders.iat));
|
|
2018
|
+
headers.set("request-id", authHeaders.requestId);
|
|
2019
|
+
injectTraceContext(createNatsHeaderCarrier(headers), span);
|
|
2020
|
+
|
|
2021
|
+
const msgResult = await this.#requestMessageWithRetry({
|
|
2022
|
+
method,
|
|
2023
|
+
subject,
|
|
2024
|
+
payload: msg,
|
|
2025
|
+
headers,
|
|
2026
|
+
timeout: opts?.timeout ?? this.timeout,
|
|
2027
|
+
callerCapabilities: ctx.callerCapabilities,
|
|
2028
|
+
});
|
|
2029
|
+
const response = msgResult.take();
|
|
2030
|
+
if (isErr(response)) {
|
|
2031
|
+
return response;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
if (response.headers?.get("status") === "error") {
|
|
2035
|
+
const json = safeJson(response).take();
|
|
2036
|
+
if (isErr(json)) {
|
|
2037
|
+
return err(requestFailedTransportError({
|
|
2038
|
+
code: "trellis.request.invalid_response",
|
|
2039
|
+
message: "Trellis returned an invalid response.",
|
|
2040
|
+
hint:
|
|
2041
|
+
"Retry the request. If it keeps happening, check the Trellis capability handling this request.",
|
|
2042
|
+
method,
|
|
2043
|
+
subject,
|
|
2044
|
+
cause: json.error.cause,
|
|
2045
|
+
}));
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const errorData = parse(TrellisErrorDataSchema, json).take();
|
|
2049
|
+
if (isErr(errorData)) {
|
|
2050
|
+
return err(requestFailedTransportError({
|
|
2051
|
+
code: "trellis.request.invalid_response",
|
|
2052
|
+
message: "Trellis returned an invalid response.",
|
|
2053
|
+
hint:
|
|
2054
|
+
"Retry the request. If it keeps happening, check the Trellis capability handling this request.",
|
|
2055
|
+
method,
|
|
2056
|
+
subject,
|
|
2057
|
+
cause: errorData.error,
|
|
2058
|
+
}));
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
const declaredErrorTypes = Array.isArray(ctx.declaredErrorTypes)
|
|
2062
|
+
? ctx.declaredErrorTypes.filter((value): value is string =>
|
|
2063
|
+
typeof value === "string"
|
|
2064
|
+
)
|
|
2065
|
+
: ctx.errors;
|
|
2066
|
+
const runtimeErrors = Array.isArray(ctx.runtimeErrors)
|
|
2067
|
+
? ctx.runtimeErrors.filter(isRuntimeRpcErrorDesc)
|
|
2068
|
+
: undefined;
|
|
2069
|
+
const reconstructed = reconstructDeclaredRpcError(
|
|
2070
|
+
declaredErrorTypes,
|
|
2071
|
+
runtimeErrors,
|
|
2072
|
+
errorData,
|
|
2073
|
+
json,
|
|
2074
|
+
);
|
|
2075
|
+
if (reconstructed) {
|
|
2076
|
+
await this.#handleBrowserAuthRequired(reconstructed);
|
|
2077
|
+
return err(reconstructed);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const remoteError = new RemoteError({ error: errorData });
|
|
2081
|
+
await this.#handleBrowserAuthRequired(remoteError);
|
|
2082
|
+
return err(remoteError);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const json = safeJson(response).take();
|
|
2086
|
+
if (isErr(json)) {
|
|
2087
|
+
return err(requestFailedTransportError({
|
|
2088
|
+
code: "trellis.request.invalid_response",
|
|
2089
|
+
message: "Trellis returned an invalid response.",
|
|
2090
|
+
hint:
|
|
2091
|
+
"Retry the request. If it keeps happening, check the Trellis capability handling this request.",
|
|
2092
|
+
method,
|
|
2093
|
+
subject,
|
|
2094
|
+
cause: json.error.cause,
|
|
2095
|
+
}));
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const outputResult = parseRuntimeSchema(ctx.output, json).take();
|
|
2099
|
+
if (isErr(outputResult)) {
|
|
2100
|
+
return err(outputResult.error);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
return ok(outputResult as TOutput);
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
return await withSpanAsync(span, async () => {
|
|
2107
|
+
try {
|
|
2108
|
+
const result = await attempt();
|
|
2109
|
+
const value = result.take();
|
|
2110
|
+
if (isErr(value)) {
|
|
2111
|
+
span.setStatus({
|
|
2112
|
+
code: SpanStatusCode.ERROR,
|
|
2113
|
+
message: value.error.message,
|
|
2114
|
+
});
|
|
2115
|
+
} else {
|
|
2116
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
2117
|
+
}
|
|
2118
|
+
return result;
|
|
2119
|
+
} catch (cause) {
|
|
2120
|
+
const unexpected = cause instanceof TransportError
|
|
2121
|
+
? cause
|
|
2122
|
+
: new UnexpectedError({ cause });
|
|
2123
|
+
span.setStatus({
|
|
2124
|
+
code: SpanStatusCode.ERROR,
|
|
2125
|
+
message: unexpected.message,
|
|
2126
|
+
});
|
|
2127
|
+
span.recordException(unexpected);
|
|
2128
|
+
return err(unexpected);
|
|
2129
|
+
} finally {
|
|
2130
|
+
span.end();
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
})());
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async #handleBrowserAuthRequired(error: unknown): Promise<void> {
|
|
2137
|
+
if (
|
|
2138
|
+
!this.#onSessionNotFound || !isBrowserAuthRequiredError(error)
|
|
2139
|
+
) {
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
await this.#onSessionNotFound();
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
async #authenticateFeedRequest(args: {
|
|
2147
|
+
feed: string;
|
|
2148
|
+
subject: string;
|
|
2149
|
+
msg: Msg;
|
|
2150
|
+
payloadHash: Uint8Array;
|
|
2151
|
+
requiredCapabilities: readonly string[];
|
|
2152
|
+
}): Promise<Result<SessionCaller, BaseError>> {
|
|
2153
|
+
const sessionKey = args.msg.headers?.get("session-key");
|
|
2154
|
+
const proof = args.msg.headers?.get("proof");
|
|
2155
|
+
const iatHeader = args.msg.headers?.get("iat");
|
|
2156
|
+
const requestId = args.msg.headers?.get("request-id");
|
|
2157
|
+
if (!sessionKey) {
|
|
2158
|
+
return err(new AuthError({ reason: "missing_session_key" }));
|
|
2159
|
+
}
|
|
2160
|
+
if (!proof) return err(new AuthError({ reason: "missing_proof" }));
|
|
2161
|
+
const iat = Number(iatHeader);
|
|
2162
|
+
if (!Number.isSafeInteger(iat) || !requestId) {
|
|
2163
|
+
return err(new AuthError({ reason: "invalid_signature" }));
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const proofInput = buildProofInput(
|
|
2167
|
+
sessionKey,
|
|
2168
|
+
args.subject,
|
|
2169
|
+
args.payloadHash,
|
|
2170
|
+
iat,
|
|
2171
|
+
requestId,
|
|
2172
|
+
);
|
|
2173
|
+
const digest = await sha256(proofInput);
|
|
2174
|
+
const verifyResult = await AsyncResult.try(async () => {
|
|
2175
|
+
const publicKeyRaw = base64urlDecode(sessionKey);
|
|
2176
|
+
const pub = await crypto.subtle.importKey(
|
|
2177
|
+
"raw",
|
|
2178
|
+
toArrayBuffer(publicKeyRaw),
|
|
2179
|
+
{ name: "Ed25519" },
|
|
2180
|
+
true,
|
|
2181
|
+
["verify"],
|
|
2182
|
+
);
|
|
2183
|
+
return crypto.subtle.verify(
|
|
2184
|
+
{ name: "Ed25519" },
|
|
2185
|
+
pub,
|
|
2186
|
+
toArrayBuffer(base64urlDecode(proof)),
|
|
2187
|
+
toArrayBuffer(digest),
|
|
2188
|
+
);
|
|
2189
|
+
});
|
|
2190
|
+
if (!verifyResult.isOk() || verifyResult.take() !== true) {
|
|
2191
|
+
return err(
|
|
2192
|
+
new AuthError({ reason: "invalid_signature", context: { sessionKey } }),
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const auth = await this.requestAuthValidate({
|
|
2197
|
+
sessionKey,
|
|
2198
|
+
proof,
|
|
2199
|
+
subject: args.subject,
|
|
2200
|
+
payloadHash: base64urlEncode(args.payloadHash),
|
|
2201
|
+
iat,
|
|
2202
|
+
requestId,
|
|
2203
|
+
capabilities: [...args.requiredCapabilities],
|
|
2204
|
+
}).take();
|
|
2205
|
+
if (isErr(auth)) return err(auth.error);
|
|
2206
|
+
|
|
2207
|
+
if (!auth.allowed) {
|
|
2208
|
+
return err(
|
|
2209
|
+
new AuthError({
|
|
2210
|
+
reason: "insufficient_permissions",
|
|
2211
|
+
context: {
|
|
2212
|
+
feed: args.feed,
|
|
2213
|
+
requiredCapabilities: args.requiredCapabilities,
|
|
2214
|
+
userCapabilities: auth.caller.capabilities,
|
|
2215
|
+
},
|
|
2216
|
+
}),
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
if (
|
|
2221
|
+
typeof args.msg.reply !== "string" ||
|
|
2222
|
+
!args.msg.reply.startsWith(`${auth.inboxPrefix}.`)
|
|
2223
|
+
) {
|
|
2224
|
+
return err(
|
|
2225
|
+
new AuthError({
|
|
2226
|
+
reason: "reply_subject_mismatch",
|
|
2227
|
+
context: { expected: auth.inboxPrefix, actual: args.msg.reply },
|
|
2228
|
+
}),
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
return ok(auth.caller);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
feed<F extends FeedsOf<TA>>(
|
|
2236
|
+
feed: F,
|
|
2237
|
+
):
|
|
2238
|
+
& FeedInputBuilder<FeedInputOf<TA, F>, FeedEventOf<TA, F>>
|
|
2239
|
+
& FeedRegistration<FeedInputOf<TA, F>, FeedEventOf<TA, F>> {
|
|
2240
|
+
const descriptor = this.api.feeds?.[feed] as
|
|
2241
|
+
| FeedDescriptorOf<TA, F>
|
|
2242
|
+
| undefined;
|
|
2243
|
+
if (!descriptor) {
|
|
2244
|
+
throw this.#unknownApiError("feed", feed.toString());
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
return {
|
|
2248
|
+
input: (input: FeedInputOf<TA, F>) => ({
|
|
2249
|
+
subscribe: (opts?: FeedSubscribeOpts) =>
|
|
2250
|
+
this.#subscribeFeed(
|
|
2251
|
+
feed.toString(),
|
|
2252
|
+
descriptor,
|
|
2253
|
+
input,
|
|
2254
|
+
opts,
|
|
2255
|
+
) as AsyncResult<
|
|
2256
|
+
FeedSubscription<FeedEventOf<TA, F>>,
|
|
2257
|
+
BaseError
|
|
2258
|
+
>,
|
|
2259
|
+
}),
|
|
2260
|
+
handle: (
|
|
2261
|
+
handler: (
|
|
2262
|
+
context: FeedHandlerContext<FeedInputOf<TA, F>, FeedEventOf<TA, F>>,
|
|
2263
|
+
) => unknown | Promise<unknown>,
|
|
2264
|
+
) => this.#handleFeed(feed.toString(), descriptor, handler),
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
#subscribeFeed<TInput, TEvent>(
|
|
2269
|
+
feed: string,
|
|
2270
|
+
descriptor: FeedDesc,
|
|
2271
|
+
input: TInput,
|
|
2272
|
+
opts?: FeedSubscribeOpts,
|
|
2273
|
+
): AsyncResult<FeedSubscription<TEvent>, BaseError> {
|
|
2274
|
+
return AsyncResult.from((async () => {
|
|
2275
|
+
const payload = encodeRuntimeSchema(descriptor.input, input).take();
|
|
2276
|
+
if (isErr(payload)) return payload;
|
|
2277
|
+
|
|
2278
|
+
const subject = this.template(
|
|
2279
|
+
descriptor.subject,
|
|
2280
|
+
input as Record<string, unknown>,
|
|
2281
|
+
).take();
|
|
2282
|
+
if (isErr(subject)) return subject;
|
|
2283
|
+
|
|
2284
|
+
const authHeaders = await this.#createProof(subject, payload);
|
|
2285
|
+
const headers = natsHeaders();
|
|
2286
|
+
headers.set("session-key", this.auth.sessionKey);
|
|
2287
|
+
headers.set("proof", authHeaders.proof);
|
|
2288
|
+
headers.set("iat", String(authHeaders.iat));
|
|
2289
|
+
headers.set("request-id", authHeaders.requestId);
|
|
2290
|
+
injectTraceContext(createNatsHeaderCarrier(headers));
|
|
2291
|
+
|
|
2292
|
+
const inbox = createInbox(`_INBOX.${this.auth.sessionKey.slice(0, 16)}`);
|
|
2293
|
+
const sub = this.nats.subscribe(inbox);
|
|
2294
|
+
const iterator = sub[Symbol.asyncIterator]();
|
|
2295
|
+
const abort = () => sub.unsubscribe();
|
|
2296
|
+
opts?.signal?.addEventListener("abort", abort, { once: true });
|
|
2297
|
+
|
|
2298
|
+
try {
|
|
2299
|
+
this.nats.publish(subject, payload, { headers, reply: inbox });
|
|
2300
|
+
await this.nats.flush();
|
|
2301
|
+
} catch (cause) {
|
|
2302
|
+
opts?.signal?.removeEventListener("abort", abort);
|
|
2303
|
+
sub.unsubscribe();
|
|
2304
|
+
return err(createTransportError({
|
|
2305
|
+
code: "trellis.feed.subscribe_failed",
|
|
2306
|
+
message: "Trellis could not subscribe to the feed.",
|
|
2307
|
+
hint:
|
|
2308
|
+
"Retry the subscription. If it keeps failing, check Trellis runtime health.",
|
|
2309
|
+
cause,
|
|
2310
|
+
context: { feed, subject },
|
|
2311
|
+
}));
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
2315
|
+
let abortHandler: (() => void) | undefined;
|
|
2316
|
+
const handshakePromises: Array<
|
|
2317
|
+
Promise<IteratorResult<Msg> | "aborted" | "timeout">
|
|
2318
|
+
> = [
|
|
2319
|
+
iterator.next(),
|
|
2320
|
+
new Promise<"timeout">((resolve) => {
|
|
2321
|
+
timeoutId = setTimeout(() => resolve("timeout"), this.timeout);
|
|
2322
|
+
}),
|
|
2323
|
+
];
|
|
2324
|
+
const signal = opts?.signal;
|
|
2325
|
+
if (signal) {
|
|
2326
|
+
handshakePromises.push(
|
|
2327
|
+
new Promise<"aborted">((resolve) => {
|
|
2328
|
+
abortHandler = () => resolve("aborted");
|
|
2329
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
2330
|
+
}),
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
const firstFrame = await Promise.race(handshakePromises);
|
|
2335
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
2336
|
+
if (signal && abortHandler) {
|
|
2337
|
+
signal.removeEventListener("abort", abortHandler);
|
|
2338
|
+
}
|
|
2339
|
+
if (firstFrame === "timeout" || firstFrame === "aborted") {
|
|
2340
|
+
opts?.signal?.removeEventListener("abort", abort);
|
|
2341
|
+
sub.unsubscribe();
|
|
2342
|
+
return err(createTransportError({
|
|
2343
|
+
code: firstFrame === "timeout"
|
|
2344
|
+
? "trellis.feed.subscribe_timeout"
|
|
2345
|
+
: "trellis.feed.subscribe_aborted",
|
|
2346
|
+
message: firstFrame === "timeout"
|
|
2347
|
+
? "Trellis did not receive a feed acknowledgement."
|
|
2348
|
+
: "The feed subscription was aborted before Trellis acknowledged it.",
|
|
2349
|
+
hint: firstFrame === "timeout"
|
|
2350
|
+
? "Check that the target service is running and has the current deployment digest, then retry."
|
|
2351
|
+
: "Retry the subscription if the feed is still needed.",
|
|
2352
|
+
context: { feed, subject },
|
|
2353
|
+
}));
|
|
2354
|
+
}
|
|
2355
|
+
if (firstFrame.done) {
|
|
2356
|
+
opts?.signal?.removeEventListener("abort", abort);
|
|
2357
|
+
sub.unsubscribe();
|
|
2358
|
+
return err(createTransportError({
|
|
2359
|
+
code: "trellis.feed.subscribe_closed",
|
|
2360
|
+
message: "Trellis closed the feed before acknowledging it.",
|
|
2361
|
+
hint:
|
|
2362
|
+
"Retry the subscription. If it keeps failing, check Trellis runtime health.",
|
|
2363
|
+
context: { feed, subject },
|
|
2364
|
+
}));
|
|
2365
|
+
}
|
|
2366
|
+
const firstMessage = firstFrame.value;
|
|
2367
|
+
if (firstMessage.headers?.get("status") === "error") {
|
|
2368
|
+
opts?.signal?.removeEventListener("abort", abort);
|
|
2369
|
+
sub.unsubscribe();
|
|
2370
|
+
return err(createTransportError({
|
|
2371
|
+
code: "trellis.feed.failed",
|
|
2372
|
+
message: "Trellis rejected the feed subscription.",
|
|
2373
|
+
hint:
|
|
2374
|
+
"Retry the subscription. If it keeps failing, check Trellis runtime health and permissions.",
|
|
2375
|
+
context: { feed, subject, frame: firstMessage.string() },
|
|
2376
|
+
}));
|
|
2377
|
+
}
|
|
2378
|
+
const firstEvent = firstMessage.headers?.get("feed-status") === "ready"
|
|
2379
|
+
? undefined
|
|
2380
|
+
: firstMessage;
|
|
2381
|
+
|
|
2382
|
+
const eventSchema = descriptor.event;
|
|
2383
|
+
return ok((async function* () {
|
|
2384
|
+
try {
|
|
2385
|
+
const parseFeedFrame = (msg: Msg): TEvent => {
|
|
2386
|
+
if (msg.headers?.get("status") === "error") {
|
|
2387
|
+
throw createTransportError({
|
|
2388
|
+
code: "trellis.feed.failed",
|
|
2389
|
+
message: "Trellis stopped the feed.",
|
|
2390
|
+
hint:
|
|
2391
|
+
"Retry the subscription. If it keeps failing, check Trellis runtime health.",
|
|
2392
|
+
context: { feed, subject, frame: msg.string() },
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
const json = safeJson(msg).take();
|
|
2396
|
+
if (isErr(json)) throw json.error;
|
|
2397
|
+
const parsed = parseRuntimeSchema(eventSchema, json).take();
|
|
2398
|
+
if (isErr(parsed)) throw parsed.error;
|
|
2399
|
+
return parsed as TEvent;
|
|
2400
|
+
};
|
|
2401
|
+
if (firstEvent) yield parseFeedFrame(firstEvent);
|
|
2402
|
+
while (true) {
|
|
2403
|
+
const next = await iterator.next();
|
|
2404
|
+
if (next.done) break;
|
|
2405
|
+
yield parseFeedFrame(next.value);
|
|
2406
|
+
}
|
|
2407
|
+
} finally {
|
|
2408
|
+
opts?.signal?.removeEventListener("abort", abort);
|
|
2409
|
+
sub.unsubscribe();
|
|
2410
|
+
}
|
|
2411
|
+
})());
|
|
2412
|
+
})());
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
async #handleFeed<TInput, TEvent>(
|
|
2416
|
+
feed: string,
|
|
2417
|
+
descriptor: FeedDesc,
|
|
2418
|
+
handler: (
|
|
2419
|
+
context: FeedHandlerContext<TInput, TEvent>,
|
|
2420
|
+
) => unknown | Promise<unknown>,
|
|
2421
|
+
): Promise<void> {
|
|
2422
|
+
const subject = this.template(descriptor.subject, {}, true).take();
|
|
2423
|
+
if (isErr(subject)) throw subject.error;
|
|
2424
|
+
let sub: ReturnType<NatsConnection["subscribe"]>;
|
|
2425
|
+
try {
|
|
2426
|
+
sub = this.nats.subscribe(subject);
|
|
2427
|
+
await this.nats.flush();
|
|
2428
|
+
} catch (cause) {
|
|
2429
|
+
throw createTransportError({
|
|
2430
|
+
code: "trellis.feed.listen_failed",
|
|
2431
|
+
message: "Trellis could not listen for feed requests.",
|
|
2432
|
+
hint:
|
|
2433
|
+
"Check the service deployment digest and runtime permissions, then restart the service.",
|
|
2434
|
+
cause,
|
|
2435
|
+
context: { feed, subject },
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
const task = AsyncResult.try(async () => {
|
|
2439
|
+
for await (const msg of sub) {
|
|
2440
|
+
void (async () => {
|
|
2441
|
+
try {
|
|
2442
|
+
const result = await this.#processFeedMessage(
|
|
2443
|
+
feed,
|
|
2444
|
+
descriptor,
|
|
2445
|
+
msg,
|
|
2446
|
+
handler,
|
|
2447
|
+
);
|
|
2448
|
+
const value = result.take();
|
|
2449
|
+
if (isErr(value)) {
|
|
2450
|
+
this.#respondWithError(msg, value.error);
|
|
2451
|
+
}
|
|
2452
|
+
} catch (cause) {
|
|
2453
|
+
const error = cause instanceof BaseError
|
|
2454
|
+
? cause
|
|
2455
|
+
: new UnexpectedError({ cause });
|
|
2456
|
+
this.#respondWithError(msg, error);
|
|
2457
|
+
}
|
|
2458
|
+
})();
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
this.#tasks.add(`feed:${feed}`, task);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
async #processFeedMessage<TInput, TEvent>(
|
|
2465
|
+
feed: string,
|
|
2466
|
+
descriptor: FeedDesc,
|
|
2467
|
+
msg: Msg,
|
|
2468
|
+
handler: (
|
|
2469
|
+
context: FeedHandlerContext<TInput, TEvent>,
|
|
2470
|
+
) => unknown | Promise<unknown>,
|
|
2471
|
+
): Promise<Result<void, BaseError>> {
|
|
2472
|
+
const json = safeJson(msg).take();
|
|
2473
|
+
if (isErr(json)) return json;
|
|
2474
|
+
const parsed = parseRuntimeSchema(descriptor.input, json).take();
|
|
2475
|
+
if (isErr(parsed)) return parsed;
|
|
2476
|
+
|
|
2477
|
+
const caller = await this.#authenticateFeedRequest({
|
|
2478
|
+
feed,
|
|
2479
|
+
subject: msg.subject,
|
|
2480
|
+
msg,
|
|
2481
|
+
payloadHash: await sha256(msg.data ?? new Uint8Array()),
|
|
2482
|
+
requiredCapabilities: descriptor.subscribeCapabilities,
|
|
2483
|
+
});
|
|
2484
|
+
const callerValue = caller.take();
|
|
2485
|
+
if (isErr(callerValue)) return callerValue;
|
|
2486
|
+
if (!msg.reply) {
|
|
2487
|
+
return err(
|
|
2488
|
+
new UnexpectedError({
|
|
2489
|
+
context: { feed, reason: "missing_reply" },
|
|
2490
|
+
}),
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
const readyHeaders = natsHeaders();
|
|
2494
|
+
readyHeaders.set("feed-status", "ready");
|
|
2495
|
+
this.nats.publish(msg.reply, new Uint8Array(), { headers: readyHeaders });
|
|
2496
|
+
await this.nats.flush();
|
|
2497
|
+
|
|
2498
|
+
const controller = new AbortController();
|
|
2499
|
+
try {
|
|
2500
|
+
await handler({
|
|
2501
|
+
input: parsed as TInput,
|
|
2502
|
+
caller: callerValue,
|
|
2503
|
+
signal: controller.signal,
|
|
2504
|
+
emit: (event: TEvent) =>
|
|
2505
|
+
AsyncResult.from((async () => {
|
|
2506
|
+
const payload = encodeRuntimeSchema(descriptor.event, event).take();
|
|
2507
|
+
if (isErr(payload)) return payload;
|
|
2508
|
+
if (!msg.reply) {
|
|
2509
|
+
return err(
|
|
2510
|
+
new UnexpectedError({
|
|
2511
|
+
context: { feed, reason: "missing_reply" },
|
|
2512
|
+
}),
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
this.nats.publish(msg.reply, payload);
|
|
2516
|
+
await this.nats.flush();
|
|
2517
|
+
return ok(undefined);
|
|
2518
|
+
})()),
|
|
2519
|
+
});
|
|
2520
|
+
return ok(undefined);
|
|
2521
|
+
} finally {
|
|
2522
|
+
controller.abort();
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
operation<O extends OperationsOf<TA>>(
|
|
2527
|
+
operation: O,
|
|
2528
|
+
): OperationSurface<TA, TMode, O> {
|
|
2529
|
+
const descriptor = this.api["operations"]?.[operation];
|
|
2530
|
+
if (!descriptor) {
|
|
2531
|
+
throw this.#unknownApiError("operation", operation.toString());
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
const transport: OperationTransport = {
|
|
2535
|
+
requestJson: (subject, body) =>
|
|
2536
|
+
this.#requestJson(subject, body as JsonValue),
|
|
2537
|
+
watchJson: (subject, body) => this.#watchJson(subject, body as JsonValue),
|
|
2538
|
+
putTransfer: (
|
|
2539
|
+
grant: SendTransferGrant,
|
|
2540
|
+
body: TransferBody,
|
|
2541
|
+
): AsyncResult<FileInfo, TransferError> =>
|
|
2542
|
+
AsyncResult.from((async () => {
|
|
2543
|
+
const handle = createTransferHandle(
|
|
2544
|
+
this.nats,
|
|
2545
|
+
this.auth,
|
|
2546
|
+
this.timeout,
|
|
2547
|
+
grant,
|
|
2548
|
+
);
|
|
2549
|
+
if (!(handle instanceof Object) || !("send" in handle)) {
|
|
2550
|
+
return err(
|
|
2551
|
+
new TransferError({
|
|
2552
|
+
operation: "transfer",
|
|
2553
|
+
context: { reason: "invalid_operation_transfer_grant" },
|
|
2554
|
+
}),
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
return await handle.send(body);
|
|
2558
|
+
})()),
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
return new OperationInvoker(
|
|
2562
|
+
transport,
|
|
2563
|
+
descriptor as TA["operations"][O] & RuntimeOperationDesc,
|
|
2564
|
+
) as OperationSurface<TA, TMode, O>;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Creates a helper for a short-lived Trellis transfer grant.
|
|
2569
|
+
*/
|
|
2570
|
+
transfer(grant: SendTransferGrant): SendTransferHandle;
|
|
2571
|
+
transfer(grant: ReceiveTransferGrant): ReceiveTransferHandle;
|
|
2572
|
+
transfer(grant: TransferGrant): ReturnType<typeof createTransferHandle> {
|
|
2573
|
+
return createTransferHandle(this.nats, this.auth, this.timeout, grant);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/*
|
|
2577
|
+
* Mount a handler to process requests made to a specific Trellis API
|
|
2578
|
+
*/
|
|
2579
|
+
async mount(
|
|
2580
|
+
method: string,
|
|
2581
|
+
fn: (args: {
|
|
2582
|
+
input: unknown;
|
|
2583
|
+
context: RpcHandlerContext;
|
|
2584
|
+
trellis: HandlerTrellis<TA, TRequests>;
|
|
2585
|
+
}) => MaybePromise<Result<unknown, BaseError>>,
|
|
2586
|
+
) {
|
|
2587
|
+
const methodName = method as MethodsOf<TA>;
|
|
2588
|
+
const ctx = this.api["rpc"][methodName];
|
|
2589
|
+
if (!ctx) {
|
|
2590
|
+
throw this.#unknownApiError("RPC method", method.toString());
|
|
2591
|
+
}
|
|
2592
|
+
const task = this.#handleRPC(
|
|
2593
|
+
methodName,
|
|
2594
|
+
fn as HandlerFn<TA, MethodsOf<TA>, TA, HandlerTrellis<TA, TRequests>>,
|
|
2595
|
+
);
|
|
2596
|
+
this.#tasks.add(methodName, task);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
#handleRPC(
|
|
2600
|
+
method: MethodsOf<TA>,
|
|
2601
|
+
fn: HandlerFn<TA, MethodsOf<TA>, TA, HandlerTrellis<TA, TRequests>>,
|
|
2602
|
+
subjectData: Record<string, unknown> = {},
|
|
2603
|
+
): AsyncResult<void, ValidationError | UnexpectedError> {
|
|
2604
|
+
// Get API details
|
|
2605
|
+
const ctx = this.api["rpc"][method] as RpcDescriptorOf<TA, MethodsOf<TA>>;
|
|
2606
|
+
|
|
2607
|
+
const subject = this.template(ctx.subject, subjectData, true).take();
|
|
2608
|
+
if (isErr(subject)) {
|
|
2609
|
+
return AsyncResult.lift(subject);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
const handlerTrellis: HandlerTrellis<TA, TRequests> = this;
|
|
2613
|
+
|
|
2614
|
+
this.#log.info(
|
|
2615
|
+
{ method: String(method) },
|
|
2616
|
+
`Mounting ${method.toString()} RPC handler`,
|
|
2617
|
+
);
|
|
2618
|
+
const sub = this.nats.subscribe(subject);
|
|
2619
|
+
|
|
2620
|
+
return AsyncResult.try(async () => {
|
|
2621
|
+
for await (const msg of sub) {
|
|
2622
|
+
const resultPromise = await this.#processRPCMessage(
|
|
2623
|
+
method,
|
|
2624
|
+
ctx,
|
|
2625
|
+
msg,
|
|
2626
|
+
fn,
|
|
2627
|
+
handlerTrellis,
|
|
2628
|
+
);
|
|
2629
|
+
const result = resultPromise.take();
|
|
2630
|
+
|
|
2631
|
+
if (isErr(result)) {
|
|
2632
|
+
this.#respondWithError(msg, result.error, { method: String(method) });
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
const sent = this.#respondWithPayload(msg, result, undefined, {
|
|
2637
|
+
method: String(method),
|
|
2638
|
+
responseKind: "success",
|
|
2639
|
+
});
|
|
2640
|
+
if (sent.isErr()) {
|
|
2641
|
+
const responseBytes = payloadByteLength(result);
|
|
2642
|
+
const message = causeMessage(sent.error.cause);
|
|
2643
|
+
this.#respondWithError(
|
|
2644
|
+
msg,
|
|
2645
|
+
new TransportError({
|
|
2646
|
+
code: "trellis.rpc.response_send_failed",
|
|
2647
|
+
message: message.includes("max_payload")
|
|
2648
|
+
? "Trellis RPC response exceeded NATS max_payload."
|
|
2649
|
+
: "Trellis could not send the RPC response.",
|
|
2650
|
+
hint:
|
|
2651
|
+
"Reduce the requested page size or use a narrower RPC that does not include large detail payloads.",
|
|
2652
|
+
cause: sent.error.cause,
|
|
2653
|
+
context: {
|
|
2654
|
+
method: String(method),
|
|
2655
|
+
subject: msg.subject,
|
|
2656
|
+
responseBytes,
|
|
2657
|
+
causeMessage: message,
|
|
2658
|
+
},
|
|
2659
|
+
}),
|
|
2660
|
+
{ method: String(method), responseBytes },
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
async #processRPCMessage(
|
|
2668
|
+
method: MethodsOf<TA>,
|
|
2669
|
+
ctx: RpcDescriptorOf<TA, MethodsOf<TA>>,
|
|
2670
|
+
msg: Msg,
|
|
2671
|
+
fn: HandlerFn<TA, MethodsOf<TA>, TA, HandlerTrellis<TA, TRequests>>,
|
|
2672
|
+
handlerTrellis: HandlerTrellis<TA, TRequests>,
|
|
2673
|
+
): Promise<Result<string, BaseError>> {
|
|
2674
|
+
this.#log.debug(
|
|
2675
|
+
{ method: String(method), subject: msg.subject },
|
|
2676
|
+
"Processing RPC message",
|
|
2677
|
+
);
|
|
2678
|
+
|
|
2679
|
+
// Extract trace context from incoming NATS headers
|
|
2680
|
+
const parentContext = extractTraceContext(
|
|
2681
|
+
createNatsHeaderCarrier({
|
|
2682
|
+
get: (k: string) => msg.headers?.get(k) ?? undefined,
|
|
2683
|
+
set: () => {}, // Server doesn't need to set headers on incoming messages
|
|
2684
|
+
}),
|
|
2685
|
+
);
|
|
2686
|
+
|
|
2687
|
+
// Start a server span for this RPC handler
|
|
2688
|
+
const span = startServerSpan(method, msg.subject, parentContext);
|
|
2689
|
+
const incomingTraceId = traceIdFromTraceparent(
|
|
2690
|
+
msg.headers?.get("traceparent"),
|
|
2691
|
+
);
|
|
2692
|
+
|
|
2693
|
+
// Execute the handler within the span's context
|
|
2694
|
+
return withSpanAsync(span, async () => {
|
|
2695
|
+
const execute = async (): Promise<Result<string, BaseError>> => {
|
|
2696
|
+
const jsonData = safeJson(msg).take();
|
|
2697
|
+
if (isErr(jsonData)) {
|
|
2698
|
+
this.#log.warn(
|
|
2699
|
+
{ method, error: jsonData.error.message },
|
|
2700
|
+
"Failed to parse JSON",
|
|
2701
|
+
);
|
|
2702
|
+
span.setStatus({
|
|
2703
|
+
code: SpanStatusCode.ERROR,
|
|
2704
|
+
message: "Failed to parse JSON",
|
|
2705
|
+
});
|
|
2706
|
+
return jsonData;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
const parsedInput = parseRuntimeSchema(ctx.input, jsonData).take();
|
|
2710
|
+
if (isErr(parsedInput)) {
|
|
2711
|
+
span.setStatus({
|
|
2712
|
+
code: SpanStatusCode.ERROR,
|
|
2713
|
+
message: "Input validation failed",
|
|
2714
|
+
});
|
|
2715
|
+
return parsedInput;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
let caller: SessionCaller;
|
|
2719
|
+
const callerSessionKey = msg.headers?.get("session-key") ?? "";
|
|
2720
|
+
|
|
2721
|
+
const authRequired = ctx.authRequired ?? true;
|
|
2722
|
+
if (!authRequired) {
|
|
2723
|
+
caller = {
|
|
2724
|
+
type: "service",
|
|
2725
|
+
id: "system",
|
|
2726
|
+
active: true,
|
|
2727
|
+
name: "System",
|
|
2728
|
+
capabilities: ["service"],
|
|
2729
|
+
};
|
|
2730
|
+
} else {
|
|
2731
|
+
const sessionKey = msg.headers?.get("session-key");
|
|
2732
|
+
const proof = msg.headers?.get("proof");
|
|
2733
|
+
const iatHeader = msg.headers?.get("iat");
|
|
2734
|
+
const requestId = msg.headers?.get("request-id");
|
|
2735
|
+
if (!sessionKey) {
|
|
2736
|
+
this.#log.warn({ method }, "Missing session-key header");
|
|
2737
|
+
span.setStatus({
|
|
2738
|
+
code: SpanStatusCode.ERROR,
|
|
2739
|
+
message: "Missing session-key",
|
|
2740
|
+
});
|
|
2741
|
+
return err(new AuthError({ reason: "missing_session_key" }));
|
|
2742
|
+
}
|
|
2743
|
+
if (!proof) {
|
|
2744
|
+
this.#log.warn({ method }, "Missing proof in request");
|
|
2745
|
+
span.setStatus({
|
|
2746
|
+
code: SpanStatusCode.ERROR,
|
|
2747
|
+
message: "Missing proof",
|
|
2748
|
+
});
|
|
2749
|
+
return err(new AuthError({ reason: "missing_proof" }));
|
|
2750
|
+
}
|
|
2751
|
+
const iat = Number(iatHeader);
|
|
2752
|
+
if (!Number.isSafeInteger(iat) || !requestId) {
|
|
2753
|
+
return err(new AuthError({ reason: "invalid_signature" }));
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// Verify proof signature locally using the raw request bytes we received.
|
|
2757
|
+
const payloadBytes = msg.data ?? new Uint8Array();
|
|
2758
|
+
const payloadHash = await sha256(payloadBytes);
|
|
2759
|
+
const proofInput = buildProofInput(
|
|
2760
|
+
sessionKey,
|
|
2761
|
+
msg.subject,
|
|
2762
|
+
payloadHash,
|
|
2763
|
+
iat,
|
|
2764
|
+
requestId,
|
|
2765
|
+
);
|
|
2766
|
+
const digest = await sha256(proofInput);
|
|
2767
|
+
|
|
2768
|
+
const verifyResult = await AsyncResult.try(async () => {
|
|
2769
|
+
const publicKeyRaw = base64urlDecode(sessionKey);
|
|
2770
|
+
const pub = await crypto.subtle.importKey(
|
|
2771
|
+
"raw",
|
|
2772
|
+
toArrayBuffer(publicKeyRaw),
|
|
2773
|
+
{ name: "Ed25519" },
|
|
2774
|
+
true,
|
|
2775
|
+
["verify"],
|
|
2776
|
+
);
|
|
2777
|
+
return crypto.subtle.verify(
|
|
2778
|
+
{ name: "Ed25519" },
|
|
2779
|
+
pub,
|
|
2780
|
+
toArrayBuffer(base64urlDecode(proof)),
|
|
2781
|
+
toArrayBuffer(digest),
|
|
2782
|
+
);
|
|
2783
|
+
});
|
|
2784
|
+
const signatureOk = verifyResult.isOk() &&
|
|
2785
|
+
verifyResult.take() === true;
|
|
2786
|
+
|
|
2787
|
+
if (!signatureOk) {
|
|
2788
|
+
span.setStatus({
|
|
2789
|
+
code: SpanStatusCode.ERROR,
|
|
2790
|
+
message: "Invalid signature",
|
|
2791
|
+
});
|
|
2792
|
+
return err(
|
|
2793
|
+
new AuthError({
|
|
2794
|
+
reason: "invalid_signature",
|
|
2795
|
+
context: { sessionKey },
|
|
2796
|
+
}),
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
let auth:
|
|
2801
|
+
| AuthRequestsValidateResponse
|
|
2802
|
+
| AuthError
|
|
2803
|
+
| RemoteError
|
|
2804
|
+
| TransportError
|
|
2805
|
+
| ValidationError
|
|
2806
|
+
| UnexpectedError
|
|
2807
|
+
| undefined;
|
|
2808
|
+
for (
|
|
2809
|
+
let attempt = 0;
|
|
2810
|
+
attempt < DEFAULT_AUTH_VALIDATE_SESSION_RETRY_ATTEMPTS;
|
|
2811
|
+
attempt++
|
|
2812
|
+
) {
|
|
2813
|
+
const authValue = await this.requestAuthValidate({
|
|
2814
|
+
sessionKey,
|
|
2815
|
+
proof,
|
|
2816
|
+
subject: msg.subject,
|
|
2817
|
+
payloadHash: base64urlEncode(payloadHash),
|
|
2818
|
+
iat,
|
|
2819
|
+
requestId,
|
|
2820
|
+
capabilities: [...ctx.callerCapabilities],
|
|
2821
|
+
}).take();
|
|
2822
|
+
if (!isErr(authValue)) {
|
|
2823
|
+
auth = authValue;
|
|
2824
|
+
break;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
const authError = authValue.error;
|
|
2828
|
+
|
|
2829
|
+
if (
|
|
2830
|
+
!isTransientAuthValidateSessionError(authError) ||
|
|
2831
|
+
attempt === DEFAULT_AUTH_VALIDATE_SESSION_RETRY_ATTEMPTS - 1
|
|
2832
|
+
) {
|
|
2833
|
+
auth = authError;
|
|
2834
|
+
break;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
await sleep(
|
|
2838
|
+
DEFAULT_AUTH_VALIDATE_SESSION_RETRY_MS * (attempt + 1),
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
if (!auth) {
|
|
2843
|
+
return err(
|
|
2844
|
+
new UnexpectedError({
|
|
2845
|
+
context: { reason: "missing_auth_validate_result" },
|
|
2846
|
+
}),
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
if (auth instanceof Error) {
|
|
2851
|
+
this.#log.warn(
|
|
2852
|
+
{
|
|
2853
|
+
method,
|
|
2854
|
+
error: auth.message,
|
|
2855
|
+
errorType: auth.name,
|
|
2856
|
+
remoteError: auth instanceof RemoteError
|
|
2857
|
+
? auth.toSerializable()
|
|
2858
|
+
: undefined,
|
|
2859
|
+
},
|
|
2860
|
+
"Auth.Requests.Validate failed",
|
|
2861
|
+
);
|
|
2862
|
+
span.setStatus({
|
|
2863
|
+
code: SpanStatusCode.ERROR,
|
|
2864
|
+
message: "Auth.Requests.Validate failed",
|
|
2865
|
+
});
|
|
2866
|
+
if (auth instanceof BaseError) {
|
|
2867
|
+
return err(auth);
|
|
2868
|
+
}
|
|
2869
|
+
return err(new UnexpectedError({ cause: auth }));
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
if (!auth.allowed) {
|
|
2873
|
+
span.setStatus({
|
|
2874
|
+
code: SpanStatusCode.ERROR,
|
|
2875
|
+
message: "Insufficient permissions",
|
|
2876
|
+
});
|
|
2877
|
+
return err(
|
|
2878
|
+
new AuthError({
|
|
2879
|
+
reason: "insufficient_permissions",
|
|
2880
|
+
context: {
|
|
2881
|
+
requiredCapabilities: ctx.callerCapabilities,
|
|
2882
|
+
userCapabilities: auth.caller.capabilities,
|
|
2883
|
+
},
|
|
2884
|
+
}),
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if (
|
|
2889
|
+
typeof msg.reply !== "string" ||
|
|
2890
|
+
!msg.reply.startsWith(`${auth.inboxPrefix}.`)
|
|
2891
|
+
) {
|
|
2892
|
+
span.setStatus({
|
|
2893
|
+
code: SpanStatusCode.ERROR,
|
|
2894
|
+
message: "Reply subject mismatch",
|
|
2895
|
+
});
|
|
2896
|
+
return err(
|
|
2897
|
+
new AuthError({
|
|
2898
|
+
reason: "reply_subject_mismatch",
|
|
2899
|
+
context: { expected: auth.inboxPrefix, actual: msg.reply },
|
|
2900
|
+
}),
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
caller = auth.caller;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
span.setAttribute("auth.caller.type", caller.type);
|
|
2908
|
+
if (caller.type === "user") {
|
|
2909
|
+
span.setAttribute("user.id", caller.userId);
|
|
2910
|
+
span.setAttribute("user.identity.provider", caller.identity.provider);
|
|
2911
|
+
span.setAttribute("user.identity.subject", caller.identity.subject);
|
|
2912
|
+
}
|
|
2913
|
+
if (caller.type === "service") {
|
|
2914
|
+
const { id } = caller;
|
|
2915
|
+
span.setAttribute("service.id", id);
|
|
2916
|
+
}
|
|
2917
|
+
if (caller.type === "device") {
|
|
2918
|
+
span.setAttribute("device.id", caller.deviceId);
|
|
2919
|
+
span.setAttribute("device.deployment_id", caller.deploymentId);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
const invokeHandler = fn as (
|
|
2923
|
+
args: {
|
|
2924
|
+
input: unknown;
|
|
2925
|
+
context: RpcHandlerContext;
|
|
2926
|
+
trellis: HandlerTrellis<TA, TRequests>;
|
|
2927
|
+
},
|
|
2928
|
+
) => MaybeAsync<unknown, BaseError>;
|
|
2929
|
+
const handlerResultWrapped = await AsyncResult.try(async () =>
|
|
2930
|
+
await Promise.resolve(
|
|
2931
|
+
invokeHandler({
|
|
2932
|
+
input: parsedInput,
|
|
2933
|
+
context: {
|
|
2934
|
+
caller,
|
|
2935
|
+
sessionKey: callerSessionKey,
|
|
2936
|
+
},
|
|
2937
|
+
trellis: handlerTrellis,
|
|
2938
|
+
}),
|
|
2939
|
+
)
|
|
2940
|
+
);
|
|
2941
|
+
|
|
2942
|
+
if (handlerResultWrapped.isErr()) {
|
|
2943
|
+
const error = handlerResultWrapped.error.withContext({ method });
|
|
2944
|
+
this.#log.error(
|
|
2945
|
+
{
|
|
2946
|
+
method,
|
|
2947
|
+
error: error.message,
|
|
2948
|
+
cause: error.cause instanceof Error
|
|
2949
|
+
? { message: error.cause.message, stack: error.cause.stack }
|
|
2950
|
+
: error.cause,
|
|
2951
|
+
},
|
|
2952
|
+
"Handler threw unexpectedly.",
|
|
2953
|
+
);
|
|
2954
|
+
span.setStatus({
|
|
2955
|
+
code: SpanStatusCode.ERROR,
|
|
2956
|
+
message: error.message,
|
|
2957
|
+
});
|
|
2958
|
+
span.recordException(error);
|
|
2959
|
+
return err(error);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
const handlerResult = handlerResultWrapped.take() as {
|
|
2963
|
+
take: () => unknown;
|
|
2964
|
+
};
|
|
2965
|
+
const handlerOutcome = handlerResult.take();
|
|
2966
|
+
if (isErr(handlerOutcome)) {
|
|
2967
|
+
const handlerError = handlerOutcome.error;
|
|
2968
|
+
|
|
2969
|
+
const error = handlerError instanceof BaseError &&
|
|
2970
|
+
!(handlerError instanceof RemoteError)
|
|
2971
|
+
? handlerError
|
|
2972
|
+
: new UnexpectedError({ cause: handlerError });
|
|
2973
|
+
|
|
2974
|
+
this.#log.error(
|
|
2975
|
+
{
|
|
2976
|
+
method,
|
|
2977
|
+
error: error.message,
|
|
2978
|
+
errorType: error.name,
|
|
2979
|
+
cause: error.cause instanceof Error
|
|
2980
|
+
? { message: error.cause.message, stack: error.cause.stack }
|
|
2981
|
+
: error.cause,
|
|
2982
|
+
},
|
|
2983
|
+
"Handler returned error.",
|
|
2984
|
+
);
|
|
2985
|
+
span.setStatus({
|
|
2986
|
+
code: SpanStatusCode.ERROR,
|
|
2987
|
+
message: error.message,
|
|
2988
|
+
});
|
|
2989
|
+
return err(error);
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
const encoded = encodeSchema(ctx.output, handlerOutcome).take();
|
|
2993
|
+
if (isErr(encoded)) {
|
|
2994
|
+
span.setStatus({
|
|
2995
|
+
code: SpanStatusCode.ERROR,
|
|
2996
|
+
message: "Output encoding failed",
|
|
2997
|
+
});
|
|
2998
|
+
return encoded;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
3002
|
+
return ok(encoded);
|
|
3003
|
+
};
|
|
3004
|
+
|
|
3005
|
+
const result = await execute();
|
|
3006
|
+
if (isErr(result)) {
|
|
3007
|
+
result.error.withTraceId(activeTraceId(span) ?? incomingTraceId);
|
|
3008
|
+
}
|
|
3009
|
+
span.end();
|
|
3010
|
+
return result;
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
#respondWithPayload(
|
|
3015
|
+
msg: Msg,
|
|
3016
|
+
payload: string,
|
|
3017
|
+
options: { headers?: MsgHdrs } | undefined,
|
|
3018
|
+
context: {
|
|
3019
|
+
method?: string;
|
|
3020
|
+
responseKind: "success" | "error";
|
|
3021
|
+
},
|
|
3022
|
+
): Result<void, UnexpectedError> {
|
|
3023
|
+
const responseBytes = payloadByteLength(payload);
|
|
3024
|
+
try {
|
|
3025
|
+
msg.respond(payload, options);
|
|
3026
|
+
return ok(undefined);
|
|
3027
|
+
} catch (cause) {
|
|
3028
|
+
const error = new UnexpectedError({
|
|
3029
|
+
cause,
|
|
3030
|
+
context: {
|
|
3031
|
+
method: context.method,
|
|
3032
|
+
responseKind: context.responseKind,
|
|
3033
|
+
subject: msg.subject,
|
|
3034
|
+
reply: msg.reply,
|
|
3035
|
+
responseBytes,
|
|
3036
|
+
causeMessage: causeMessage(cause),
|
|
3037
|
+
},
|
|
3038
|
+
});
|
|
3039
|
+
this.#log.error(
|
|
3040
|
+
{
|
|
3041
|
+
method: context.method,
|
|
3042
|
+
responseKind: context.responseKind,
|
|
3043
|
+
subject: msg.subject,
|
|
3044
|
+
reply: msg.reply,
|
|
3045
|
+
responseBytes,
|
|
3046
|
+
cause: causeLogData(cause),
|
|
3047
|
+
},
|
|
3048
|
+
"Failed to send RPC response",
|
|
3049
|
+
);
|
|
3050
|
+
return err(error);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
#respondWithError(
|
|
3055
|
+
msg: Msg,
|
|
3056
|
+
error: Error | BaseError,
|
|
3057
|
+
context: { method?: string; responseBytes?: number } = {},
|
|
3058
|
+
): void {
|
|
3059
|
+
const trellisError = error instanceof BaseError &&
|
|
3060
|
+
!(error instanceof RemoteError)
|
|
3061
|
+
? error
|
|
3062
|
+
: new UnexpectedError({ cause: error });
|
|
3063
|
+
|
|
3064
|
+
this.#log.error(
|
|
3065
|
+
{
|
|
3066
|
+
method: context.method,
|
|
3067
|
+
subject: msg.subject,
|
|
3068
|
+
responseBytes: context.responseBytes,
|
|
3069
|
+
error: trellisError.toSerializable(),
|
|
3070
|
+
},
|
|
3071
|
+
"RPC error",
|
|
3072
|
+
);
|
|
3073
|
+
|
|
3074
|
+
const errorData = trellisError.toSerializable();
|
|
3075
|
+
const hdrs = natsHeaders();
|
|
3076
|
+
hdrs.set("status", "error");
|
|
3077
|
+
|
|
3078
|
+
const serialized = Result.try(() => JSON.stringify(errorData));
|
|
3079
|
+
if (serialized.isErr()) {
|
|
3080
|
+
this.#log.error(
|
|
3081
|
+
{ error: serialized.error },
|
|
3082
|
+
"Failed to serialize error response",
|
|
3083
|
+
);
|
|
3084
|
+
this.#respondWithPayload(
|
|
3085
|
+
msg,
|
|
3086
|
+
'{"type":"UnexpectedError","message":"Failed to serialize error"}',
|
|
3087
|
+
{ headers: hdrs },
|
|
3088
|
+
{ method: context.method, responseKind: "error" },
|
|
3089
|
+
);
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
this.#respondWithPayload(
|
|
3093
|
+
msg,
|
|
3094
|
+
serialized.take() as string,
|
|
3095
|
+
{ headers: hdrs },
|
|
3096
|
+
{ method: context.method, responseKind: "error" },
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
respondWithError(msg: Msg, error: Error | BaseError): void {
|
|
3101
|
+
this.#respondWithError(msg, error);
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
publish(
|
|
3105
|
+
event: string,
|
|
3106
|
+
data: Record<string, unknown>,
|
|
3107
|
+
): AsyncResult<void, ValidationError | UnexpectedError> {
|
|
3108
|
+
return AsyncResult.from((async () => {
|
|
3109
|
+
try {
|
|
3110
|
+
const eventName = event as EventsOf<TA>;
|
|
3111
|
+
const ctx = this.api["events"][eventName] as EventDescriptorOf<
|
|
3112
|
+
TA,
|
|
3113
|
+
typeof eventName
|
|
3114
|
+
>;
|
|
3115
|
+
if (!ctx) {
|
|
3116
|
+
return err(
|
|
3117
|
+
new UnexpectedError({
|
|
3118
|
+
cause: this.#unknownApiError("event", event.toString()),
|
|
3119
|
+
context: { event: event.toString() },
|
|
3120
|
+
}),
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
const subject = this.template(ctx.subject, data).take();
|
|
3125
|
+
if (isErr(subject)) {
|
|
3126
|
+
logger.error({ err: subject.error }, "Failed to template event.");
|
|
3127
|
+
return subject;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
const header = {
|
|
3131
|
+
id: ulid(),
|
|
3132
|
+
time: new Date().toISOString(),
|
|
3133
|
+
};
|
|
3134
|
+
const payload: Record<string, unknown> = {
|
|
3135
|
+
...data,
|
|
3136
|
+
header,
|
|
3137
|
+
};
|
|
3138
|
+
const msg = encodeSchema(ctx.event, payload).take();
|
|
3139
|
+
if (isErr(msg)) {
|
|
3140
|
+
logger.error({ err: msg.error }, "Failed to encode event.");
|
|
3141
|
+
return err(new UnexpectedError({ cause: msg.error }));
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
const headers = natsHeaders();
|
|
3145
|
+
headers.set("Nats-Msg-Id", header.id);
|
|
3146
|
+
injectTraceContext(createNatsHeaderCarrier(headers));
|
|
3147
|
+
|
|
3148
|
+
logger.trace({ subject }, `Publishing ${event.toString()} event.`);
|
|
3149
|
+
await this.js.publish(subject, msg, { headers });
|
|
3150
|
+
return ok(undefined);
|
|
3151
|
+
} catch (cause) {
|
|
3152
|
+
return err(
|
|
3153
|
+
new UnexpectedError({ cause, context: { event: event.toString() } }),
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
})());
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
event<E extends EventsOf<TA>>(
|
|
3160
|
+
event: E,
|
|
3161
|
+
subjectData: Record<string, unknown>,
|
|
3162
|
+
fn: EventCallback<EventOf<TA, E>>,
|
|
3163
|
+
opts?: EventOpts,
|
|
3164
|
+
): AsyncResult<void, ValidationError | UnexpectedError> {
|
|
3165
|
+
return AsyncResult.from((async () => {
|
|
3166
|
+
try {
|
|
3167
|
+
const eventName = event as EventsOf<TA>;
|
|
3168
|
+
const ctx = this.api["events"][eventName] as EventDescriptorOf<
|
|
3169
|
+
TA,
|
|
3170
|
+
typeof eventName
|
|
3171
|
+
>;
|
|
3172
|
+
if (!ctx) {
|
|
3173
|
+
return err(
|
|
3174
|
+
new UnexpectedError({
|
|
3175
|
+
cause: this.#unknownApiError("event", event.toString()),
|
|
3176
|
+
context: { event: event.toString() },
|
|
3177
|
+
}),
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
const subject = this.template(ctx.subject, subjectData, true).take();
|
|
3181
|
+
if (isErr(subject)) return subject;
|
|
3182
|
+
|
|
3183
|
+
if (opts?.mode === "ephemeral") {
|
|
3184
|
+
return this.#startEphemeralEvent(
|
|
3185
|
+
eventName,
|
|
3186
|
+
ctx,
|
|
3187
|
+
subject,
|
|
3188
|
+
fn,
|
|
3189
|
+
opts.signal,
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
const jsm = await jetstreamManager(this.nats);
|
|
3194
|
+
|
|
3195
|
+
const consumerName = opts?.durableName ??
|
|
3196
|
+
`${this.name}-${String(event).replaceAll(".", "_")}`;
|
|
3197
|
+
const addResult = await AsyncResult.try(() =>
|
|
3198
|
+
jsm.consumers.add(this.stream, {
|
|
3199
|
+
durable_name: consumerName,
|
|
3200
|
+
ack_policy: "explicit",
|
|
3201
|
+
deliver_policy: opts?.replay === "new" ? "new" : "all",
|
|
3202
|
+
filter_subjects: [subject],
|
|
3203
|
+
})
|
|
3204
|
+
);
|
|
3205
|
+
|
|
3206
|
+
const consumerInfoResult = addResult.isOk()
|
|
3207
|
+
? addResult
|
|
3208
|
+
: await AsyncResult.try(() =>
|
|
3209
|
+
jsm.consumers.info(this.stream, consumerName)
|
|
3210
|
+
);
|
|
3211
|
+
|
|
3212
|
+
const info = consumerInfoResult.take();
|
|
3213
|
+
if (isErr(info)) return info;
|
|
3214
|
+
|
|
3215
|
+
const consumer = this.js.consumers.getConsumerFromInfo(info);
|
|
3216
|
+
const messages = await consumer.consume();
|
|
3217
|
+
if (opts?.signal) {
|
|
3218
|
+
if (opts.signal.aborted) {
|
|
3219
|
+
messages.stop();
|
|
3220
|
+
} else {
|
|
3221
|
+
opts.signal.addEventListener("abort", () => messages.stop(), {
|
|
3222
|
+
once: true,
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
this.#tasks.add(
|
|
3228
|
+
`event:${eventName}:${ulid()}`,
|
|
3229
|
+
this.#handleDurableEvent(eventName, ctx, messages, fn),
|
|
3230
|
+
);
|
|
3231
|
+
return ok(undefined);
|
|
3232
|
+
} catch (cause) {
|
|
3233
|
+
return err(
|
|
3234
|
+
new UnexpectedError({ cause, context: { event: event.toString() } }),
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
})());
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
#startEphemeralEvent(
|
|
3241
|
+
event: EventsOf<TA>,
|
|
3242
|
+
ctx: EventDescriptorOf<TA, EventsOf<TA>>,
|
|
3243
|
+
subject: string,
|
|
3244
|
+
fn: EventCallback<EventOf<TA, EventsOf<TA>>>,
|
|
3245
|
+
signal?: AbortSignal,
|
|
3246
|
+
): Result<void, ValidationError | UnexpectedError> {
|
|
3247
|
+
const sub = this.nats.subscribe(subject);
|
|
3248
|
+
if (signal) {
|
|
3249
|
+
if (signal.aborted) {
|
|
3250
|
+
sub.unsubscribe();
|
|
3251
|
+
return ok(undefined);
|
|
3252
|
+
}
|
|
3253
|
+
signal.addEventListener("abort", () => sub.unsubscribe(), { once: true });
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
const task = AsyncResult.try(async () => {
|
|
3257
|
+
for await (const msg of sub) {
|
|
3258
|
+
const parsedEvent = this.#parseEventMessage(event, ctx, msg);
|
|
3259
|
+
const m = parsedEvent.take();
|
|
3260
|
+
if (isErr(m)) {
|
|
3261
|
+
this.#log.error({ error: m.error }, "Event validation failed");
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
const handlerResult = await AsyncResult.lift(
|
|
3266
|
+
fn(m as EventOf<TA, EventsOf<TA>>),
|
|
3267
|
+
);
|
|
3268
|
+
if (handlerResult.isErr()) {
|
|
3269
|
+
this.#log.error(
|
|
3270
|
+
{
|
|
3271
|
+
error: handlerResult.error.toSerializable(),
|
|
3272
|
+
event,
|
|
3273
|
+
subject: msg.subject,
|
|
3274
|
+
},
|
|
3275
|
+
"Event handler failed",
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
this.#tasks.add(`event:${event}:${ulid()}`, task);
|
|
3282
|
+
return ok(undefined);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
#handleDurableEvent(
|
|
3286
|
+
event: EventsOf<TA>,
|
|
3287
|
+
ctx: EventDescriptorOf<TA, EventsOf<TA>>,
|
|
3288
|
+
messages: ConsumerMessages,
|
|
3289
|
+
fn: EventCallback<EventOf<TA, EventsOf<TA>>>,
|
|
3290
|
+
): AsyncResult<void, ValidationError | UnexpectedError> {
|
|
3291
|
+
return AsyncResult.try(async () => {
|
|
3292
|
+
for await (const msg of messages) {
|
|
3293
|
+
const parsedEvent = this.#parseEventMessage(event, ctx, msg);
|
|
3294
|
+
const m = parsedEvent.take();
|
|
3295
|
+
if (isErr(m)) {
|
|
3296
|
+
this.#log.error({ error: m.error }, "Event validation failed");
|
|
3297
|
+
msg.term();
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
const handlerResult = await AsyncResult.lift(
|
|
3302
|
+
fn(m as EventOf<TA, EventsOf<TA>>),
|
|
3303
|
+
);
|
|
3304
|
+
if (handlerResult.isErr()) {
|
|
3305
|
+
this.#log.error(
|
|
3306
|
+
{
|
|
3307
|
+
error: handlerResult.error.toSerializable(),
|
|
3308
|
+
event,
|
|
3309
|
+
subject: msg.subject,
|
|
3310
|
+
},
|
|
3311
|
+
"Event handler failed",
|
|
3312
|
+
);
|
|
3313
|
+
msg.nak();
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
msg.ack();
|
|
3318
|
+
}
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
#parseEventMessage(
|
|
3323
|
+
event: EventsOf<TA>,
|
|
3324
|
+
ctx: EventDescriptorOf<TA, EventsOf<TA>>,
|
|
3325
|
+
msg: Pick<Msg, "json" | "subject">,
|
|
3326
|
+
): Result<unknown, ValidationError | UnexpectedError> {
|
|
3327
|
+
const jsonData = Result.try<JsonValue>(() => msg.json());
|
|
3328
|
+
const json = jsonData.take();
|
|
3329
|
+
if (isErr(json)) {
|
|
3330
|
+
this.#log.error(
|
|
3331
|
+
{ error: json.error, event, subject: msg.subject },
|
|
3332
|
+
"Event parse failed",
|
|
3333
|
+
);
|
|
3334
|
+
return json;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
return parseRuntimeSchema(ctx.event, json);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
wait(): AsyncResult<void, BaseError> {
|
|
3341
|
+
return this.#tasks.wait();
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
// FIXME: If are validating things twice in most cases...
|
|
3345
|
+
template(
|
|
3346
|
+
subject: string,
|
|
3347
|
+
data: unknown,
|
|
3348
|
+
allowWildcards = false,
|
|
3349
|
+
): Result<string, ValidationError> {
|
|
3350
|
+
// Find all template placeholders and check if values exist
|
|
3351
|
+
const placeholders = subject.match(/\{([^}]+)\}/g) || [];
|
|
3352
|
+
for (const placeholder of placeholders) {
|
|
3353
|
+
const key = placeholder.slice(1, -1); // Remove { and }
|
|
3354
|
+
const value = Pointer.Get(data, key);
|
|
3355
|
+
|
|
3356
|
+
if ((value === undefined || value === null) && !allowWildcards) {
|
|
3357
|
+
return err(
|
|
3358
|
+
new ValidationError({
|
|
3359
|
+
errors: [
|
|
3360
|
+
{
|
|
3361
|
+
path: key,
|
|
3362
|
+
message: "Missing required data for subject template",
|
|
3363
|
+
},
|
|
3364
|
+
],
|
|
3365
|
+
context: { key },
|
|
3366
|
+
}),
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
const result = subject.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
3372
|
+
const value = Pointer.Get(data, key);
|
|
3373
|
+
if (allowWildcards && value === "*") {
|
|
3374
|
+
return "*";
|
|
3375
|
+
}
|
|
3376
|
+
if (allowWildcards && (value === undefined || value === null)) {
|
|
3377
|
+
return "*";
|
|
3378
|
+
}
|
|
3379
|
+
return this.#escapeSubjectToken(`${value}`);
|
|
3380
|
+
});
|
|
3381
|
+
|
|
3382
|
+
return ok(result);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
#escapeSubjectToken(token: string): string {
|
|
3386
|
+
const out = token.replace(
|
|
3387
|
+
NATS_SUBJECT_TOKEN_FORBIDDEN,
|
|
3388
|
+
(ch) => `~${ch.codePointAt(0)!.toString(16).toUpperCase()}~`,
|
|
3389
|
+
);
|
|
3390
|
+
|
|
3391
|
+
// Protect stapRet with $ due to NATS internal use of it
|
|
3392
|
+
if (out.length === 0 || out.startsWith("$")) {
|
|
3393
|
+
return `_${out}`;
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
return out;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
#currentIat(): number {
|
|
3400
|
+
return this.auth.currentIat?.() ?? Math.floor(Date.now() / 1000);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
async #createProof(
|
|
3404
|
+
subject: string,
|
|
3405
|
+
payload: string,
|
|
3406
|
+
): Promise<{ proof: string; iat: number; requestId: string }> {
|
|
3407
|
+
const payloadBytes = new TextEncoder().encode(payload);
|
|
3408
|
+
const payloadHash = await sha256(payloadBytes);
|
|
3409
|
+
const iat = this.#currentIat();
|
|
3410
|
+
const requestId = ulid();
|
|
3411
|
+
const input = buildProofInput(
|
|
3412
|
+
this.auth.sessionKey,
|
|
3413
|
+
subject,
|
|
3414
|
+
payloadHash,
|
|
3415
|
+
iat,
|
|
3416
|
+
requestId,
|
|
3417
|
+
);
|
|
3418
|
+
const digest = await sha256(input);
|
|
3419
|
+
const sigBytes = await this.auth.sign(digest);
|
|
3420
|
+
return { proof: base64urlEncode(sigBytes), iat, requestId };
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
async #requestMessageWithRetry(args: {
|
|
3424
|
+
method?: string;
|
|
3425
|
+
subject: string;
|
|
3426
|
+
payload: string;
|
|
3427
|
+
headers: MsgHdrs;
|
|
3428
|
+
timeout: number;
|
|
3429
|
+
callerCapabilities?: readonly string[];
|
|
3430
|
+
}): Promise<Result<Msg, TransportError>> {
|
|
3431
|
+
for (let retry = 0; retry <= this.#noResponderMaxRetries; retry++) {
|
|
3432
|
+
const result = await AsyncResult.try(() =>
|
|
3433
|
+
this.nats.request(args.subject, args.payload, {
|
|
3434
|
+
headers: args.headers,
|
|
3435
|
+
timeout: args.timeout,
|
|
3436
|
+
})
|
|
3437
|
+
);
|
|
3438
|
+
|
|
3439
|
+
if (result.isOk()) {
|
|
3440
|
+
return ok(result.take() as Msg);
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
const cause = result.error.cause;
|
|
3444
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3445
|
+
const isNoResponders = message.includes("no responders");
|
|
3446
|
+
|
|
3447
|
+
if (isNoResponders && retry < this.#noResponderMaxRetries) {
|
|
3448
|
+
this.#log.debug(
|
|
3449
|
+
{ method: args.method, subject: args.subject, retry },
|
|
3450
|
+
"No responders, retrying...",
|
|
3451
|
+
);
|
|
3452
|
+
await new Promise((resolve) =>
|
|
3453
|
+
setTimeout(resolve, this.#noResponderRetryMs * (retry + 1))
|
|
3454
|
+
);
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
this.#log.warn(
|
|
3459
|
+
{ method: args.method, subject: args.subject, error: message },
|
|
3460
|
+
"NATS request failed",
|
|
3461
|
+
);
|
|
3462
|
+
return err(classifyRequestTransportFailure({
|
|
3463
|
+
method: args.method,
|
|
3464
|
+
subject: args.subject,
|
|
3465
|
+
callerCapabilities: args.callerCapabilities,
|
|
3466
|
+
cause,
|
|
3467
|
+
}));
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
return err(
|
|
3471
|
+
requestFailedTransportError({
|
|
3472
|
+
code: "trellis.request.retry_exhausted",
|
|
3473
|
+
message: "Trellis could not complete the request after retrying.",
|
|
3474
|
+
hint:
|
|
3475
|
+
"Retry the request. If it keeps failing, check that the target service is available.",
|
|
3476
|
+
method: args.method,
|
|
3477
|
+
subject: args.subject,
|
|
3478
|
+
context: { retries: this.#noResponderMaxRetries + 1 },
|
|
3479
|
+
}),
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
#requestJson(
|
|
3484
|
+
subject: string,
|
|
3485
|
+
body: JsonValue,
|
|
3486
|
+
): AsyncResult<JsonValue, TransportError | UnexpectedError> {
|
|
3487
|
+
return AsyncResult.from((async () => {
|
|
3488
|
+
const span = startClientSpan(subject, subject);
|
|
3489
|
+
return await withSpanAsync(span, async () => {
|
|
3490
|
+
try {
|
|
3491
|
+
const payload = JSON.stringify(body);
|
|
3492
|
+
const authHeaders = await this.#createProof(subject, payload);
|
|
3493
|
+
|
|
3494
|
+
const headers = natsHeaders();
|
|
3495
|
+
headers.set("session-key", this.auth.sessionKey);
|
|
3496
|
+
headers.set("proof", authHeaders.proof);
|
|
3497
|
+
headers.set("iat", String(authHeaders.iat));
|
|
3498
|
+
headers.set("request-id", authHeaders.requestId);
|
|
3499
|
+
injectTraceContext(createNatsHeaderCarrier(headers), span);
|
|
3500
|
+
|
|
3501
|
+
const response = (await this.#requestMessageWithRetry({
|
|
3502
|
+
subject,
|
|
3503
|
+
payload,
|
|
3504
|
+
headers,
|
|
3505
|
+
timeout: this.timeout,
|
|
3506
|
+
})).take();
|
|
3507
|
+
if (isErr(response)) {
|
|
3508
|
+
span.setStatus({
|
|
3509
|
+
code: SpanStatusCode.ERROR,
|
|
3510
|
+
message: response.error.message,
|
|
3511
|
+
});
|
|
3512
|
+
return response;
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
const json = safeJson(response).take();
|
|
3516
|
+
if (isErr(json)) {
|
|
3517
|
+
const error = createTransportError({
|
|
3518
|
+
code: "trellis.request.invalid_response",
|
|
3519
|
+
message: "Trellis returned an invalid response.",
|
|
3520
|
+
hint:
|
|
3521
|
+
"Retry the request. If it keeps happening, reconnect to Trellis and try again.",
|
|
3522
|
+
cause: json.error.cause,
|
|
3523
|
+
context: { subject },
|
|
3524
|
+
});
|
|
3525
|
+
span.setStatus({
|
|
3526
|
+
code: SpanStatusCode.ERROR,
|
|
3527
|
+
message: error.message,
|
|
3528
|
+
});
|
|
3529
|
+
return err(error);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
3533
|
+
return ok(json);
|
|
3534
|
+
} catch (cause) {
|
|
3535
|
+
const error = new UnexpectedError({ cause });
|
|
3536
|
+
span.setStatus({
|
|
3537
|
+
code: SpanStatusCode.ERROR,
|
|
3538
|
+
message: error.message,
|
|
3539
|
+
});
|
|
3540
|
+
span.recordException(error);
|
|
3541
|
+
return err(error);
|
|
3542
|
+
} finally {
|
|
3543
|
+
span.end();
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
})());
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
#watchJson(
|
|
3550
|
+
subject: string,
|
|
3551
|
+
body: JsonValue,
|
|
3552
|
+
): AsyncResult<
|
|
3553
|
+
AsyncIterable<Result<JsonValue, TransportError | UnexpectedError>>,
|
|
3554
|
+
TransportError | UnexpectedError
|
|
3555
|
+
> {
|
|
3556
|
+
return AsyncResult.from((async () => {
|
|
3557
|
+
const payload = JSON.stringify(body);
|
|
3558
|
+
const authHeaders = await this.#createProof(subject, payload);
|
|
3559
|
+
|
|
3560
|
+
const headers = natsHeaders();
|
|
3561
|
+
headers.set("session-key", this.auth.sessionKey);
|
|
3562
|
+
headers.set("proof", authHeaders.proof);
|
|
3563
|
+
headers.set("iat", String(authHeaders.iat));
|
|
3564
|
+
headers.set("request-id", authHeaders.requestId);
|
|
3565
|
+
|
|
3566
|
+
const inbox = createInbox(`_INBOX.${this.auth.sessionKey.slice(0, 16)}`);
|
|
3567
|
+
const sub = this.nats.subscribe(inbox);
|
|
3568
|
+
|
|
3569
|
+
try {
|
|
3570
|
+
this.nats.publish(subject, payload, {
|
|
3571
|
+
headers,
|
|
3572
|
+
reply: inbox,
|
|
3573
|
+
});
|
|
3574
|
+
await this.nats.flush();
|
|
3575
|
+
} catch (cause) {
|
|
3576
|
+
sub.unsubscribe();
|
|
3577
|
+
return err(createTransportError({
|
|
3578
|
+
code: "trellis.watch.failed",
|
|
3579
|
+
message: "Trellis could not start the operation watch.",
|
|
3580
|
+
hint:
|
|
3581
|
+
"Retry watching the operation. If it keeps failing, reconnect to Trellis and try again.",
|
|
3582
|
+
cause,
|
|
3583
|
+
context: { subject },
|
|
3584
|
+
}));
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
return ok((async function* () {
|
|
3588
|
+
try {
|
|
3589
|
+
for await (const msg of sub) {
|
|
3590
|
+
if (msg.headers?.get("status") === "error") {
|
|
3591
|
+
yield err(createTransportError({
|
|
3592
|
+
code: "trellis.watch.failed",
|
|
3593
|
+
message: "Trellis stopped the operation watch.",
|
|
3594
|
+
hint:
|
|
3595
|
+
"Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
|
|
3596
|
+
context: { subject, frame: msg.string() },
|
|
3597
|
+
}));
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
const json = safeJson(msg).take();
|
|
3602
|
+
if (isErr(json)) {
|
|
3603
|
+
yield err(createTransportError({
|
|
3604
|
+
code: "trellis.watch.invalid_response",
|
|
3605
|
+
message: "Trellis returned an invalid watch update.",
|
|
3606
|
+
hint:
|
|
3607
|
+
"Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
|
|
3608
|
+
cause: json.error.cause,
|
|
3609
|
+
context: { subject },
|
|
3610
|
+
}));
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
yield ok(json);
|
|
3615
|
+
}
|
|
3616
|
+
} finally {
|
|
3617
|
+
sub.unsubscribe();
|
|
3618
|
+
}
|
|
3619
|
+
})());
|
|
3620
|
+
})());
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
protected requestAuthValidate(
|
|
3624
|
+
input: AuthRequestsValidateInput,
|
|
3625
|
+
): AsyncResult<
|
|
3626
|
+
AuthRequestsValidateResponse,
|
|
3627
|
+
AuthError | RemoteError | TransportError | ValidationError | UnexpectedError
|
|
3628
|
+
> {
|
|
3629
|
+
const request = this.request.bind(this) as (
|
|
3630
|
+
method: string,
|
|
3631
|
+
input: unknown,
|
|
3632
|
+
opts?: RequestOpts,
|
|
3633
|
+
) => AsyncResult<
|
|
3634
|
+
unknown,
|
|
3635
|
+
| AuthError
|
|
3636
|
+
| RemoteError
|
|
3637
|
+
| TransportError
|
|
3638
|
+
| ValidationError
|
|
3639
|
+
| UnexpectedError
|
|
3640
|
+
>;
|
|
3641
|
+
return request("Auth.Requests.Validate", input) as AsyncResult<
|
|
3642
|
+
AuthRequestsValidateResponse,
|
|
3643
|
+
| AuthError
|
|
3644
|
+
| RemoteError
|
|
3645
|
+
| TransportError
|
|
3646
|
+
| ValidationError
|
|
3647
|
+
| UnexpectedError
|
|
3648
|
+
>;
|
|
3649
|
+
}
|
|
3650
|
+
}
|