@pikku/core 0.12.78 → 0.12.80
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/CHANGELOG.md +234 -0
- package/dist/dev/hot-reload.js +1 -6
- package/dist/ecosystem.d.ts +27 -0
- package/dist/ecosystem.js +26 -0
- package/dist/errors/error-handler.js +4 -2
- package/dist/function/function-runner.js +20 -42
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/index.d.ts +4 -9
- package/dist/index.js +3 -8
- package/dist/middleware/auth-cookie.d.ts +0 -4
- package/dist/middleware/auth-cookie.js +38 -2
- package/dist/middleware/cors.js +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.js +14 -3
- package/dist/permissions.d.ts +8 -10
- package/dist/permissions.js +0 -11
- package/dist/pikku-state.js +12 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-run-state-service.d.ts +7 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
- package/dist/services/in-memory-ai-run-state-service.js +2 -1
- package/dist/services/in-memory-workflow-service.d.ts +10 -0
- package/dist/services/in-memory-workflow-service.js +25 -0
- package/dist/services/local-content-request-handler.d.ts +21 -0
- package/dist/services/local-content-request-handler.js +72 -53
- package/dist/services/local-content.d.ts +6 -0
- package/dist/services/local-content.js +14 -1
- package/dist/services/workflow-service.d.ts +4 -2
- package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
- package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
- package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
- package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/channel-store-tests.js +72 -0
- package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/credential-service-tests.js +109 -0
- package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/deployment-service-tests.js +21 -0
- package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
- package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/secret-service-tests.js +80 -0
- package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/session-store-tests.js +43 -0
- package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
- package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-service-tests.js +150 -0
- package/dist/testing/service-tests.d.ts +6 -0
- package/dist/testing/service-tests.js +26 -791
- package/dist/types/core.types.d.ts +11 -1
- package/dist/types/state.types.d.ts +1 -1
- package/dist/utils/node-host-resolver.d.ts +12 -0
- package/dist/utils/node-host-resolver.js +16 -0
- package/dist/utils/safe-fetch.d.ts +18 -0
- package/dist/utils/safe-fetch.js +167 -29
- package/dist/wirings/actor-flow/run-conversation.js +1 -6
- package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
- package/dist/wirings/ai-agent/agent-rpc.js +53 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
- package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/channel/channel-common.js +1 -0
- package/dist/wirings/channel/channel-handler.js +3 -5
- package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
- package/dist/wirings/channel/channel-rpc-service.js +0 -8
- package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
- package/dist/wirings/channel/channel-rpc.types.js +8 -0
- package/dist/wirings/channel/channel-runner.d.ts +1 -3
- package/dist/wirings/channel/channel-runner.js +16 -8
- package/dist/wirings/channel/channel.types.d.ts +2 -0
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
- package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
- package/dist/wirings/cli/cli-runner.js +4 -2
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/gateway/gateway-runner.js +41 -5
- package/dist/wirings/http/http-routes.js +2 -0
- package/dist/wirings/http/http-runner.d.ts +0 -10
- package/dist/wirings/http/http-runner.js +6 -13
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
- package/dist/wirings/mcp/mcp-runner.js +0 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +53 -86
- package/dist/wirings/rpc/rpc-types.d.ts +3 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
- package/dist/wirings/trigger/trigger-runner.js +1 -0
- package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
- package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
- package/dist/wirings/workflow/feature.js +7 -4
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
- package/dist/wirings/workflow/graph/graph-runner.js +8 -7
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
- package/dist/wirings/workflow/index.d.ts +7 -2
- package/dist/wirings/workflow/index.js +5 -1
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
- package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
- package/dist/wirings/workflow/workflow-approval.js +114 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
- package/dist/wirings/workflow/workflow-constants.js +35 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
- package/dist/wirings/workflow/workflow-errors.js +112 -0
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
- package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
- package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
- package/dist/wirings/workflow/workflow-recovery.js +101 -0
- package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
- package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
- package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
- package/dist/wirings/workflow/workflow-suspend.js +33 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
- package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
- package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
- package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
- package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
- package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
- package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
- package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
- package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
- package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
- package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
- package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
- package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
- package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
- package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
- package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
- package/knowledge/decisions/internals/index.md +38 -26
- package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
- package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
- package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
- package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
- package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
- package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
- package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
- package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
- package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
- package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
- package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
- package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
- package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
- package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
- package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
- package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
- package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
- package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
- package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
- package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
- package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
- package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
- package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
- package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
- package/knowledge/decisions/security/index.md +7 -0
- package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
- package/knowledge/questions/index.md +2 -1
- package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
- package/package.json +14 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +218 -0
- package/src/api-report.test.ts +69 -0
- package/src/column-form.ts +1 -4
- package/src/crypto-utils.test.ts +15 -2
- package/src/dev/hot-reload.ts +2 -7
- package/src/ecosystem.ts +40 -0
- package/src/errors/error-handler.ts +5 -3
- package/src/errors/error.test.ts +4 -1
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +28 -39
- package/src/function/functions.types.ts +15 -9
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -22
- package/src/middleware/auth-cookie.test.ts +50 -0
- package/src/middleware/auth-cookie.ts +38 -2
- package/src/middleware/cors.ts +2 -1
- package/src/middleware/index.ts +1 -1
- package/src/middleware/remote-auth.test.ts +43 -0
- package/src/middleware/remote-auth.ts +17 -3
- package/src/middleware-runner.ts +4 -2
- package/src/no-any-casts.test.ts +57 -0
- package/src/permissions.test.ts +3 -1
- package/src/permissions.ts +22 -24
- package/src/pikku-state.ts +17 -6
- package/src/public-surface.json +578 -0
- package/src/public-surface.json.README +25 -0
- package/src/public-surface.test.ts +105 -0
- package/src/removed-legacy-exports.test.ts +62 -0
- package/src/schema.test.ts +78 -0
- package/src/schema.ts +36 -1
- package/src/services/ai-run-state-service.ts +7 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +3 -2
- package/src/services/in-memory-workflow-service.ts +25 -0
- package/src/services/index.ts +1 -4
- package/src/services/local-content-request-handler.test.ts +43 -5
- package/src/services/local-content-request-handler.ts +103 -74
- package/src/services/local-content.ts +15 -1
- package/src/services/local-email-service.ts +5 -1
- package/src/services/system-role-guard.test.ts +4 -1
- package/src/services/workflow-service.ts +9 -2
- package/src/side-effects-are-declared.test.ts +84 -0
- package/src/source-files-stay-composable.test.ts +41 -0
- package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
- package/src/testing/service-tests/channel-store-tests.ts +98 -0
- package/src/testing/service-tests/credential-service-tests.ts +143 -0
- package/src/testing/service-tests/deployment-service-tests.ts +33 -0
- package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
- package/src/testing/service-tests/secret-service-tests.ts +105 -0
- package/src/testing/service-tests/session-store-tests.ts +62 -0
- package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
- package/src/testing/service-tests/workflow-service-tests.ts +308 -0
- package/src/testing/service-tests.ts +31 -1111
- package/src/types/core.types.ts +15 -1
- package/src/types/state.types.ts +1 -1
- package/src/utils/node-host-resolver.ts +20 -0
- package/src/utils/safe-fetch.test.ts +143 -1
- package/src/utils/safe-fetch.ts +200 -25
- package/src/wirings/actor-flow/run-conversation.ts +1 -6
- package/src/wirings/ai-agent/agent-rpc.ts +120 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
- package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/channel/channel-common.ts +5 -2
- package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
- package/src/wirings/channel/channel-handler.ts +7 -7
- package/src/wirings/channel/channel-rpc-service.ts +0 -14
- package/src/wirings/channel/channel-rpc.test.ts +25 -4
- package/src/wirings/channel/channel-rpc.types.ts +14 -0
- package/src/wirings/channel/channel-runner.ts +33 -20
- package/src/wirings/channel/channel.types.ts +2 -0
- package/src/wirings/channel/local/local-channel-runner.ts +1 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
- package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +7 -4
- package/src/wirings/cli/cli.types.ts +0 -39
- package/src/wirings/cli/command-parser.test.ts +19 -0
- package/src/wirings/cli/command-parser.ts +17 -1
- package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +62 -22
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.ts +11 -26
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +1 -7
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -16
- package/src/wirings/persona/persona-environments.test.ts +14 -3
- package/src/wirings/persona/persona.test.ts +13 -3
- package/src/wirings/persona/validate-personas.ts +5 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/rpc-runner.ts +58 -136
- package/src/wirings/rpc/rpc-types.ts +7 -0
- package/src/wirings/rpc/wire-addon.ts +3 -1
- package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
- package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
- package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
- package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
- package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
- package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
- package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
- package/src/wirings/workflow/feature.ts +7 -4
- package/src/wirings/workflow/graph/graph-node.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
- package/src/wirings/workflow/graph/graph-runner.ts +13 -16
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
- package/src/wirings/workflow/index.ts +12 -4
- package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
- package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
- package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
- package/src/wirings/workflow/workflow-approval.ts +185 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
- package/src/wirings/workflow/workflow-constants.ts +47 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
- package/src/wirings/workflow/workflow-errors.ts +128 -0
- package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
- package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
- package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
- package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
- package/src/wirings/workflow/workflow-recovery.ts +162 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
- package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/src/wirings-stay-decoupled.test.ts +123 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/internal.ts +0 -10
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
import { dirname, join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const packageRoot = join(srcRoot, '..')
|
|
9
|
+
|
|
10
|
+
const packageJson = JSON.parse(
|
|
11
|
+
readFileSync(join(packageRoot, 'package.json'), 'utf-8')
|
|
12
|
+
)
|
|
13
|
+
const expected: Record<string, string[]> = JSON.parse(
|
|
14
|
+
readFileSync(join(srcRoot, 'public-surface.json'), 'utf-8')
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const subpaths = packageJson.exports as Record<string, string>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every runtime export reachable through an entry point, keyed by subpath.
|
|
21
|
+
*
|
|
22
|
+
* Types are absent: they are erased before this can see them, so a type-only
|
|
23
|
+
* addition is not pinned here.
|
|
24
|
+
*/
|
|
25
|
+
const actualSurface = async () => {
|
|
26
|
+
const surface: Record<string, string[]> = {}
|
|
27
|
+
for (const [subpath, dist] of Object.entries(subpaths)) {
|
|
28
|
+
const src = resolve(packageRoot, dist.replace('./dist/', './src/'))
|
|
29
|
+
const module = await import(pathToFileURL(src).href)
|
|
30
|
+
surface[subpath] = Object.keys(module).sort()
|
|
31
|
+
}
|
|
32
|
+
return surface
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const REGENERATE =
|
|
36
|
+
'Regenerate with the snippet in public-surface.json.README, and make sure ' +
|
|
37
|
+
'every added name is one @pikku/core means to publish.'
|
|
38
|
+
|
|
39
|
+
describe('the published surface is the surface we meant to publish', () => {
|
|
40
|
+
test('every export subpath resolves to a source file', () => {
|
|
41
|
+
const missing = Object.entries(subpaths).filter(([, dist]) => {
|
|
42
|
+
try {
|
|
43
|
+
readFileSync(
|
|
44
|
+
resolve(packageRoot, dist.replace('./dist/', './src/')).replace(
|
|
45
|
+
/\.js$/,
|
|
46
|
+
'.ts'
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
return false
|
|
50
|
+
} catch {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
missing.map(([subpath]) => subpath),
|
|
57
|
+
[],
|
|
58
|
+
'package.json exports point at files that do not exist in src'
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('no entry point gained or lost a runtime export', async () => {
|
|
63
|
+
const actual = await actualSurface()
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
Object.keys(actual).sort(),
|
|
67
|
+
Object.keys(expected).sort(),
|
|
68
|
+
`the set of export subpaths changed. ${REGENERATE}`
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for (const subpath of Object.keys(expected)) {
|
|
72
|
+
const added = actual[subpath]!.filter(
|
|
73
|
+
(name) => !expected[subpath]!.includes(name)
|
|
74
|
+
)
|
|
75
|
+
const removed = expected[subpath]!.filter(
|
|
76
|
+
(name) => !actual[subpath]!.includes(name)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
{ added, removed },
|
|
81
|
+
{ added: [], removed: [] },
|
|
82
|
+
`'${subpath}' changed its public exports.\n` +
|
|
83
|
+
(added.length ? ` added: ${added.join(', ')}\n` : '') +
|
|
84
|
+
(removed.length ? ` removed: ${removed.join(', ')}\n` : '') +
|
|
85
|
+
`${REGENERATE}`
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('the wildcard re-exports have not widened past their barrels', async () => {
|
|
91
|
+
// `errors/index.ts` and `dev/hot-reload.ts` re-export with `export *`, so a
|
|
92
|
+
// new export in the wrapped module is published without anyone editing an
|
|
93
|
+
// entry point. This pins the two that are reachable publicly.
|
|
94
|
+
const actual = await actualSurface()
|
|
95
|
+
|
|
96
|
+
assert.equal(
|
|
97
|
+
actual['./errors']!.every(
|
|
98
|
+
(name) => name.endsWith('Error') || name.startsWith('addError')
|
|
99
|
+
),
|
|
100
|
+
true,
|
|
101
|
+
`'@pikku/core/errors' exports something that is not an error or an error ` +
|
|
102
|
+
`registrar: ${actual['./errors']!.filter((n) => !n.endsWith('Error') && !n.startsWith('addError')).join(', ')}`
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { join, dirname, relative } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const srcRoot = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const thisFile = fileURLToPath(import.meta.url)
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Names removed from the public surface, each with the entry point that must
|
|
12
|
+
* no longer carry it.
|
|
13
|
+
*
|
|
14
|
+
* They survived as throwing stubs only so the pinned bootstrap CLI could
|
|
15
|
+
* resolve the import; `@pikku/cli@0.12.96` emits none of them, so the stubs
|
|
16
|
+
* went too.
|
|
17
|
+
*/
|
|
18
|
+
const REMOVED = [
|
|
19
|
+
{ name: 'addTagPermission', entry: './middleware/index.js' },
|
|
20
|
+
{ name: 'addHTTPPermission', entry: './wirings/http/index.js' },
|
|
21
|
+
{ name: 'ZodLike', entry: './index.js' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const collectSourceFiles = (directory: string): string[] =>
|
|
25
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
26
|
+
const entryPath = join(directory, entry.name)
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
|
|
29
|
+
}
|
|
30
|
+
return /\.ts$/.test(entry.name) ? [entryPath] : []
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('removed legacy exports stay removed', () => {
|
|
34
|
+
for (const { name, entry } of REMOVED) {
|
|
35
|
+
test(`${entry} no longer exports ${name}`, async () => {
|
|
36
|
+
const module = await import(entry)
|
|
37
|
+
assert.equal(
|
|
38
|
+
name in module,
|
|
39
|
+
false,
|
|
40
|
+
`${entry} exports ${name} again; it was removed in #972`
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('no source file mentions the removed permission wrappers', () => {
|
|
46
|
+
const offenders = collectSourceFiles(srcRoot)
|
|
47
|
+
.filter(
|
|
48
|
+
(file) =>
|
|
49
|
+
file !== thisFile &&
|
|
50
|
+
/\baddTagPermission\b|\baddHTTPPermission\b|\bZodLike\b/.test(
|
|
51
|
+
readFileSync(file, 'utf-8')
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
.map((file) => relative(srcRoot, file))
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(
|
|
57
|
+
offenders,
|
|
58
|
+
[],
|
|
59
|
+
`removed names still referenced in:\n${offenders.join('\n')}`
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
})
|
package/src/schema.test.ts
CHANGED
|
@@ -265,6 +265,84 @@ describe('Schema', () => {
|
|
|
265
265
|
await validateSchema(logger, schemaService, null, {})
|
|
266
266
|
})
|
|
267
267
|
|
|
268
|
+
test('strips undefined-valued properties before validating', async () => {
|
|
269
|
+
addSchema('undefinedProps', {
|
|
270
|
+
properties: { name: { type: 'string' }, count: { type: 'number' } },
|
|
271
|
+
})
|
|
272
|
+
const logger = {
|
|
273
|
+
info: () => {},
|
|
274
|
+
warn: () => {},
|
|
275
|
+
error: () => {},
|
|
276
|
+
debug: () => {},
|
|
277
|
+
} as any
|
|
278
|
+
let seen: any
|
|
279
|
+
const schemaService = {
|
|
280
|
+
compileSchema: async () => {},
|
|
281
|
+
validateSchema: async (_name: string, data: any) => {
|
|
282
|
+
seen = data
|
|
283
|
+
},
|
|
284
|
+
getSchemaNames: () => new Set<string>(),
|
|
285
|
+
}
|
|
286
|
+
await validateSchema(logger, schemaService, 'undefinedProps', {
|
|
287
|
+
name: 'ada',
|
|
288
|
+
count: undefined,
|
|
289
|
+
})
|
|
290
|
+
assert.deepStrictEqual(seen, { name: 'ada' })
|
|
291
|
+
assert.ok(!('count' in seen))
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('strips undefined nested in objects and arrays', async () => {
|
|
295
|
+
addSchema('nestedUndefined', { properties: {} })
|
|
296
|
+
const logger = {
|
|
297
|
+
info: () => {},
|
|
298
|
+
warn: () => {},
|
|
299
|
+
error: () => {},
|
|
300
|
+
debug: () => {},
|
|
301
|
+
} as any
|
|
302
|
+
let seen: any
|
|
303
|
+
const schemaService = {
|
|
304
|
+
compileSchema: async () => {},
|
|
305
|
+
validateSchema: async (_name: string, data: any) => {
|
|
306
|
+
seen = data
|
|
307
|
+
},
|
|
308
|
+
getSchemaNames: () => new Set<string>(),
|
|
309
|
+
}
|
|
310
|
+
await validateSchema(logger, schemaService, 'nestedUndefined', {
|
|
311
|
+
nested: { keep: 1, drop: undefined },
|
|
312
|
+
list: [{ keep: 2, drop: undefined }],
|
|
313
|
+
})
|
|
314
|
+
assert.deepStrictEqual(seen, {
|
|
315
|
+
nested: { keep: 1 },
|
|
316
|
+
list: [{ keep: 2 }],
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('leaves Date instances intact while stripping', async () => {
|
|
321
|
+
addSchema('withDate', { properties: {} })
|
|
322
|
+
const logger = {
|
|
323
|
+
info: () => {},
|
|
324
|
+
warn: () => {},
|
|
325
|
+
error: () => {},
|
|
326
|
+
debug: () => {},
|
|
327
|
+
} as any
|
|
328
|
+
const when = new Date('2020-01-01T00:00:00.000Z')
|
|
329
|
+
let seen: any
|
|
330
|
+
const schemaService = {
|
|
331
|
+
compileSchema: async () => {},
|
|
332
|
+
validateSchema: async (_name: string, data: any) => {
|
|
333
|
+
seen = data
|
|
334
|
+
},
|
|
335
|
+
getSchemaNames: () => new Set<string>(),
|
|
336
|
+
}
|
|
337
|
+
await validateSchema(logger, schemaService, 'withDate', {
|
|
338
|
+
when,
|
|
339
|
+
drop: undefined,
|
|
340
|
+
})
|
|
341
|
+
assert.ok(seen.when instanceof Date)
|
|
342
|
+
assert.equal(seen.when.getTime(), when.getTime())
|
|
343
|
+
assert.ok(!('drop' in seen))
|
|
344
|
+
})
|
|
345
|
+
|
|
268
346
|
test('should throw MissingSchemaError when schema not found', async () => {
|
|
269
347
|
const logger = {
|
|
270
348
|
info: () => {},
|
package/src/schema.ts
CHANGED
|
@@ -135,6 +135,41 @@ export const coerceTopLevelDataFromSchema = (
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Drops object properties whose value is literally `undefined`.
|
|
140
|
+
*
|
|
141
|
+
* JSON Schema has no way to describe such a property, and the validator refuses
|
|
142
|
+
* the whole instance rather than reporting it — `{ a: undefined }` throws
|
|
143
|
+
* "Instances of \"undefined\" type are not supported" instead of validating.
|
|
144
|
+
*
|
|
145
|
+
* That matters because whether a payload can contain one depends on how the
|
|
146
|
+
* call travelled. Over HTTP `JSON.stringify` drops these keys before the
|
|
147
|
+
* request is built, so they never arrive. Dispatched in-process — an inline
|
|
148
|
+
* workflow step calling an RPC — the object is handed over as-is, and
|
|
149
|
+
* `{ retries: data.maybeRetries }` with the field omitted by the caller is
|
|
150
|
+
* enough to kill the step. Normalising here makes the two paths agree, and an
|
|
151
|
+
* explicitly-undefined *required* field now reports as the missing property it
|
|
152
|
+
* is rather than as an internal error.
|
|
153
|
+
*
|
|
154
|
+
* Deliberately not a JSON round-trip: `coerceTopLevelDataFromSchema` puts real
|
|
155
|
+
* `Date` instances on the data first, and stringifying would flatten them back
|
|
156
|
+
* to strings the validator has never been given.
|
|
157
|
+
*/
|
|
158
|
+
const stripUndefinedValues = <T>(value: T): T => {
|
|
159
|
+
if (Array.isArray(value)) return value.map(stripUndefinedValues) as T
|
|
160
|
+
if (value === null || typeof value !== 'object') return value
|
|
161
|
+
// Only plain objects: a Date, Map or class instance is data, not a bag of
|
|
162
|
+
// properties to be rewritten.
|
|
163
|
+
const prototype = Object.getPrototypeOf(value)
|
|
164
|
+
if (prototype !== Object.prototype && prototype !== null) return value
|
|
165
|
+
|
|
166
|
+
return Object.fromEntries(
|
|
167
|
+
Object.entries(value)
|
|
168
|
+
.filter(([, entry]) => entry !== undefined)
|
|
169
|
+
.map(([key, entry]) => [key, stripUndefinedValues(entry)])
|
|
170
|
+
) as T
|
|
171
|
+
}
|
|
172
|
+
|
|
138
173
|
export const validateSchema = async (
|
|
139
174
|
logger: Logger,
|
|
140
175
|
schemaService: SchemaService | undefined,
|
|
@@ -164,6 +199,6 @@ export const validateSchema = async (
|
|
|
164
199
|
)
|
|
165
200
|
}
|
|
166
201
|
await schemaService.compileSchema(key, schema)
|
|
167
|
-
await schemaService.validateSchema(key, data ?? {})
|
|
202
|
+
await schemaService.validateSchema(key, stripUndefinedValues(data ?? {}))
|
|
168
203
|
}
|
|
169
204
|
}
|
|
@@ -10,10 +10,16 @@ export interface AIRunStateService {
|
|
|
10
10
|
updateRun(runId: string, updates: Partial<AgentRunState>): Promise<void>
|
|
11
11
|
getRun(runId: string): Promise<AgentRunState | null>
|
|
12
12
|
getRunsByThread(threadId: string): Promise<AgentRunState[]>
|
|
13
|
+
/**
|
|
14
|
+
* Claims an approval: removes it from the run's pending list and records the
|
|
15
|
+
* decision, as one atomic step. Returns whether THIS caller made the claim —
|
|
16
|
+
* concurrent approvals of the same tool call produce exactly one `true`, and
|
|
17
|
+
* only that caller may run the tool.
|
|
18
|
+
*/
|
|
13
19
|
resolveApproval(
|
|
14
20
|
toolCallId: string,
|
|
15
21
|
status: 'approved' | 'denied'
|
|
16
|
-
): Promise<
|
|
22
|
+
): Promise<boolean>
|
|
17
23
|
findRunByToolCallId(
|
|
18
24
|
toolCallId: string
|
|
19
25
|
): Promise<{ run: AgentRunState; approval: PendingApproval } | null>
|
|
@@ -150,7 +150,7 @@ class DisabledInvocationAudit implements AuditLog {
|
|
|
150
150
|
if (!this.warned) {
|
|
151
151
|
this.warned = true
|
|
152
152
|
// knowledge: decisions/security/a-dropped-audit-write-is-always-logged.md
|
|
153
|
-
const logger =
|
|
153
|
+
const logger = this.wire.logger ?? this.logger
|
|
154
154
|
logger?.warn?.(
|
|
155
155
|
`audit.write() dropped for '${this.wire.functionId ?? 'unknown function'}' — the function has no audit config (set audit: true on it)`
|
|
156
156
|
)
|
|
@@ -207,7 +207,7 @@ class InvocationAuditLog implements AuditLog {
|
|
|
207
207
|
await this.service.audit(event)
|
|
208
208
|
}
|
|
209
209
|
} catch (error) {
|
|
210
|
-
const logger =
|
|
210
|
+
const logger = this.wire.logger ?? this.logger
|
|
211
211
|
logger?.warn?.('best-effort audit flush failed', error)
|
|
212
212
|
}
|
|
213
213
|
}
|
|
@@ -38,7 +38,7 @@ export class InMemoryAIRunStateService implements AIRunStateService {
|
|
|
38
38
|
async resolveApproval(
|
|
39
39
|
toolCallId: string,
|
|
40
40
|
status: 'approved' | 'denied'
|
|
41
|
-
): Promise<
|
|
41
|
+
): Promise<boolean> {
|
|
42
42
|
for (const run of this.runs.values()) {
|
|
43
43
|
if (run.pendingApprovals) {
|
|
44
44
|
const approval = run.pendingApprovals.find(
|
|
@@ -49,10 +49,11 @@ export class InMemoryAIRunStateService implements AIRunStateService {
|
|
|
49
49
|
(a) => a.toolCallId !== toolCallId
|
|
50
50
|
)
|
|
51
51
|
run.updatedAt = new Date()
|
|
52
|
-
return
|
|
52
|
+
return true
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
return false
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
async findRunByToolCallId(
|
|
@@ -142,6 +142,7 @@ export class InMemoryWorkflowService
|
|
|
142
142
|
const step: StepState & { stepName: string } = {
|
|
143
143
|
stepId,
|
|
144
144
|
status: 'pending',
|
|
145
|
+
rpcName,
|
|
145
146
|
attemptCount: 1,
|
|
146
147
|
retries: stepOptions?.retries,
|
|
147
148
|
retryDelay: stepOptions?.retryDelay,
|
|
@@ -273,6 +274,7 @@ export class InMemoryWorkflowService
|
|
|
273
274
|
const newStep: StepState & { stepName: string } = {
|
|
274
275
|
stepId: newStepId,
|
|
275
276
|
status,
|
|
277
|
+
rpcName: failedStep.rpcName,
|
|
276
278
|
attemptCount: failedStep.attemptCount + 1,
|
|
277
279
|
retries: failedStep.retries,
|
|
278
280
|
retryDelay: failedStep.retryDelay,
|
|
@@ -301,6 +303,29 @@ export class InMemoryWorkflowService
|
|
|
301
303
|
return newStep
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Safe to opt in despite the pass-through `withStepLock`: this service wires
|
|
308
|
+
* no queues and runs every step inline in one process, so the relay's
|
|
309
|
+
* redundant dispatch has no second holder to race with. See
|
|
310
|
+
* knowledge: decisions/internals/the-in-memory-workflow-service-is-inline-only-and-single-process.md
|
|
311
|
+
*/
|
|
312
|
+
protected async findUndispatchedSteps(
|
|
313
|
+
before: Date,
|
|
314
|
+
limit: number
|
|
315
|
+
): Promise<Array<{ runId: string; stepId: string }>> {
|
|
316
|
+
const undispatched: Array<{ runId: string; stepId: string }> = []
|
|
317
|
+
for (const [runId, run] of this.runs) {
|
|
318
|
+
if (run.status !== 'running') continue
|
|
319
|
+
for (const step of this.stepHistory.get(runId) ?? []) {
|
|
320
|
+
if (step.status !== 'pending') continue
|
|
321
|
+
if (step.updatedAt >= before) continue
|
|
322
|
+
undispatched.push({ runId, stepId: step.stepId })
|
|
323
|
+
if (undispatched.length >= limit) return undispatched
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return undispatched
|
|
327
|
+
}
|
|
328
|
+
|
|
304
329
|
protected async findStalledRunIds(
|
|
305
330
|
before: Date,
|
|
306
331
|
limit: number
|
package/src/services/index.ts
CHANGED
|
@@ -45,10 +45,7 @@ export type {
|
|
|
45
45
|
ScenarioRpcMap,
|
|
46
46
|
ScenarioHttpResponse,
|
|
47
47
|
} from './personas-service.js'
|
|
48
|
-
//
|
|
49
|
-
// `readScenarioHttpResponse`, `postScenarioJson` — is exported from
|
|
50
|
-
// `@pikku/core/persona`, not from here. Types are free; those are values, and
|
|
51
|
-
// they reach the actor-flow and agent runners, which no production server runs.
|
|
48
|
+
// knowledge: decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md
|
|
52
49
|
export type { HttpPersonasConfig } from './http-personas.js'
|
|
53
50
|
export type { JWTService } from './jwt-service.js'
|
|
54
51
|
export type {
|
|
@@ -81,11 +81,17 @@ describe('createLocalContentRequestHandler', () => {
|
|
|
81
81
|
test('refuses an upload whose key escapes the content directory', async () => {
|
|
82
82
|
writeFileSync(join(tmpDir, '..', 'pikku-handler-secret.txt'), 'secret')
|
|
83
83
|
try {
|
|
84
|
+
// Signed for the escaping path itself, so it clears the signature gate
|
|
85
|
+
// and the path-escape guard is what rejects it.
|
|
86
|
+
const signed = await content.signURL({
|
|
87
|
+
url: '/upload/bucket/..%2f..%2fpikku-handler-secret.txt',
|
|
88
|
+
dateLessThan: new Date(Date.now() + 60_000),
|
|
89
|
+
} as any)
|
|
84
90
|
const response = await handler(
|
|
85
|
-
new Request(
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
)
|
|
91
|
+
new Request(`http://localhost${signed}`, {
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
body: 'overwritten',
|
|
94
|
+
})
|
|
89
95
|
)
|
|
90
96
|
assert.equal(response?.status, 400)
|
|
91
97
|
assert.equal(
|
|
@@ -104,6 +110,33 @@ describe('createLocalContentRequestHandler', () => {
|
|
|
104
110
|
assert.equal(response?.status, 400)
|
|
105
111
|
})
|
|
106
112
|
|
|
113
|
+
test('refuses an unsigned PUT to the upload prefix', async () => {
|
|
114
|
+
const put = await handler(
|
|
115
|
+
new Request('http://localhost/upload/bucket/evil.bin', {
|
|
116
|
+
method: 'PUT',
|
|
117
|
+
body: 'attacker-controlled',
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
assert.equal(put?.status, 403, 'an upload URL must be signed like a read')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('refuses a PUT whose signature was minted for a different key', async () => {
|
|
124
|
+
const { uploadUrl } = await content.getUploadURL({
|
|
125
|
+
bucket: 'bucket',
|
|
126
|
+
fileKey: 'legit/one.bin',
|
|
127
|
+
contentType: 'application/octet-stream',
|
|
128
|
+
} as any)
|
|
129
|
+
const query = uploadUrl.slice(uploadUrl.indexOf('?'))
|
|
130
|
+
// Same signature, a different target path.
|
|
131
|
+
const put = await handler(
|
|
132
|
+
new Request(`http://localhost/upload/bucket/other.bin${query}`, {
|
|
133
|
+
method: 'PUT',
|
|
134
|
+
body: 'payload',
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
assert.equal(put?.status, 403)
|
|
138
|
+
})
|
|
139
|
+
|
|
107
140
|
test('round-trips an upload and a signed read', async () => {
|
|
108
141
|
const { uploadUrl } = await content.getUploadURL({
|
|
109
142
|
bucket: 'bucket',
|
|
@@ -188,8 +221,13 @@ describe('createLocalContentRequestHandler', () => {
|
|
|
188
221
|
})
|
|
189
222
|
|
|
190
223
|
test('rejects a body over the size limit without writing it', async () => {
|
|
224
|
+
const { uploadUrl } = await content.getUploadURL({
|
|
225
|
+
bucket: 'bucket',
|
|
226
|
+
fileKey: 'big.bin',
|
|
227
|
+
contentType: 'application/octet-stream',
|
|
228
|
+
} as any)
|
|
191
229
|
const response = await handler(
|
|
192
|
-
new Request(
|
|
230
|
+
new Request(`http://localhost${uploadUrl}`, {
|
|
193
231
|
method: 'PUT',
|
|
194
232
|
body: Buffer.alloc(1024 * 1024 + 1),
|
|
195
233
|
})
|