@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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
combineChannelMiddleware,
|
|
5
5
|
wrapChannelWithMiddleware,
|
|
6
6
|
} from '../wirings/channel/channel-middleware-runner.js'
|
|
7
|
-
import { runPermissions } from '../permissions.js'
|
|
7
|
+
import { runPermissions, type PermissionWire } from '../permissions.js'
|
|
8
8
|
import { withoutSecrets } from '../services/secretless.js'
|
|
9
9
|
import { pikkuState } from '../pikku-state.js'
|
|
10
10
|
import {
|
|
@@ -248,6 +248,28 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
248
248
|
invocationWire,
|
|
249
249
|
'rpc'
|
|
250
250
|
)
|
|
251
|
+
|
|
252
|
+
// knowledge: decisions/internals/core-function-runner-restores-the-wire-fields-it-overwrites.md
|
|
253
|
+
const restoreInvocationWire = () => {
|
|
254
|
+
if (previousRpcDescriptor) {
|
|
255
|
+
Object.defineProperty(invocationWire, 'rpc', previousRpcDescriptor)
|
|
256
|
+
}
|
|
257
|
+
restoreField('functionId', previousFunctionId)
|
|
258
|
+
restoreField('audit', previousAudit)
|
|
259
|
+
restoreField('addonNamespace', previousAddonNamespace)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function restoreField<K extends 'functionId' | 'audit' | 'addonNamespace'>(
|
|
263
|
+
key: K,
|
|
264
|
+
previous: PikkuWire[K]
|
|
265
|
+
) {
|
|
266
|
+
if (previous === undefined) {
|
|
267
|
+
delete invocationWire[key]
|
|
268
|
+
} else {
|
|
269
|
+
invocationWire[key] = previous
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
251
273
|
invocationWire.functionId = resolvedFunctionId
|
|
252
274
|
invocationWire.audit = resolvedAuditConfig
|
|
253
275
|
// Track which addon instance is executing so intra-addon sibling calls
|
|
@@ -294,7 +316,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
294
316
|
}
|
|
295
317
|
}
|
|
296
318
|
|
|
297
|
-
if (
|
|
319
|
+
if (session?.readonly && !funcMeta.readonly) {
|
|
298
320
|
throw new ReadonlySessionError()
|
|
299
321
|
}
|
|
300
322
|
|
|
@@ -347,7 +369,8 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
347
369
|
resolvedSingletonServices,
|
|
348
370
|
'a permission'
|
|
349
371
|
) as CoreSingletonServices),
|
|
350
|
-
|
|
372
|
+
// knowledge: decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md
|
|
373
|
+
wire: invocationWire as PermissionWire,
|
|
351
374
|
data: actualData,
|
|
352
375
|
packageName,
|
|
353
376
|
})
|
|
@@ -435,47 +458,13 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
435
458
|
executeFunction
|
|
436
459
|
)) as Out
|
|
437
460
|
} finally {
|
|
438
|
-
|
|
439
|
-
Object.defineProperty(invocationWire, 'rpc', previousRpcDescriptor)
|
|
440
|
-
}
|
|
441
|
-
if (previousFunctionId === undefined) {
|
|
442
|
-
delete (invocationWire as any).functionId
|
|
443
|
-
} else {
|
|
444
|
-
invocationWire.functionId = previousFunctionId
|
|
445
|
-
}
|
|
446
|
-
if (previousAudit === undefined) {
|
|
447
|
-
delete (invocationWire as any).audit
|
|
448
|
-
} else {
|
|
449
|
-
invocationWire.audit = previousAudit
|
|
450
|
-
}
|
|
451
|
-
if (previousAddonNamespace === undefined) {
|
|
452
|
-
delete (invocationWire as any).addonNamespace
|
|
453
|
-
} else {
|
|
454
|
-
invocationWire.addonNamespace = previousAddonNamespace
|
|
455
|
-
}
|
|
461
|
+
restoreInvocationWire()
|
|
456
462
|
}
|
|
457
463
|
}
|
|
458
464
|
|
|
459
465
|
try {
|
|
460
466
|
return (await executeFunction()) as Out
|
|
461
467
|
} finally {
|
|
462
|
-
|
|
463
|
-
Object.defineProperty(invocationWire, 'rpc', previousRpcDescriptor)
|
|
464
|
-
}
|
|
465
|
-
if (previousFunctionId === undefined) {
|
|
466
|
-
delete (invocationWire as any).functionId
|
|
467
|
-
} else {
|
|
468
|
-
invocationWire.functionId = previousFunctionId
|
|
469
|
-
}
|
|
470
|
-
if (previousAudit === undefined) {
|
|
471
|
-
delete (invocationWire as any).audit
|
|
472
|
-
} else {
|
|
473
|
-
invocationWire.audit = previousAudit
|
|
474
|
-
}
|
|
475
|
-
if (previousAddonNamespace === undefined) {
|
|
476
|
-
delete (invocationWire as any).addonNamespace
|
|
477
|
-
} else {
|
|
478
|
-
invocationWire.addonNamespace = previousAddonNamespace
|
|
479
|
-
}
|
|
468
|
+
restoreInvocationWire()
|
|
480
469
|
}
|
|
481
470
|
}
|
|
@@ -14,12 +14,6 @@ import type { CoreNodeConfig } from '../wirings/node/node.types.js'
|
|
|
14
14
|
import type { ScenarioSurface } from '../wirings/workflow/scenario-step.types.js'
|
|
15
15
|
import type { Safe } from '../secret-value.js'
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated Use StandardSchemaV1 from @standard-schema/spec instead.
|
|
19
|
-
* This alias exists only for backward compatibility with generated code.
|
|
20
|
-
*/
|
|
21
|
-
export type ZodLike<T = any> = StandardSchemaV1<T, T>
|
|
22
|
-
|
|
23
17
|
export type CorePikkuFunction<
|
|
24
18
|
In,
|
|
25
19
|
Out,
|
|
@@ -141,6 +135,14 @@ export type CorePikkuAuthConfig<
|
|
|
141
135
|
description?: string
|
|
142
136
|
}
|
|
143
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Marks a permission produced by `pikkuAuth`, so agent tool filtering can tell a
|
|
140
|
+
* session check apart from an ordinary permission.
|
|
141
|
+
*
|
|
142
|
+
* knowledge: decisions/security/permission-auth-filtering-requires-live-permission-functions.md
|
|
143
|
+
*/
|
|
144
|
+
export type AuthBranded = { __pikkuAuth?: true }
|
|
145
|
+
|
|
144
146
|
export const pikkuAuth = <
|
|
145
147
|
Services extends CoreSecretlessSingletonServices =
|
|
146
148
|
SecretlessServices<CoreServices>,
|
|
@@ -151,13 +153,17 @@ export const pikkuAuth = <
|
|
|
151
153
|
| CorePikkuAuthConfig<Services, Session>
|
|
152
154
|
): CorePikkuPermission<any, Services, any> => {
|
|
153
155
|
const fn = typeof auth === 'function' ? auth : auth.func
|
|
154
|
-
const wrapper
|
|
156
|
+
const wrapper: CorePikkuPermission<any, Services, any> & AuthBranded = async (
|
|
157
|
+
services: Services,
|
|
158
|
+
_data: any,
|
|
159
|
+
wire: any
|
|
160
|
+
) => {
|
|
155
161
|
const session = wire.session
|
|
156
162
|
if (!session) return false
|
|
157
163
|
return fn(services, session as Session)
|
|
158
164
|
}
|
|
159
|
-
|
|
160
|
-
return wrapper
|
|
165
|
+
wrapper.__pikkuAuth = true
|
|
166
|
+
return wrapper
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
export type CorePermissionGroup<PikkuPermission = CorePikkuPermission<any>> =
|
package/src/handle-error.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const handleHTTPError = (
|
|
|
33
33
|
e.message !== 'An error occurred'
|
|
34
34
|
? e.message
|
|
35
35
|
: errorResponse.message,
|
|
36
|
-
payload: clientFacing ? (e as
|
|
36
|
+
payload: clientFacing ? (e as { payload?: unknown }).payload : undefined,
|
|
37
37
|
errorId: traceId,
|
|
38
38
|
})
|
|
39
39
|
|
package/src/index.ts
CHANGED
|
@@ -63,7 +63,6 @@ export type {
|
|
|
63
63
|
CorePikkuPermissionFactory,
|
|
64
64
|
CorePikkuApprovalDescription,
|
|
65
65
|
CorePermissionGroup,
|
|
66
|
-
ZodLike,
|
|
67
66
|
} from './function/functions.types.js'
|
|
68
67
|
export {
|
|
69
68
|
pikkuAuth,
|
|
@@ -71,7 +70,7 @@ export {
|
|
|
71
70
|
pikkuPermissionFactory,
|
|
72
71
|
pikkuApprovalDescription,
|
|
73
72
|
} from './function/functions.types.js'
|
|
74
|
-
export {
|
|
73
|
+
export { getAllFunctionNames } from './function/index.js'
|
|
75
74
|
export type {
|
|
76
75
|
ListInput,
|
|
77
76
|
ListOutput,
|
|
@@ -79,6 +78,7 @@ export type {
|
|
|
79
78
|
LeafFilter,
|
|
80
79
|
LeafValue,
|
|
81
80
|
} from './function/list.types.js'
|
|
81
|
+
export { pikkuCLIRender } from './wirings/cli/cli-runner.js'
|
|
82
82
|
export { PikkuRequest } from './pikku-request.js'
|
|
83
83
|
export {
|
|
84
84
|
getRelativeTimeOffsetFromNow,
|
|
@@ -90,7 +90,6 @@ export {
|
|
|
90
90
|
isVersionedId,
|
|
91
91
|
parseVersionedId,
|
|
92
92
|
} from './version.js'
|
|
93
|
-
export { runPikkuFunc } from './function/function-runner.js'
|
|
94
93
|
export {
|
|
95
94
|
AbandonedError,
|
|
96
95
|
beginChanges,
|
|
@@ -98,20 +97,12 @@ export {
|
|
|
98
97
|
runInAbortScope,
|
|
99
98
|
type AbortScope,
|
|
100
99
|
} from './function/abort-scope.js'
|
|
101
|
-
export { runCLICommand, pikkuCLIRender } from './wirings/cli/cli-runner.js'
|
|
102
100
|
export { fetch } from './wirings/http/http-runner.js'
|
|
103
|
-
export {
|
|
104
|
-
runMCPTool,
|
|
105
|
-
runMCPResource,
|
|
106
|
-
runMCPPrompt,
|
|
107
|
-
} from './wirings/mcp/mcp-runner.js'
|
|
108
101
|
export type {
|
|
109
102
|
MCPToolResponse,
|
|
110
103
|
MCPResourceResponse,
|
|
111
104
|
MCPPromptResponse,
|
|
112
105
|
} from './wirings/mcp/mcp.types.js'
|
|
113
|
-
export { runQueueJob } from './wirings/queue/queue-runner.js'
|
|
114
|
-
export { runScheduledTask } from './wirings/scheduler/scheduler-runner.js'
|
|
115
106
|
export {
|
|
116
107
|
AIProviderAuthError,
|
|
117
108
|
AIProviderNotConfiguredError,
|
|
@@ -254,12 +245,7 @@ export type {
|
|
|
254
245
|
RemoteAddonAuth,
|
|
255
246
|
} from './wirings/rpc/wire-remote-addon.js'
|
|
256
247
|
export type { PikkuPackageState } from './types/state.types.js'
|
|
257
|
-
export {
|
|
258
|
-
runMiddleware,
|
|
259
|
-
addTagMiddleware,
|
|
260
|
-
addTagMiddleware as addMiddleware,
|
|
261
|
-
addGlobalMiddleware,
|
|
262
|
-
} from './middleware-runner.js'
|
|
248
|
+
export { runMiddleware, addTagMiddleware } from './middleware-runner.js'
|
|
263
249
|
export { addGlobalPermission, checkAuthPermissions } from './permissions.js'
|
|
264
250
|
export { hasScopes, verifyScopes } from './scopes.js'
|
|
265
251
|
export {
|
|
@@ -267,11 +253,6 @@ export {
|
|
|
267
253
|
stopSingletonServices,
|
|
268
254
|
pikkuServerLifecycle,
|
|
269
255
|
} from './utils.js'
|
|
270
|
-
export {
|
|
271
|
-
getSingletonServices,
|
|
272
|
-
getCreateWireServices,
|
|
273
|
-
setSingletonServices,
|
|
274
|
-
} from './pikku-state.js'
|
|
275
256
|
export { clearPikkuRuntimeState } from './test-utils.js'
|
|
276
257
|
export {
|
|
277
258
|
type ScheduledTaskInfo,
|
|
@@ -131,6 +131,56 @@ describe('authCookie middleware', () => {
|
|
|
131
131
|
assert(setCookies[0].options.expires instanceof Date)
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
+
test('clears the cookie on logout instead of re-minting a credential', async () => {
|
|
135
|
+
const SessionService = new PikkuSessionService<CoreUserSession>()
|
|
136
|
+
let encodeCalled = false
|
|
137
|
+
const jwtService = {
|
|
138
|
+
// A JWT of an absent session still decodes to a truthy {iat,exp} the
|
|
139
|
+
// runner accepts — so encode must never be called on logout.
|
|
140
|
+
encode: async () => {
|
|
141
|
+
encodeCalled = true
|
|
142
|
+
return 'reminted-jwt'
|
|
143
|
+
},
|
|
144
|
+
decode: async () => ({ userId: 'user123' }),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const middleware = authCookie({
|
|
148
|
+
name: 'session',
|
|
149
|
+
expiresIn: { value: 30, unit: 'day' },
|
|
150
|
+
options: { httpOnly: true },
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const mockResponse = createMockHTTPResponse()
|
|
154
|
+
|
|
155
|
+
await middleware(
|
|
156
|
+
{ jwt: jwtService, logger: createMockLogger() } as any,
|
|
157
|
+
{
|
|
158
|
+
...createMiddlewareSessionWireProps(SessionService),
|
|
159
|
+
http: {
|
|
160
|
+
request: createMockHTTPRequest({ session: 'existing-jwt' }),
|
|
161
|
+
response: mockResponse,
|
|
162
|
+
},
|
|
163
|
+
} as any,
|
|
164
|
+
async () => {
|
|
165
|
+
// The logout handler clears the session.
|
|
166
|
+
SessionService.clear()
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const setCookies = mockResponse.getCookies()
|
|
171
|
+
assert.equal(setCookies.length, 1, 'a clearing cookie must be written')
|
|
172
|
+
const cleared = setCookies[0]!
|
|
173
|
+
assert.equal(cleared.name, 'session')
|
|
174
|
+
assert.equal(cleared.value, '', 'the cookie value must be empty')
|
|
175
|
+
assert.equal(cleared.options.maxAge, 0)
|
|
176
|
+
assert.ok(
|
|
177
|
+
cleared.options.expires instanceof Date &&
|
|
178
|
+
cleared.options.expires.getTime() === 0,
|
|
179
|
+
'the cookie must expire at the epoch'
|
|
180
|
+
)
|
|
181
|
+
assert.equal(encodeCalled, false, 'must not sign an absent session')
|
|
182
|
+
})
|
|
183
|
+
|
|
134
184
|
test('should not set cookie when session does not change', async () => {
|
|
135
185
|
const mockUserSession: CoreUserSession = { userId: 'user789' }
|
|
136
186
|
const SessionService = new PikkuSessionService<CoreUserSession>()
|
|
@@ -7,6 +7,27 @@ import { getRelativeTimeOffsetFromNow } from '../time-utils.js'
|
|
|
7
7
|
* Reads a JWT session from a cookie, and re-issues the cookie after the
|
|
8
8
|
* request whenever the session changed (e.g. after login).
|
|
9
9
|
*/
|
|
10
|
+
/** Standard JWT registered claims — present on a token even with no session. */
|
|
11
|
+
const JWT_REGISTERED_CLAIMS = new Set([
|
|
12
|
+
'iat',
|
|
13
|
+
'exp',
|
|
14
|
+
'nbf',
|
|
15
|
+
'iss',
|
|
16
|
+
'aud',
|
|
17
|
+
'sub',
|
|
18
|
+
'jti',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether a decoded payload carries an actual session, as opposed to being
|
|
23
|
+
* absent or a bare JWT envelope of `{ iat, exp }`. True only for a non-null
|
|
24
|
+
* object with at least one key that is not a registered JWT claim.
|
|
25
|
+
*/
|
|
26
|
+
const hasSessionIdentity = (session: unknown): boolean => {
|
|
27
|
+
if (!session || typeof session !== 'object') return false
|
|
28
|
+
return Object.keys(session).some((key) => !JWT_REGISTERED_CLAIMS.has(key))
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
export const authCookie = pikkuMiddlewareFactory<{
|
|
11
32
|
name: string
|
|
12
33
|
options: SerializeOptions
|
|
@@ -32,7 +53,12 @@ export const authCookie = pikkuMiddlewareFactory<{
|
|
|
32
53
|
const cookieValue = http.request.cookie(name)
|
|
33
54
|
if (cookieValue && jwtService) {
|
|
34
55
|
const userSession = await jwtService.decode(cookieValue)
|
|
35
|
-
|
|
56
|
+
// Not just truthy: a JWT of an absent session still decodes to a
|
|
57
|
+
// `{ iat, exp }` object, which is truthy and which the function runner
|
|
58
|
+
// would accept as a session. Require at least one claim that is not a
|
|
59
|
+
// standard JWT timestamp, so an identity-less token is treated as no
|
|
60
|
+
// session rather than as an authenticated one.
|
|
61
|
+
if (userSession && hasSessionIdentity(userSession)) {
|
|
36
62
|
setSession?.(userSession)
|
|
37
63
|
}
|
|
38
64
|
}
|
|
@@ -45,7 +71,17 @@ export const authCookie = pikkuMiddlewareFactory<{
|
|
|
45
71
|
|
|
46
72
|
if (hasSessionChanged?.()) {
|
|
47
73
|
const currentSession = await getSession?.()
|
|
48
|
-
|
|
74
|
+
// Logout path: `clearSession()` marks the session changed while leaving
|
|
75
|
+
// it absent. Re-signing that into a fresh, unexpired cookie would hand
|
|
76
|
+
// the browser a credential on the way out — so clear the cookie instead
|
|
77
|
+
// of minting one, and never call `encode` with an absent session.
|
|
78
|
+
if (!hasSessionIdentity(currentSession)) {
|
|
79
|
+
http.response.cookie(name, '', {
|
|
80
|
+
...mergedOptions,
|
|
81
|
+
expires: new Date(0),
|
|
82
|
+
maxAge: 0,
|
|
83
|
+
})
|
|
84
|
+
} else if (jwtService) {
|
|
49
85
|
http.response.cookie(
|
|
50
86
|
name,
|
|
51
87
|
await jwtService.encode(expiresIn, currentSession),
|
package/src/middleware/cors.ts
CHANGED
|
@@ -72,7 +72,8 @@ export const cors = pikkuMiddlewareFactory<{
|
|
|
72
72
|
|
|
73
73
|
if (request.method() === 'options') {
|
|
74
74
|
response.header('Access-Control-Max-Age', String(maxAge))
|
|
75
|
-
|
|
75
|
+
// 204 carries no body, and `json` has no no-content overload.
|
|
76
|
+
response.status(204).json(undefined as never)
|
|
76
77
|
return
|
|
77
78
|
}
|
|
78
79
|
|
package/src/middleware/index.ts
CHANGED
|
@@ -263,6 +263,49 @@ describe('pikkuRemoteAuthMiddleware', () => {
|
|
|
263
263
|
)
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
+
test('enforces the fn binding even when the path prefix varies in case', async () => {
|
|
267
|
+
// The router matches /remote/rpc/ case-insensitively, so /Remote/RPC/
|
|
268
|
+
// reaches the same handler. The fn-binding check must run for it too, or
|
|
269
|
+
// a token scoped to one function invokes any other by varying the case.
|
|
270
|
+
await assert.rejects(
|
|
271
|
+
() =>
|
|
272
|
+
pikkuRemoteAuthMiddleware(
|
|
273
|
+
{
|
|
274
|
+
secrets: createMockSecrets(TEST_SECRET),
|
|
275
|
+
jwt: createMockJWT({ aud: 'pikku-remote', fn: 'allowedFunc' }),
|
|
276
|
+
} as any,
|
|
277
|
+
{
|
|
278
|
+
http: {
|
|
279
|
+
request: createMockRequest(
|
|
280
|
+
{ authorization: 'Bearer valid-token' },
|
|
281
|
+
'/Remote/RPC/otherFunc'
|
|
282
|
+
),
|
|
283
|
+
},
|
|
284
|
+
} as any,
|
|
285
|
+
async () => {}
|
|
286
|
+
),
|
|
287
|
+
(err: any) => err instanceof UnauthorizedError
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('requires authorization for a case-varied remote path when no secret is set', async () => {
|
|
292
|
+
// The no-secret branch must also treat /REMOTE/RPC/ as remote, or a
|
|
293
|
+
// misconfigured mesh silently serves it unauthenticated.
|
|
294
|
+
await assert.rejects(
|
|
295
|
+
() =>
|
|
296
|
+
pikkuRemoteAuthMiddleware(
|
|
297
|
+
{ secrets: createMockSecrets(undefined) } as any,
|
|
298
|
+
{
|
|
299
|
+
http: {
|
|
300
|
+
request: createMockRequest({}, '/REMOTE/RPC/anyFunc'),
|
|
301
|
+
},
|
|
302
|
+
} as any,
|
|
303
|
+
async () => {}
|
|
304
|
+
),
|
|
305
|
+
(err: any) => err instanceof UnauthorizedError
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
266
309
|
test('should allow when token fn matches requested function', async () => {
|
|
267
310
|
let nextCalled = false
|
|
268
311
|
await pikkuRemoteAuthMiddleware(
|
|
@@ -6,6 +6,18 @@ import {
|
|
|
6
6
|
REMOTE_SESSION_INFO,
|
|
7
7
|
} from '../crypto-utils.js'
|
|
8
8
|
|
|
9
|
+
const REMOTE_RPC_PREFIX = '/remote/rpc/'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether this request targets the remote-RPC surface. Compared case-insensitively
|
|
13
|
+
* because the router matches routes that way (path-to-regexp defaults to
|
|
14
|
+
* `sensitive: false`), so `/Remote/RPC/fn` reaches the same handler as
|
|
15
|
+
* `/remote/rpc/fn`. A case-sensitive check here let a case-varied path slip
|
|
16
|
+
* past the trust gate and the token's `fn` binding while still being dispatched.
|
|
17
|
+
*/
|
|
18
|
+
const isRemoteRpcPath = (path: string): boolean =>
|
|
19
|
+
path.toLowerCase().startsWith(REMOTE_RPC_PREFIX)
|
|
20
|
+
|
|
9
21
|
export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
|
|
10
22
|
async ({ secrets, jwt }, { http, setSession }, next) => {
|
|
11
23
|
if (!http?.request || !secrets) {
|
|
@@ -16,7 +28,7 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
|
|
|
16
28
|
try {
|
|
17
29
|
secret = (await secrets.getSecret('PIKKU_REMOTE_SECRET')).reveal()
|
|
18
30
|
} catch {
|
|
19
|
-
if (http.request.path()
|
|
31
|
+
if (isRemoteRpcPath(http.request.path())) {
|
|
20
32
|
throw new UnauthorizedError()
|
|
21
33
|
}
|
|
22
34
|
return next()
|
|
@@ -51,9 +63,11 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
|
|
|
51
63
|
throw new UnauthorizedError()
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
if (payload?.fn && http.request.path()
|
|
66
|
+
if (payload?.fn && isRemoteRpcPath(http.request.path())) {
|
|
67
|
+
// The prefix length is the same in any case, so slicing the raw path
|
|
68
|
+
// keeps the requested function name in its original case for the compare.
|
|
55
69
|
const fn = decodeURIComponent(
|
|
56
|
-
http.request.path().slice(
|
|
70
|
+
http.request.path().slice(REMOTE_RPC_PREFIX.length)
|
|
57
71
|
)
|
|
58
72
|
if (fn && payload.fn !== fn) {
|
|
59
73
|
throw new UnauthorizedError()
|
package/src/middleware-runner.ts
CHANGED
|
@@ -47,8 +47,10 @@ export const runMiddleware = async <
|
|
|
47
47
|
let result: any
|
|
48
48
|
const dispatch = async (index: number): Promise<any> => {
|
|
49
49
|
if (sorted && index < sorted.length) {
|
|
50
|
-
return await sorted[index]!(
|
|
51
|
-
|
|
50
|
+
return await sorted[index]!(
|
|
51
|
+
services as Parameters<Middleware>[0],
|
|
52
|
+
wire,
|
|
53
|
+
() => dispatch(index + 1)
|
|
52
54
|
)
|
|
53
55
|
} else if (main) {
|
|
54
56
|
result = await main()
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
* `as any` in a cast position. The word boundary and the following delimiter
|
|
12
|
+
* keep prose such as "as any URL a browser can load" out of the match.
|
|
13
|
+
*/
|
|
14
|
+
const AS_ANY = /\bas any\b(?!\s+[A-Za-z])/
|
|
15
|
+
|
|
16
|
+
const collectSourceFiles = (directory: string): string[] =>
|
|
17
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
18
|
+
const entryPath = join(directory, entry.name)
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
return entry.name === 'dist' ? [] : collectSourceFiles(entryPath)
|
|
21
|
+
}
|
|
22
|
+
return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')
|
|
23
|
+
? [entryPath]
|
|
24
|
+
: []
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('core asserts to named types, never to any', () => {
|
|
28
|
+
test('no non-test module casts through `as any`', () => {
|
|
29
|
+
const offenders = collectSourceFiles(srcRoot)
|
|
30
|
+
.filter((file) => file !== thisFile)
|
|
31
|
+
.flatMap((file) =>
|
|
32
|
+
readFileSync(file, 'utf-8')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.flatMap((line, index) =>
|
|
35
|
+
AS_ANY.test(line)
|
|
36
|
+
? [`${relative(srcRoot, file)}:${index + 1} ${line.trim()}`]
|
|
37
|
+
: []
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
offenders,
|
|
43
|
+
[],
|
|
44
|
+
'`as any` discards the target type, so a wrong value passes as readily as ' +
|
|
45
|
+
'a right one. Assert to the type actually wanted — and if the narrowing ' +
|
|
46
|
+
'cannot be expressed, say why in a knowledge note and point at it:\n' +
|
|
47
|
+
offenders.join('\n')
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('the matcher finds a cast and ignores the prose that reads like one', () => {
|
|
52
|
+
// Guards the test above: a regex that matched nothing would pass forever.
|
|
53
|
+
assert.equal(AS_ANY.test('const x = y as any'), true)
|
|
54
|
+
assert.equal(AS_ANY.test('foo(bar as any)'), true)
|
|
55
|
+
assert.equal(AS_ANY.test('a picture, as any URL a browser can load'), false)
|
|
56
|
+
})
|
|
57
|
+
})
|
package/src/permissions.test.ts
CHANGED
|
@@ -72,7 +72,9 @@ describe('runPermissions — function permissions (OR gate)', () => {
|
|
|
72
72
|
run({ funcPermissions: { grp: [async () => true, async () => false] } }),
|
|
73
73
|
{ message: 'Permission denied' }
|
|
74
74
|
)
|
|
75
|
-
await run({
|
|
75
|
+
await run({
|
|
76
|
+
funcPermissions: { grp: [async () => true, async () => true] },
|
|
77
|
+
})
|
|
76
78
|
})
|
|
77
79
|
|
|
78
80
|
test('accepts a bare permission array as a single AND branch', async () => {
|
package/src/permissions.ts
CHANGED
|
@@ -3,7 +3,24 @@ import type {
|
|
|
3
3
|
CoreUserSession,
|
|
4
4
|
PikkuWire,
|
|
5
5
|
} from './types/core.types.js'
|
|
6
|
+
import type { PikkuRPC } from './wirings/rpc/rpc-types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The wire a permission function sees. `Out = never` denies it `channel.send`.
|
|
10
|
+
*
|
|
11
|
+
* knowledge: decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md
|
|
12
|
+
*/
|
|
13
|
+
export type PermissionWire = PikkuWire<
|
|
14
|
+
any,
|
|
15
|
+
never,
|
|
16
|
+
false,
|
|
17
|
+
any,
|
|
18
|
+
PikkuRPC,
|
|
19
|
+
never,
|
|
20
|
+
never
|
|
21
|
+
>
|
|
6
22
|
import type {
|
|
23
|
+
AuthBranded,
|
|
7
24
|
CorePermissionGroup,
|
|
8
25
|
CorePikkuPermission,
|
|
9
26
|
} from './function/functions.types.js'
|
|
@@ -14,7 +31,7 @@ const verifyPermissions = async (
|
|
|
14
31
|
permissions: CorePermissionGroup,
|
|
15
32
|
services: CoreServices,
|
|
16
33
|
data: any,
|
|
17
|
-
wire:
|
|
34
|
+
wire: PermissionWire
|
|
18
35
|
): Promise<boolean> => {
|
|
19
36
|
if (!permissions) {
|
|
20
37
|
return true
|
|
@@ -34,7 +51,7 @@ const verifyPermissions = async (
|
|
|
34
51
|
return true
|
|
35
52
|
}
|
|
36
53
|
} else {
|
|
37
|
-
if (await funcs(services, data, wire
|
|
54
|
+
if (await funcs(services, data, wire)) {
|
|
38
55
|
return true
|
|
39
56
|
}
|
|
40
57
|
}
|
|
@@ -42,7 +59,6 @@ const verifyPermissions = async (
|
|
|
42
59
|
return false
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
|
|
46
62
|
const globalPermissionsCache: Record<
|
|
47
63
|
string,
|
|
48
64
|
readonly (CorePermissionGroup | CorePikkuPermission)[]
|
|
@@ -71,24 +87,6 @@ export const addGlobalPermission = (
|
|
|
71
87
|
return permissions
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
/**
|
|
75
|
-
* @deprecated Tag-level permissions were removed in #972 — permissions are now
|
|
76
|
-
* function-scoped only (declare them on the function via `pikkuFunc({ permissions })`).
|
|
77
|
-
* This throwing stub exists solely so the pinned bootstrap CLI (which still
|
|
78
|
-
* generates an `addTagPermission` wrapper) can resolve the import at build time;
|
|
79
|
-
* it is never called. Delete once `PIKKU_CLI_VERSION` in the CLI build is bumped
|
|
80
|
-
* past the release that removes tag permissions.
|
|
81
|
-
*/
|
|
82
|
-
export const addTagPermission = (
|
|
83
|
-
_tag: string,
|
|
84
|
-
_permissions: CorePermissionGroup | CorePikkuPermission[],
|
|
85
|
-
_packageName: string | null = null
|
|
86
|
-
): never => {
|
|
87
|
-
throw new Error(
|
|
88
|
-
'addTagPermission was removed in #972 — tag-level permissions no longer exist. Declare permissions on the function definition instead: pikkuFunc({ permissions }).'
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
90
|
const bucket = (
|
|
93
91
|
packageName: string | null
|
|
94
92
|
): (CorePermissionGroup | CorePikkuPermission)[] =>
|
|
@@ -141,7 +139,7 @@ export const runPermissions = async ({
|
|
|
141
139
|
}: {
|
|
142
140
|
funcPermissions?: CorePermissionGroup | CorePikkuPermission[]
|
|
143
141
|
services: CoreServices
|
|
144
|
-
wire:
|
|
142
|
+
wire: PermissionWire
|
|
145
143
|
data: any
|
|
146
144
|
packageName?: string | null
|
|
147
145
|
/** What the non-global gate is called in debug logs, e.g. 'function', 'agent'. */
|
|
@@ -188,14 +186,14 @@ export const checkAuthPermissions = async (
|
|
|
188
186
|
|
|
189
187
|
const collect = (perm: CorePermissionGroup | CorePikkuPermission) => {
|
|
190
188
|
if (typeof perm === 'function') {
|
|
191
|
-
if ((perm as
|
|
189
|
+
if ((perm as AuthBranded).__pikkuAuth) {
|
|
192
190
|
authPerms.push(perm)
|
|
193
191
|
}
|
|
194
192
|
} else if (perm && typeof perm === 'object') {
|
|
195
193
|
for (const funcs of Object.values(perm)) {
|
|
196
194
|
const arr = Array.isArray(funcs) ? funcs : [funcs]
|
|
197
195
|
for (const fn of arr) {
|
|
198
|
-
if (typeof fn === 'function' && (fn as
|
|
196
|
+
if (typeof fn === 'function' && (fn as AuthBranded).__pikkuAuth) {
|
|
199
197
|
authPerms.push(fn)
|
|
200
198
|
}
|
|
201
199
|
}
|